├── .changeset ├── README.md └── config.json ├── .eslintrc.cjs ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .turbo ├── cookies │ └── 1.cookie └── daemon │ └── 03e8599f0c2f549a-turbo.log.2024-10-25 ├── .vscode └── settings.json ├── CHANGELOG.md ├── README.md ├── bin.js ├── examples └── zero-survey │ ├── .cursorrules │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .gitignore.txt │ ├── .node-version │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── drizzle.config.ts │ ├── drizzle │ ├── 0000_grey_moonstone.sql │ ├── 0001_petite_roland_deschain.sql │ ├── 0002_happy_boomer.sql │ ├── 0003_big_flatman.sql │ ├── 0004_narrow_korvac.sql │ ├── 0005_sour_warhawk.sql │ ├── 0006_remarkable_quentin_quire.sql │ ├── 0007_typical_mandarin.sql │ ├── 0008_lethal_swordsman.sql │ └── meta │ │ ├── 0000_snapshot.json │ │ ├── 0001_snapshot.json │ │ ├── 0002_snapshot.json │ │ ├── 0003_snapshot.json │ │ ├── 0004_snapshot.json │ │ ├── 0005_snapshot.json │ │ ├── 0006_snapshot.json │ │ ├── 0007_snapshot.json │ │ ├── 0008_snapshot.json │ │ └── _journal.json │ ├── drop-in.config.js │ ├── example.env │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.d.ts │ ├── app.html │ ├── hooks.server.ts │ ├── lib │ │ ├── auth.svelte.ts │ │ ├── auth │ │ │ ├── ForgotPassword.svelte │ │ │ ├── Login.svelte │ │ │ ├── NewEmail.svelte │ │ │ ├── Signup.svelte │ │ │ ├── Verify.svelte │ │ │ └── auth_form.svelte.ts │ │ ├── components │ │ │ └── UserMenu.svelte │ │ ├── data │ │ │ ├── db.ts │ │ │ └── db_schema.ts │ │ ├── email.ts │ │ ├── new-query.svelte.ts │ │ ├── shared │ │ │ ├── abort-error.ts │ │ │ ├── arrays.test.ts │ │ │ ├── arrays.ts │ │ │ ├── asserts.ts │ │ │ ├── base62.test.ts │ │ │ ├── base62.ts │ │ │ ├── browser-env.ts │ │ │ ├── buffer-sizer.test.ts │ │ │ ├── buffer-sizer.ts │ │ │ ├── build.d.ts │ │ │ ├── build.js │ │ │ ├── config.ts │ │ │ ├── custom-key-map.ts │ │ │ ├── custom-key-set.ts │ │ │ ├── deep-clone.test.ts │ │ │ ├── deep-clone.ts │ │ │ ├── document-visible.test.ts │ │ │ ├── document-visible.ts │ │ │ ├── events │ │ │ │ └── connection-seconds.ts │ │ │ ├── expand.ts │ │ │ ├── fetch-mocker.ts │ │ │ ├── float-to-ordered-string.test.ts │ │ │ ├── float-to-ordered-string.ts │ │ │ ├── h64-with-reverse.ts │ │ │ ├── has-own.ts │ │ │ ├── headers.test.ts │ │ │ ├── headers.ts │ │ │ ├── immutable.ts │ │ │ ├── iterables.test.ts │ │ │ ├── iterables.ts │ │ │ ├── json-schema.test.ts │ │ │ ├── json-schema.ts │ │ │ ├── json.test.ts │ │ │ ├── json.ts │ │ │ ├── logging-test-utils.ts │ │ │ ├── mirror │ │ │ │ ├── is-supported-semver-range.test.ts │ │ │ │ └── is-supported-semver-range.ts │ │ │ ├── mod.ts │ │ │ ├── must.ts │ │ │ ├── navigator.ts │ │ │ ├── options.test.ts │ │ │ ├── options.ts │ │ │ ├── parse-big-int.test.ts │ │ │ ├── parse-big-int.ts │ │ │ ├── queue.test.ts │ │ │ ├── queue.ts │ │ │ ├── rand.ts │ │ │ ├── random-uint64.ts │ │ │ ├── random-values.ts │ │ │ ├── read-json-file.js │ │ │ ├── resolved-promises.ts │ │ │ ├── reverse-string.ts │ │ │ ├── set-util.test.ts │ │ │ ├── set-utils.ts │ │ │ ├── sleep.test.ts │ │ │ ├── sleep.ts │ │ │ ├── sorted-entries.ts │ │ │ ├── string-compare.ts │ │ │ ├── timed.ts │ │ │ ├── tool │ │ │ │ ├── get-external-from-package-json.js │ │ │ │ ├── inject-require.js │ │ │ │ ├── internal-packages.js │ │ │ │ └── vitest-config.ts │ │ │ ├── types.ts │ │ │ ├── valita.test.ts │ │ │ ├── valita.ts │ │ │ ├── writable.ts │ │ │ └── xxhash.ts │ │ ├── svelte-view.svelte.ts │ │ ├── types.ts │ │ └── z.svelte.ts │ ├── routes │ │ ├── (app) │ │ │ ├── +layout.svelte │ │ │ ├── +layout.ts │ │ │ ├── Footer.svelte │ │ │ ├── Header.svelte │ │ │ ├── README.md │ │ │ ├── dashboard │ │ │ │ └── +page.svelte │ │ │ ├── profile │ │ │ │ └── +page.svelte │ │ │ └── surveys │ │ │ │ └── [id] │ │ │ │ ├── +layout.svelte │ │ │ │ ├── +page.svelte │ │ │ │ └── edit │ │ │ │ ├── +page.svelte │ │ │ │ └── EditRow.svelte │ │ ├── (site) │ │ │ ├── +layout.svelte │ │ │ ├── +page.svelte │ │ │ ├── README.md │ │ │ ├── auth │ │ │ │ ├── +layout.svelte │ │ │ │ ├── confirm-email-change │ │ │ │ │ └── +page.svelte │ │ │ │ ├── confirm-password-reset │ │ │ │ │ └── [token] │ │ │ │ │ │ └── +page.svelte │ │ │ │ ├── confirm-verification │ │ │ │ │ └── +page.svelte │ │ │ │ ├── forgot-password │ │ │ │ │ └── +page.svelte │ │ │ │ ├── login │ │ │ │ │ └── +page.svelte │ │ │ │ └── signup │ │ │ │ │ └── +page.svelte │ │ │ └── survey │ │ │ │ └── [id] │ │ │ │ ├── +page.svelte │ │ │ │ └── [response_id] │ │ │ │ ├── +page.svelte │ │ │ │ ├── Answer.svelte │ │ │ │ ├── LongText.svelte │ │ │ │ ├── Rating.svelte │ │ │ │ └── Text.svelte │ │ ├── +layout.server.ts │ │ ├── +layout.svelte │ │ ├── UserWrapper.svelte │ │ └── global.css │ ├── schema.ts │ ├── state │ │ ├── README.md │ │ └── app.svelte.ts │ └── utils │ │ ├── app_guard.ts │ │ └── auth_guard.ts │ ├── svelte.config.js │ ├── tsconfig.json │ ├── vite.config.ts │ └── zero-schema.json ├── jsconfig.json ├── package.json ├── packages ├── beeper │ ├── package.json │ ├── src │ │ ├── app.d.ts │ │ ├── beeper.ts │ │ └── index.ts │ └── tsconfig.json ├── decks │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── lib │ │ │ ├── Accordion.svelte │ │ │ ├── AreYouSure.svelte │ │ │ ├── Dialog.svelte │ │ │ ├── Drawer.svelte │ │ │ ├── Menu.svelte │ │ │ ├── Pill.svelte │ │ │ ├── Pills.svelte │ │ │ ├── Share.svelte │ │ │ ├── animated-details.ts │ │ │ ├── drawer.ts │ │ │ ├── index.ts │ │ │ ├── local │ │ │ │ └── Docs.svelte │ │ │ ├── pannable.ts │ │ │ └── toast │ │ │ │ ├── Toast.svelte │ │ │ │ ├── ToastSlice.svelte │ │ │ │ ├── index.ts │ │ │ │ └── toaster.svelte.ts │ │ └── routes │ │ │ └── +page.svelte │ ├── static │ │ └── favicon.png │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── docs │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ └── launch.json │ ├── README.md │ ├── astro.config.mjs │ ├── package.json │ ├── public │ │ └── favicon.svg │ ├── src │ │ ├── assets │ │ │ ├── drop-in-light.svg │ │ │ └── drop-in.svg │ │ ├── content.config.ts │ │ ├── content │ │ │ └── docs │ │ │ │ ├── beeper │ │ │ │ └── reference.md │ │ │ │ ├── guides │ │ │ │ └── get-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── pass │ │ │ │ └── reference.md │ │ │ │ ├── plugin │ │ │ │ └── reference.md │ │ │ │ └── ramps │ │ │ │ └── reference.md │ │ └── styles │ │ │ └── custom.css │ └── tsconfig.json ├── graffiti │ ├── README.md │ ├── bin.js │ ├── build.js │ ├── drop-in.css │ ├── index.html │ ├── package-lock.json │ ├── package.json │ └── raw.js ├── pass │ ├── .gitignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── app.d.ts │ │ ├── authenticate.ts │ │ ├── client │ │ │ ├── api_calls.ts │ │ │ └── index.ts │ │ ├── client_jwt.ts │ │ ├── cookies.ts │ │ ├── db.ts │ │ ├── email.ts │ │ ├── find_user.ts │ │ ├── index.ts │ │ ├── jwt.ts │ │ ├── login.ts │ │ ├── logout.ts │ │ ├── password.ts │ │ ├── routes.ts │ │ ├── schema.ts │ │ ├── sign_up.ts │ │ ├── token.ts │ │ └── utils.ts │ └── tsconfig.json ├── plugin │ ├── global.d.ts │ ├── package.json │ ├── src │ │ ├── hook.ts │ │ └── index.ts │ └── tsconfig.json └── ramps │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ ├── app.d.ts │ ├── app.html │ ├── lib │ │ ├── auth │ │ │ ├── Login.svelte │ │ │ ├── Signup.svelte │ │ │ ├── Verify.svelte │ │ │ └── auth_form.svelte.ts │ │ └── index.ts │ └── routes │ │ └── +page.svelte │ ├── static │ └── favicon.png │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── templates └── z │ ├── .cursorrules │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .gitignore.txt │ ├── .meta.json │ ├── .node-version │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── docker │ └── docker-compose.yml │ ├── drizzle.config.ts │ ├── drizzle │ ├── 0000_grey_moonstone.sql │ ├── 0001_petite_roland_deschain.sql │ ├── 0002_happy_boomer.sql │ ├── 0003_big_flatman.sql │ └── meta │ │ ├── 0000_snapshot.json │ │ ├── 0001_snapshot.json │ │ ├── 0002_snapshot.json │ │ ├── 0003_snapshot.json │ │ └── _journal.json │ ├── drop-in.config.js │ ├── example.env │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.d.ts │ ├── app.html │ ├── db_schema.ts │ ├── hooks.server.ts │ ├── lib │ │ ├── auth │ │ │ ├── ForgotPassword.svelte │ │ │ ├── NewEmail.svelte │ │ │ └── auth_form.svelte.ts │ │ ├── components │ │ │ └── UserMenu.svelte │ │ ├── data │ │ │ └── db.ts │ │ ├── email.ts │ │ ├── queries.ts │ │ └── z.svelte.ts │ ├── routes │ │ ├── (app) │ │ │ ├── +layout.svelte │ │ │ ├── +layout.ts │ │ │ ├── Footer.svelte │ │ │ ├── Header.svelte │ │ │ ├── README.md │ │ │ ├── dashboard │ │ │ │ └── +page.svelte │ │ │ └── profile │ │ │ │ └── +page.svelte │ │ ├── (site) │ │ │ ├── +layout.server.ts │ │ │ ├── +layout.svelte │ │ │ ├── +page.svelte │ │ │ ├── README.md │ │ │ └── auth │ │ │ │ ├── confirm-email-change │ │ │ │ └── +page.svelte │ │ │ │ ├── confirm-password-reset │ │ │ │ └── [token] │ │ │ │ │ └── +page.svelte │ │ │ │ ├── forgot-password │ │ │ │ └── +page.svelte │ │ │ │ ├── login │ │ │ │ └── +page.svelte │ │ │ │ ├── signup │ │ │ │ └── +page.svelte │ │ │ │ └── verify-email │ │ │ │ └── +page.svelte │ │ ├── +layout.server.ts │ │ └── +layout.svelte │ ├── schema.ts │ ├── state │ │ ├── README.md │ │ └── app.svelte.ts │ └── utils │ │ ├── app_guard.ts │ │ └── auth_guard.ts │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type { import("eslint").Linter.Config } */ 2 | module.exports = { 3 | root: true, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:svelte/recommended', 8 | 'prettier', 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['@typescript-eslint'], 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaVersion: 2020, 15 | extraFileExtensions: ['.svelte'], 16 | }, 17 | env: { 18 | browser: true, 19 | es2017: true, 20 | node: true, 21 | }, 22 | overrides: [ 23 | { 24 | files: ['*.svelte'], 25 | parser: 'svelte-eslint-parser', 26 | parserOptions: { 27 | parser: '@typescript-eslint/parser', 28 | }, 29 | }, 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # Or your primary branch 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v4 17 | # Fetch all history so Changesets can determine changed packages 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Setup PNPM 22 | uses: pnpm/action-setup@v4 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 # Use your project's node version 28 | cache: 'pnpm' 29 | 30 | - name: Install Dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | # Optional: Add build step if needed before versioning/publishing 34 | # - name: Build Packages 35 | # run: pnpm turbo build --filter='!./apps/*' # Example: Build all packages except those in apps/ 36 | 37 | - name: Create Release Pull Request or Publish to npm 38 | id: changesets 39 | uses: changesets/action@v1 40 | with: 41 | # This command will create a PR if changesets are present and run publish if on main/master 42 | # It automatically bumps versions, commits, tags, and publishes 43 | publish: pnpm publish -r --no-git-checks 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # Needs to be configured in repo secrets -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | .svelte-kit 4 | **/.svelte-kit 5 | /package 6 | /dist 7 | .env 8 | .env.* 9 | !.env.example 10 | 11 | .DS_STORE 12 | **/dist/ 13 | **/build/ 14 | *.tgz 15 | 16 | .turbo -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | engine-strict=true 3 | link-workspace-packages=true 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "useTabs": true 5 | } 6 | -------------------------------------------------------------------------------- /.turbo/cookies/1.cookie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stolinski/drop-in/bbeff69a9b5568ed49517b3936cba67a8ae0f1cc/.turbo/cookies/1.cookie -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["pocketbase"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drop-in/new 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 730a653: Removes workspace from copied template 8 | -------------------------------------------------------------------------------- /examples/zero-survey/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /examples/zero-survey/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type { import("eslint").Linter.Config } */ 2 | module.exports = { 3 | root: true, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:svelte/recommended', 8 | 'prettier', 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['@typescript-eslint'], 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaVersion: 2020, 15 | extraFileExtensions: ['.svelte'], 16 | }, 17 | env: { 18 | browser: true, 19 | es2017: true, 20 | node: true, 21 | }, 22 | overrides: [ 23 | { 24 | files: ['*.svelte'], 25 | parser: 'svelte-eslint-parser', 26 | parserOptions: { 27 | parser: '@typescript-eslint/parser', 28 | }, 29 | }, 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /examples/zero-survey/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | /.svelte-kit 4 | /package 5 | .env 6 | .env.* 7 | !.env.example 8 | 9 | .DS_Store -------------------------------------------------------------------------------- /examples/zero-survey/.gitignore.txt: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | /.svelte-kit 4 | /package 5 | .env 6 | .env.* 7 | !.env.example 8 | -------------------------------------------------------------------------------- /examples/zero-survey/.node-version: -------------------------------------------------------------------------------- 1 | 17.0.1 -------------------------------------------------------------------------------- /examples/zero-survey/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | link-workspace-packages=true 3 | -------------------------------------------------------------------------------- /examples/zero-survey/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /examples/zero-survey/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreFiles": ["package.json"], 3 | "overrides": [ 4 | { 5 | "files": "*.svelte", 6 | "options": { 7 | "parser": "svelte" 8 | } 9 | } 10 | ], 11 | "plugins": ["prettier-plugin-svelte"], 12 | "printWidth": 100, 13 | "singleQuote": true, 14 | "trailingComma": "none", 15 | "useTabs": true 16 | } 17 | -------------------------------------------------------------------------------- /examples/zero-survey/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit'; 2 | 3 | export default defineConfig({ 4 | schema: './src/lib/data/db_schema.ts', 5 | dialect: 'postgresql', 6 | dbCredentials: { 7 | url: process.env.DATABASE_URL, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/zero-survey/drizzle/0000_grey_moonstone.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "profile" ( 2 | "id" varchar PRIMARY KEY NOT NULL, 3 | "user_id" varchar NOT NULL 4 | ); 5 | --> statement-breakpoint 6 | CREATE TABLE IF NOT EXISTS "refresh_token" ( 7 | "id" varchar PRIMARY KEY NOT NULL, 8 | "user_id" varchar NOT NULL, 9 | "token" varchar(255) NOT NULL, 10 | "created_at" timestamp DEFAULT now() NOT NULL, 11 | "expires_at" timestamp NOT NULL 12 | ); 13 | --> statement-breakpoint 14 | CREATE TABLE IF NOT EXISTS "user" ( 15 | "id" varchar PRIMARY KEY NOT NULL, 16 | "email" varchar(255) NOT NULL, 17 | "password_hash" varchar(255) NOT NULL, 18 | "created_at" timestamp DEFAULT now() NOT NULL, 19 | "updated_at" timestamp DEFAULT now() NOT NULL, 20 | "verified" boolean DEFAULT false NOT NULL, 21 | "verification_token" varchar(255), 22 | CONSTRAINT "user_email_unique" UNIQUE("email") 23 | ); 24 | --> statement-breakpoint 25 | DO $$ BEGIN 26 | ALTER TABLE "profile" ADD CONSTRAINT "profile_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; 27 | EXCEPTION 28 | WHEN duplicate_object THEN null; 29 | END $$; 30 | --> statement-breakpoint 31 | DO $$ BEGIN 32 | ALTER TABLE "refresh_token" ADD CONSTRAINT "refresh_token_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; 33 | EXCEPTION 34 | WHEN duplicate_object THEN null; 35 | END $$; 36 | -------------------------------------------------------------------------------- /examples/zero-survey/drizzle/0001_petite_roland_deschain.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "profile" ADD COLUMN "username" varchar NOT NULL; -------------------------------------------------------------------------------- /examples/zero-survey/drizzle/0002_happy_boomer.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "profile" RENAME COLUMN "username" TO "name"; -------------------------------------------------------------------------------- /examples/zero-survey/drizzle/0003_big_flatman.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "profile" ADD COLUMN "avatar" varchar; -------------------------------------------------------------------------------- /examples/zero-survey/drizzle/0005_sour_warhawk.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "questions" ADD COLUMN "description" text; -------------------------------------------------------------------------------- /examples/zero-survey/drizzle/0006_remarkable_quentin_quire.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "questions" RENAME COLUMN "order_num" TO "order";--> statement-breakpoint 2 | ALTER TABLE "questions" ADD COLUMN "config" jsonb DEFAULT '{"required":false}'::jsonb NOT NULL; -------------------------------------------------------------------------------- /examples/zero-survey/drizzle/0007_typical_mandarin.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "user" DROP COLUMN "created_at";--> statement-breakpoint 2 | ALTER TABLE "user" DROP COLUMN "updated_at"; -------------------------------------------------------------------------------- /examples/zero-survey/drizzle/0008_lethal_swordsman.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "user" ADD COLUMN "created_at" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "user" ADD COLUMN "updated_at" timestamp DEFAULT now() NOT NULL; -------------------------------------------------------------------------------- /examples/zero-survey/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1729802614860, 9 | "tag": "0000_grey_moonstone", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1729875764211, 16 | "tag": "0001_petite_roland_deschain", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "7", 22 | "when": 1729881836443, 23 | "tag": "0002_happy_boomer", 24 | "breakpoints": true 25 | }, 26 | { 27 | "idx": 3, 28 | "version": "7", 29 | "when": 1731085971419, 30 | "tag": "0003_big_flatman", 31 | "breakpoints": true 32 | }, 33 | { 34 | "idx": 4, 35 | "version": "7", 36 | "when": 1731699278991, 37 | "tag": "0004_narrow_korvac", 38 | "breakpoints": true 39 | }, 40 | { 41 | "idx": 5, 42 | "version": "7", 43 | "when": 1732899862162, 44 | "tag": "0005_sour_warhawk", 45 | "breakpoints": true 46 | }, 47 | { 48 | "idx": 6, 49 | "version": "7", 50 | "when": 1733522441302, 51 | "tag": "0006_remarkable_quentin_quire", 52 | "breakpoints": true 53 | }, 54 | { 55 | "idx": 7, 56 | "version": "7", 57 | "when": 1733832289748, 58 | "tag": "0007_typical_mandarin", 59 | "breakpoints": true 60 | }, 61 | { 62 | "idx": 8, 63 | "version": "7", 64 | "when": 1733848339041, 65 | "tag": "0008_lethal_swordsman", 66 | "breakpoints": true 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /examples/zero-survey/drop-in.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | email: { 3 | from: 'fake@changeme.com' 4 | }, 5 | db: { 6 | url: process.env.DATABASE_URL 7 | }, 8 | app: { 9 | public: { 10 | url: 'http://localhost:5173', 11 | name: 'Drop In', 12 | route: '/dashboard' 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /examples/zero-survey/example.env: -------------------------------------------------------------------------------- 1 | # Required 2 | PUBLIC_PB_URL= 3 | 4 | # Optional 5 | # For `types` script 6 | PB_TYPEGEN_URL= 7 | PB_TYPEGEN_EMAIL= 8 | PB_TYPEGEN_PASSWORD= 9 | -------------------------------------------------------------------------------- /examples/zero-survey/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@drop-in/beeper": "workspace:^", 4 | "@drop-in/decks": "workspace:^", 5 | "@drop-in/graffiti": "^0.3.4", 6 | "@drop-in/pass": "workspace:^", 7 | "@drop-in/plugin": "workspace:^", 8 | "@rocicorp/zero": "^0.18.2025042300", 9 | "drizzle-orm": "^0.43.1", 10 | "nanoid": "^5.1.5", 11 | "pg": "^8.15.6", 12 | "zero-svelte": "^0.3.3" 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "^6.0.0", 16 | "@sveltejs/kit": "^2.20.7", 17 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 18 | "@types/pg": "^8.11.14", 19 | "@typescript-eslint/eslint-plugin": "^8.31.0", 20 | "@typescript-eslint/parser": "^8.31.0", 21 | "drizzle-kit": "^0.31.0", 22 | "prettier": "^3.5.3", 23 | "prettier-plugin-svelte": "^3.3.3", 24 | "svelte": "^5.28.2", 25 | "svelte-check": "^4.1.6", 26 | "svelte-preprocess": "^6.0.2", 27 | "tslib": "^2.8.1", 28 | "typescript": "^5.8.3", 29 | "vite": "^6.3.3" 30 | }, 31 | "engines": { 32 | "node": ">20.11.1" 33 | }, 34 | "name": "dropin-survey", 35 | "scripts": { 36 | "build": "vite build", 37 | "check": "svelte-check --tsconfig ./tsconfig.json", 38 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", 39 | "dev": "vite dev", 40 | "format": "prettier --write --plugin-search-dir=. .", 41 | "lint": "prettier --check --plugin-search-dir=. . && eslint .", 42 | "make-package": "node ./src/packages/@drop-in/tools/package.js", 43 | "package": "svelte-kit package", 44 | "prepare": "svelte-kit sync", 45 | "preview": "svelte-kit preview", 46 | "zero:server": "npx zero-cache -p src/schema.ts", 47 | "db:change": "drizzle-kit generate; drizzle-kit migrate; pnpm exec zero-build-schema -p 'src/schema.ts'; rm /tmp/zerotest-sync-replica.db*;" 48 | }, 49 | "type": "module", 50 | "version": "0.0.4" 51 | } 52 | -------------------------------------------------------------------------------- /examples/zero-survey/src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // See https://kit.svelte.dev/docs/types#app 4 | // for information about these interfaces 5 | // and what to do when importing types 6 | declare global { 7 | namespace App { 8 | // interface Locals {} 9 | // interface Platform {} 10 | // interface Session {} 11 | // interface Stuff {} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/zero-survey/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | %sveltekit.head% 15 | 16 | 17 |
%sveltekit.body%
18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/zero-survey/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { sequence } from '@sveltejs/kit/hooks'; 2 | import { beeper } from '@drop-in/beeper'; 3 | import { pass_routes } from '@drop-in/pass'; 4 | 5 | beeper.send({ 6 | to: 'test@test.com', 7 | subject: 'Test', 8 | html: '

Test

' 9 | }); 10 | 11 | export const handle = sequence(pass_routes); 12 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/auth.svelte.ts: -------------------------------------------------------------------------------- 1 | import { get_login } from '@drop-in/pass/client'; 2 | import type { User } from '@drop-in/pass/schema'; 3 | import type { Profile } from './data/db_schema'; 4 | 5 | // This is a way to keep track of the user globally 6 | class Auth { 7 | user: Partial }> = $state({}); 8 | auth: { sub: string | undefined; jwt: string | undefined } | null = $state(null); 9 | constructor() { 10 | this.auth = get_login(); 11 | } 12 | 13 | set_user(user: User | {}) { 14 | console.log('user', user); 15 | this.user = user; 16 | } 17 | } 18 | export const auth = new Auth(); 19 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/auth/ForgotPassword.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | Forgot Password 32 | {#if auth.status === 'SUCCESS'} 33 |

Please check your email for password reset instructions

34 | {:else} 35 |
36 |
37 | 38 |
39 |
40 | 45 |
46 |
47 | {#if auth.error_message} 48 |

{auth.error_message}

49 | {/if} 50 |
51 | {/if} 52 | 53 |
54 |

55 | Need an account? 56 | Sign Up 57 |

58 |

59 | Know your account? 60 | Login 61 |

62 |
63 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/auth/NewEmail.svelte: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/auth/Signup.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 | Sign up 46 |
47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 | 60 |
61 |
62 | {#if auth.error_message} 63 |

{auth.error_message}

64 | {/if} 65 |
66 |
67 |

Already have an account?

68 | Sign in 69 |
70 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/auth/Verify.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {#if !verified_sent} 15 |

16 | Your email is not verified. 17 | 18 |

19 | {:else} 20 |

Verification email sent to {user.email}. Please check your email.

21 | {/if} 22 |
23 | 24 | 36 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/auth/auth_form.svelte.ts: -------------------------------------------------------------------------------- 1 | import { goto } from '$app/navigation'; 2 | import { toaster } from '@drop-in/decks'; 3 | 4 | export class AuthForm { 5 | status: 'LOADING' | 'SUCCESS' | 'ERROR' | 'INITIAL' = $state('INITIAL'); 6 | error_message: string | undefined = $state(); 7 | 8 | loading() { 9 | this.status = 'LOADING'; 10 | } 11 | 12 | error(e_message: string) { 13 | toaster.error(e_message); 14 | this.status = 'ERROR'; 15 | this.error_message = e_message; 16 | } 17 | 18 | success(route: string | boolean = DROP_IN.app.route, message: string = 'Success') { 19 | // Reset the Zero instance 20 | toaster.success(message); 21 | this.error_message = undefined; 22 | this.status = 'SUCCESS'; 23 | // Redirect to wherever post login 24 | if (route && typeof route === 'string') goto(route, { invalidateAll: true }); 25 | } 26 | } 27 | 28 | // TODO: Magic Link 29 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/components/UserMenu.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | {#if z.current.userID !== 'anon'} 19 | 20 | {#snippet button()} 21 | {#if user?.current?.profile?.avatar} 22 | avatar 23 | {:else} 24 | {user?.current?.email?.[0]} 25 | {/if} 26 | {/snippet} 27 |
28 | Profile 29 |
30 | 42 |
43 |
44 | {/if} 45 | 46 | 61 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/data/db.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/node-postgres'; 2 | import { schema } from './db_schema'; 3 | 4 | // The db connection 5 | // We use drizzle to connect to the database 6 | // We use the global drop_in_config to get the db url 7 | // This is the same db url that is in the .env file 8 | 9 | // The question here is really how much this should be possibly created in teh app itself so that there aren't multiple connections 10 | // But tbh not sure how much of a problem that is. LMK what you think. The goal is to make the user do a little bit of work as possible 11 | // To get up and running. 12 | if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL required'); 13 | 14 | export const db = drizzle({ 15 | connection: process.env.DATABASE_URL, 16 | schema 17 | }); 18 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/email.ts: -------------------------------------------------------------------------------- 1 | import { Beeper } from '@drop-in/beeper'; 2 | 3 | export const beeper = new Beeper(); 4 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/new-query.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { Query as QueryParam, QueryType, Smash } from '@rocicorp/zero'; 2 | import { type TableSchema, type AdvancedQuery } from '@rocicorp/zero/advanced'; 3 | import { svelteViewFactory } from './svelte-view.svelte.js'; 4 | 5 | export class Query { 6 | data = $state() as unknown as Smash; 7 | q: QueryParam; 8 | 9 | constructor(q: QueryParam) { 10 | this.q = q; 11 | this.data = {} as unknown as Smash; 12 | 13 | // We probably don't need an effect here? maybe you would want one just to destroy on cleanup? 14 | // NOt sure the best way to handle this. 15 | 16 | const view = this.q.materialize(svelteViewFactory); 17 | this.data = view; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/abort-error.ts: -------------------------------------------------------------------------------- 1 | export class AbortError extends Error { 2 | name = 'AbortError'; 3 | } 4 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/arrays.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, test} from 'vitest'; 2 | import {defined} from './arrays.js'; 3 | 4 | describe('shared/arrays', () => { 5 | type Case = { 6 | input: (number | undefined)[]; 7 | output: number[]; 8 | }; 9 | 10 | const cases: Case[] = [ 11 | { 12 | input: [], 13 | output: [], 14 | }, 15 | { 16 | input: [undefined], 17 | output: [], 18 | }, 19 | { 20 | input: [undefined, undefined], 21 | output: [], 22 | }, 23 | { 24 | input: [0, undefined], 25 | output: [0], 26 | }, 27 | { 28 | input: [undefined, 0], 29 | output: [0], 30 | }, 31 | { 32 | input: [undefined, 0, undefined], 33 | output: [0], 34 | }, 35 | { 36 | input: [undefined, 0, 1], 37 | output: [0, 1], 38 | }, 39 | { 40 | input: [0, undefined, 1], 41 | output: [0, 1], 42 | }, 43 | { 44 | input: [0, undefined, 0, 1], 45 | output: [0, 0, 1], 46 | }, 47 | { 48 | input: [0, undefined, 0, 1, undefined], 49 | output: [0, 0, 1], 50 | }, 51 | { 52 | input: [0, undefined, 0, undefined, 1, undefined], 53 | output: [0, 0, 1], 54 | }, 55 | { 56 | input: [2, 1, 0, undefined, 0, undefined, 1, undefined, 2], 57 | output: [2, 1, 0, 0, 1, 2], 58 | }, 59 | ]; 60 | 61 | for (const c of cases) { 62 | test(`defined(${JSON.stringify(c.input)})`, () => { 63 | const output = defined(c.input); 64 | expect(output).toEqual(c.output); 65 | if (output.length === c.input.length) { 66 | expect(output).toBe(c.input); // No copy 67 | } 68 | }); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/arrays.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns `arr` as is if none of the elements are `undefined`. 3 | * Otherwise returns a new array with only defined elements in `arr`. 4 | */ 5 | export function defined(arr: (T | undefined)[]): T[] { 6 | // avoid an array copy if possible 7 | let i = arr.findIndex(x => x === undefined); 8 | if (i < 0) { 9 | return arr as T[]; 10 | } 11 | const defined: T[] = arr.slice(0, i) as T[]; 12 | for (i++; i < arr.length; i++) { 13 | const x = arr[i]; 14 | if (x !== undefined) { 15 | defined.push(x); 16 | } 17 | } 18 | return defined; 19 | } 20 | 21 | export function areEqual(arr1: readonly T[], arr2: readonly T[]): boolean { 22 | return arr1.length === arr2.length && arr1.every((e, i) => e === arr2[i]); 23 | } 24 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/base62.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from 'vitest'; 2 | import {encode} from './base62.js'; 3 | 4 | test('it should encode base62', () => { 5 | expect(encode(0n)).toBe('0'); 6 | expect(encode(1n)).toBe('1'); 7 | expect(encode(9n)).toBe('9'); 8 | expect(encode(10n)).toBe('A'); 9 | expect(encode(35n)).toBe('Z'); 10 | expect(encode(36n)).toBe('a'); 11 | expect(encode(61n)).toBe('z'); 12 | expect(encode(62n)).toBe('10'); 13 | expect(encode(2n ** 31n - 1n)).toBe('2LKcb1'); 14 | expect(encode(0x7fff_ffffn)).toBe('2LKcb1'); 15 | expect(encode(2n ** 64n - 1n)).toBe('LygHa16AHYF'); 16 | expect(encode(0xffff_ffff_ffff_ffffn)).toBe('LygHa16AHYF'); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/base62.ts: -------------------------------------------------------------------------------- 1 | const alphabet = 2 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 3 | 4 | export function encode(n: bigint): string { 5 | if (n === 0n) { 6 | return '0'; 7 | } 8 | let result = ''; 9 | const base = BigInt(alphabet.length); 10 | while (n > 0n) { 11 | result = alphabet[Number(n % base)] + result; 12 | n = n / base; 13 | } 14 | return result; 15 | } 16 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/browser-env.ts: -------------------------------------------------------------------------------- 1 | // Helpers for some objects from the browser environment. These are wrapped in 2 | // functions because Replicache runs in environments that do not have these 3 | // objects (such as Web Workers, Deno etc). 4 | 5 | type GlobalThis = typeof globalThis; 6 | 7 | export function getBrowserGlobal( 8 | name: T, 9 | ): GlobalThis[T] | undefined { 10 | return globalThis[name]; 11 | } 12 | 13 | /** 14 | * Returns the global method with the given name, bound to the global object. 15 | * This is important because some methods (e.g. `requestAnimationFrame`) are not 16 | * bound to the global object by default. 17 | * 18 | * If you end up using {@linkcode getBrowserGlobal} instead in a case like this: 19 | * 20 | * ```js 21 | * this.#raf = getBrowserGlobal('requestAnimationFrame') ?? rafFallback; 22 | * ... 23 | * this.#raf(() => ...); 24 | * ``` 25 | * 26 | * You will end up with `Uncaught TypeError: Illegal invocation` because `this` 27 | * is not bound to the global object 28 | */ 29 | export function getBrowserGlobalMethod( 30 | name: T, 31 | ): GlobalThis[T] | undefined { 32 | return globalThis[name]?.bind(globalThis); 33 | } 34 | 35 | export function mustGetBrowserGlobal( 36 | name: T, 37 | ): GlobalThis[T] { 38 | const r = getBrowserGlobal(name); 39 | if (r === undefined) { 40 | throw new Error( 41 | `Unsupported JavaScript environment: Could not find ${name}.`, 42 | ); 43 | } 44 | return r; 45 | } 46 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/build.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {boolean=} minify 3 | * @param {boolean=} metafile 4 | */ 5 | export function sharedOptions( 6 | minify?: boolean | undefined, 7 | metafile?: boolean | undefined, 8 | ): { 9 | readonly bundle: true; 10 | readonly target: 'es2022'; 11 | readonly format: 'esm'; 12 | readonly external: string[]; 13 | readonly minify: boolean; 14 | readonly sourcemap: true; 15 | readonly metafile: boolean; 16 | }; 17 | /** 18 | * @param {'debug'|'release'|'unknown'} mode 19 | * @return {Record} 20 | */ 21 | export function makeDefine( 22 | mode?: 'debug' | 'release' | 'unknown', 23 | ): Record; 24 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/build.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-env node, es2022 */ 3 | 4 | import {readFileSync} from 'node:fs'; 5 | 6 | const external = [ 7 | 'node:*', 8 | '@badrap/valita', 9 | '@rocicorp/datadog-util', 10 | '@rocicorp/lock', 11 | '@rocicorp/logger', 12 | '@rocicorp/resolver', 13 | 'replicache', 14 | ]; 15 | 16 | /** 17 | * @param {boolean=} minify 18 | * @param {boolean=} metafile 19 | */ 20 | export function sharedOptions(minify = true, metafile = false) { 21 | const opts = /** @type {const} */ ({ 22 | bundle: true, 23 | target: 'es2022', 24 | format: 'esm', 25 | external, 26 | minify, 27 | sourcemap: true, 28 | metafile, 29 | }); 30 | if (minify) { 31 | return /** @type {const} */ ({ 32 | ...opts, 33 | mangleProps: /^_./, 34 | reserveProps: /^__.*__$/, 35 | }); 36 | } 37 | return opts; 38 | } 39 | 40 | /** 41 | * @param {string} name 42 | * @return {string} 43 | */ 44 | 45 | function getVersion(name) { 46 | const url = new URL(`../../${name}/package.json`, import.meta.url); 47 | const s = readFileSync(url, 'utf-8'); 48 | return JSON.parse(s).version; 49 | } 50 | 51 | /** 52 | * @param {'debug'|'release'|'unknown'} mode 53 | * @return {Record} 54 | */ 55 | export function makeDefine(mode = 'unknown') { 56 | /** @type {Record} */ 57 | const define = { 58 | ['process.env.REPLICACHE_VERSION']: JSON.stringify( 59 | getVersion('replicache'), 60 | ), 61 | ['process.env.ZERO_VERSION']: JSON.stringify(getVersion('zero')), 62 | ['TESTING']: 'false', 63 | }; 64 | if (mode === 'unknown') { 65 | return define; 66 | } 67 | return { 68 | ...define, 69 | 'process.env.NODE_ENV': mode === 'debug' ? '"development"' : '"production"', 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/config.ts: -------------------------------------------------------------------------------- 1 | declare const process: { 2 | env: { 3 | // eslint-disable-next-line @typescript-eslint/naming-convention 4 | NODE_ENV?: string; 5 | }; 6 | }; 7 | 8 | export const isProd = process.env.NODE_ENV === 'production'; 9 | 10 | export {isProd as skipAssertJSONValue}; 11 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/custom-key-set.ts: -------------------------------------------------------------------------------- 1 | type Primitive = undefined | null | boolean | string | number | symbol | bigint; 2 | 3 | /** 4 | * A {@link Set} that uses a custom value transformation function to convert values 5 | * to a primitive type that can be used as a {@link Set} value. 6 | * 7 | * This allows for using objects as values in a {@link Set} without worrying about 8 | * reference equality. 9 | */ 10 | 11 | export class CustomKeySet implements Set { 12 | readonly [Symbol.toStringTag] = 'CustomKeySet'; 13 | readonly #toKey: (value: V) => Primitive; 14 | readonly #map = new Map(); 15 | 16 | constructor(toKey: (value: V) => Primitive, iterable?: Iterable | null) { 17 | this.#toKey = toKey; 18 | if (iterable) { 19 | for (const value of iterable ?? []) { 20 | this.#map.set(toKey(value), value); 21 | } 22 | } 23 | } 24 | 25 | add(value: V): this { 26 | this.#map.set(this.#toKey(value), value); 27 | return this; 28 | } 29 | 30 | clear(): void { 31 | this.#map.clear(); 32 | } 33 | 34 | delete(value: V): boolean { 35 | return this.#map.delete(this.#toKey(value)); 36 | } 37 | 38 | forEach( 39 | callbackfn: (value: V, value2: V, set: Set) => void, 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | thisArg?: any, 42 | ): void { 43 | this.#map.forEach(value => { 44 | callbackfn.call(thisArg, value, value, this); 45 | }); 46 | } 47 | 48 | has(value: V): boolean { 49 | return this.#map.has(this.#toKey(value)); 50 | } 51 | 52 | get size(): number { 53 | return this.#map.size; 54 | } 55 | 56 | *entries(): IterableIterator<[V, V]> { 57 | for (const value of this.#map.values()) { 58 | yield [value, value]; 59 | } 60 | } 61 | 62 | keys(): IterableIterator { 63 | return this.#map.values(); 64 | } 65 | 66 | values(): IterableIterator { 67 | return this.#map.values(); 68 | } 69 | 70 | [Symbol.iterator](): IterableIterator { 71 | return this.#map.values(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/deep-clone.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from 'vitest'; 2 | import {deepClone} from './deep-clone.js'; 3 | import type {JSONValue, ReadonlyJSONValue} from './json.js'; 4 | 5 | test('deepClone', () => { 6 | const t = (v: ReadonlyJSONValue) => { 7 | expect(deepClone(v)).toEqual(v); 8 | }; 9 | 10 | t(null); 11 | t(1); 12 | t(1.2); 13 | t(0); 14 | t(-3412); 15 | t(1e20); 16 | t(''); 17 | t('hi'); 18 | t(true); 19 | t(false); 20 | t([]); 21 | t({}); 22 | 23 | t({a: 42}); 24 | t({a: 42, b: null}); 25 | t({a: 42, b: 0}); 26 | t({a: 42, b: true, c: false}); 27 | t({a: 42, b: [1, 2, 3]}); 28 | t([1, {}, 2]); 29 | 30 | const cyclicObject: JSONValue = {a: 42, cycle: null}; 31 | cyclicObject.cycle = cyclicObject; 32 | expect(() => deepClone(cyclicObject)).toThrow('Cyclic object'); 33 | 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | const cyclicArray: any = {a: 42, cycle: [null]}; 36 | cyclicArray.cycle[0] = cyclicArray; 37 | expect(() => deepClone(cyclicArray)).toThrow('Cyclic object'); 38 | 39 | const sym = Symbol(); 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | expect(() => deepClone(sym as any)).toThrow('Invalid type: symbol'); 42 | }); 43 | 44 | test('deepClone - reuse references', () => { 45 | const t = (v: ReadonlyJSONValue) => expect(deepClone(v)).toEqual(v); 46 | const arr: number[] = [0, 1]; 47 | 48 | t({a: arr, b: arr}); 49 | t(['a', [arr, arr]]); 50 | t(['a', arr, {a: arr}]); 51 | t(['a', arr, {a: [arr]}]); 52 | }); 53 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/deep-clone.ts: -------------------------------------------------------------------------------- 1 | import {hasOwn} from './has-own.js'; 2 | import type {JSONValue, ReadonlyJSONValue} from './json.js'; 3 | 4 | export function deepClone(value: ReadonlyJSONValue): JSONValue { 5 | const seen: Array = []; 6 | return internalDeepClone(value, seen); 7 | } 8 | 9 | export function internalDeepClone( 10 | value: ReadonlyJSONValue, 11 | seen: Array, 12 | ): JSONValue { 13 | switch (typeof value) { 14 | case 'boolean': 15 | case 'number': 16 | case 'string': 17 | case 'undefined': 18 | return value; 19 | case 'object': { 20 | if (value === null) { 21 | return null; 22 | } 23 | if (seen.includes(value)) { 24 | throw new Error('Cyclic object'); 25 | } 26 | seen.push(value); 27 | if (Array.isArray(value)) { 28 | const rv = value.map(v => internalDeepClone(v, seen)); 29 | seen.pop(); 30 | return rv; 31 | } 32 | 33 | const obj: JSONValue = {}; 34 | 35 | for (const k in value) { 36 | if (hasOwn(value, k)) { 37 | const v = (value as Record)[k]; 38 | if (v !== undefined) { 39 | obj[k] = internalDeepClone(v, seen); 40 | } 41 | } 42 | } 43 | seen.pop(); 44 | return obj; 45 | } 46 | 47 | default: 48 | throw new Error(`Invalid type: ${typeof value}`); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/events/connection-seconds.ts: -------------------------------------------------------------------------------- 1 | import * as v from '../valita.js'; 2 | 3 | // Increment when making non-backwards compatible changes to the schema. 4 | const SCHEMA_VERSION = 2; 5 | 6 | export const connectionSecondsReportSchema = v.object({ 7 | /** Reporting period, in seconds. */ 8 | period: v.number(), 9 | 10 | /** 11 | * Connection-seconds elapsed during the interval. 12 | * It follows that `elapsed / interval` is equal to the 13 | * average number of connections during the interval. 14 | */ 15 | elapsed: v.number(), 16 | 17 | /** Room ID of the connection. */ 18 | roomID: v.string(), 19 | }); 20 | 21 | export type ConnectionSecondsReport = v.Infer< 22 | typeof connectionSecondsReportSchema 23 | >; 24 | 25 | export const CONNECTION_SECONDS_CHANNEL_NAME = `connection-seconds@v${SCHEMA_VERSION}`; 26 | 27 | // Historic schemas for processing old Workers. 28 | export const CONNECTION_SECONDS_V1_CHANNEL_NAME = `connection-seconds@v1`; 29 | export const connectionSecondsReportV1Schema = v.object({ 30 | /** Reporting interval, in seconds. */ 31 | interval: v.number(), 32 | 33 | /** 34 | * Connection-seconds elapsed during the interval. 35 | * It follows that `elapsed / interval` is equal to the 36 | * average number of connections during the interval. 37 | */ 38 | elapsed: v.number(), 39 | }); 40 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/expand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Expand/simplifies a type for display in Intellisense. 3 | */ 4 | export type Expand = T extends infer O ? {[K in keyof O]: O[K]} : never; 5 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/float-to-ordered-string.test.ts: -------------------------------------------------------------------------------- 1 | import fc from 'fast-check'; 2 | import {expect, test} from 'vitest'; 3 | import { 4 | decodeFloat64AsString, 5 | encodeFloat64AsString, 6 | } from './float-to-ordered-string.js'; 7 | 8 | const cases = [ 9 | [-0, '1y2p0ij32e8e7'], 10 | [0, '1y2p0ij32e8e8'], 11 | [1, '2x2t6dniqybcw'], 12 | [2, '2x41irsmllclc'], 13 | [3, '2x4noyv6iwv7k'], 14 | [4, '2x59v5xqg8dts'], 15 | [-1, '0z2kunendu5fj'], 16 | [-2, '0z1ci99jj7473'], 17 | [-3, '0z0qc26zlvlkv'], 18 | [-4, '0z045v4fok2yn'], 19 | [3.141592653589793, '2x4qtzjh93rx4'], 20 | [NaN, '3w4rutzm7gy68'], 21 | [NaN, '3w4rutzm7gy68'], 22 | [Infinity, '3w45omx2a5fk0'], 23 | [-Infinity, '0018ce53un18f'], 24 | [Number.MAX_SAFE_INTEGER, '2yw3f766uv4sf'], 25 | [Number.MIN_SAFE_INTEGER, '0x9altvz9xc00'], 26 | [Number.MIN_VALUE, '1y2p0ij32e8e9'], 27 | [Number.MAX_VALUE, '3w45omx2a5fjz'], 28 | ] as const; 29 | 30 | const reversedCases = cases.map(([a, b]) => [b, a] as const); 31 | 32 | test.each(cases)('encode %f -> %s', (n, expected) => { 33 | expect(encodeFloat64AsString(n)).toBe(expected); 34 | }); 35 | 36 | test.each(reversedCases)('decode %s -> %f', (s, expected) => { 37 | expect(decodeFloat64AsString(s)).toBe(expected); 38 | }); 39 | 40 | test('random with fast-check', () => { 41 | fc.assert( 42 | fc.property(fc.float(), fc.float(), (a, b) => { 43 | const as = encodeFloat64AsString(a); 44 | const bs = encodeFloat64AsString(b); 45 | 46 | const a2 = decodeFloat64AsString(as); 47 | const b2 = decodeFloat64AsString(bs); 48 | 49 | expect(a2).toBe(a); 50 | expect(b2).toBe(b); 51 | 52 | if (Object.is(a, b)) { 53 | expect(as).toBe(bs); 54 | } else { 55 | expect(as).not.toBe(bs); 56 | if (!Number.isNaN(a) && !Number.isNaN(b)) { 57 | expect(as < bs).toBe(a < b); 58 | } 59 | } 60 | }), 61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/float-to-ordered-string.ts: -------------------------------------------------------------------------------- 1 | import {assert} from './asserts.js'; 2 | import {parseBigInt} from './parse-big-int.js'; 3 | 4 | const view = new DataView(new ArrayBuffer(8)); 5 | 6 | export function encodeFloat64AsString(n: number) { 7 | view.setFloat64(0, n); 8 | 9 | const high = view.getUint32(0); 10 | const low = view.getUint32(4); 11 | 12 | // The sign bit is 1 for negative numbers 13 | // We flip the sign bit so that positive numbers are ordered before negative numbers 14 | 15 | // If negative we flip all the bits so that larger absolute numbers are treated smaller 16 | if (n < 0 || Object.is(n, -0)) { 17 | view.setUint32(0, high ^ 0xffffffff); 18 | view.setUint32(4, low ^ 0xffffffff); 19 | } else { 20 | // we only flip the sign 21 | view.setUint32(0, high ^ (1 << 31)); 22 | } 23 | 24 | const bigint = view.getBigUint64(0); 25 | return bigint.toString(36).padStart(13, '0'); 26 | } 27 | 28 | export function decodeFloat64AsString(s: string): number { 29 | assert(s.length === 13, `Invalid encoded float64: ${s}`); 30 | const bigint = parseBigInt(s, 36); 31 | view.setBigUint64(0, bigint); 32 | 33 | const high = view.getUint32(0); 34 | const low = view.getUint32(4); 35 | const sign = high >> 31; 36 | 37 | // Positive 38 | if (sign) { 39 | // we only flip the sign 40 | view.setUint32(0, high ^ (1 << 31)); 41 | } else { 42 | // If negative we flipped all the bits so that larger absolute numbers are treated smaller 43 | view.setUint32(0, high ^ 0xffffffff); 44 | view.setUint32(4, low ^ 0xffffffff); 45 | } 46 | 47 | return view.getFloat64(0); 48 | } 49 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/h64-with-reverse.ts: -------------------------------------------------------------------------------- 1 | import {reverseString} from './reverse-string.js'; 2 | import {h64} from './xxhash.js'; 3 | 4 | /** 5 | * xxhash only computes 64-bit values. Run it on the forward and reverse string 6 | * to get better collision resistance. 7 | */ 8 | export function h64WithReverse(str: string): string { 9 | const forward = h64(str); 10 | const backward = h64(reverseString(str)); 11 | const full = (forward << 64n) + backward; 12 | return full.toString(36); 13 | } 14 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/has-own.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | const objectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; 3 | 4 | /** 5 | * Object.hasOwn polyfill 6 | */ 7 | export const hasOwn: (object: any, key: PropertyKey) => boolean = 8 | (Object as any).hasOwn || 9 | ((object, key) => objectPrototypeHasOwnProperty.call(object, key)); 10 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/headers.test.ts: -------------------------------------------------------------------------------- 1 | import {test, expect} from 'vitest'; 2 | import {decodeHeaderValue, encodeHeaderValue} from './headers.js'; 3 | 4 | function testEncodeDecodeHeaderValue(value: string, expected: string): void { 5 | const encoded = encodeHeaderValue(value); 6 | expect(encoded).toEqual(expected); 7 | expect(decodeHeaderValue(encoded)).toEqual(value); 8 | } 9 | 10 | test('encodeHeaderValue/decodeHeaderValue', () => { 11 | testEncodeDecodeHeaderValue('basic test', 'basic test'); 12 | // All the chars normally escaped by encodeURIComponent, but which don't 13 | // need to be escaped for header values 14 | testEncodeDecodeHeaderValue( 15 | ':;,/"?{}[]@<>=+#$&`|^ ', 16 | ':;,/"?{}[]@<>=+#$&`|^ ', 17 | ); 18 | testEncodeDecodeHeaderValue( 19 | '% char should be escaped', 20 | '%25 char should be escaped', 21 | ); 22 | testEncodeDecodeHeaderValue( 23 | '{ userId: "1245678910" }', 24 | '{ userId: "1245678910" }', 25 | ); 26 | testEncodeDecodeHeaderValue( 27 | 'هذا اختبار', 28 | '%D9%87%D8%B0%D8%A7 %D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1', 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/headers.ts: -------------------------------------------------------------------------------- 1 | export function encodeHeaderValue(value: string): string { 2 | // encodeURIComponent escapes the following chars which are allowed 3 | // in header values. 4 | // : ; , / " ? { } [ ] @ < > = + # $ & ` | ^ space and % 5 | // Unescape all of them expect %, to make the encoded value smaller and more 6 | // readable. Do not unescape % as that would break decoding of the 7 | // percent decoding done by encodeURIComponent. 8 | return encodeURIComponent(value).replace( 9 | /%(3A|3B|2C|2F|22|3F|7B|7D|5B|5D|40|3C|3E|3D|2B|23|24|26|60|7C|5E|20)/g, 10 | (_, hex) => String.fromCharCode(parseInt(hex, 16)), 11 | ); 12 | } 13 | 14 | export function decodeHeaderValue(encoded: string): string { 15 | return decodeURIComponent(encoded); 16 | } 17 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/immutable.ts: -------------------------------------------------------------------------------- 1 | type Primitive = undefined | null | boolean | string | number | symbol | bigint; 2 | 3 | /** 4 | * Create a deeply immutable type from a type that may contain mutable types. 5 | */ 6 | export type Immutable = T extends Primitive 7 | ? T 8 | : T extends Array 9 | ? ImmutableArray 10 | : ImmutableObject; 11 | // This does not deal with Maps or Sets (or Date or RegExp or ...). 12 | 13 | export type ImmutableArray = ReadonlyArray>; 14 | 15 | export type ImmutableObject = {readonly [K in keyof T]: Immutable}; 16 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/json-schema.ts: -------------------------------------------------------------------------------- 1 | import * as valita from '@badrap/valita'; 2 | import {skipAssertJSONValue} from './config.js'; 3 | import type {ReadonlyJSONObject, ReadonlyJSONValue} from './json.js'; 4 | import {isJSONObject, isJSONValue} from './json.js'; 5 | import * as v from './valita.js'; 6 | 7 | const path: (string | number)[] = []; 8 | 9 | export const jsonSchema: valita.Type = v 10 | .unknown() 11 | .chain(v => { 12 | if (skipAssertJSONValue) { 13 | return valita.ok(v as ReadonlyJSONValue); 14 | } 15 | const rv = isJSONValue(v, path) 16 | ? valita.ok(v) 17 | : valita.err({ 18 | message: `Not a JSON value`, 19 | path: path.slice(), 20 | }); 21 | path.length = 0; 22 | return rv; 23 | }); 24 | 25 | export const jsonObjectSchema: valita.Type = v 26 | .unknown() 27 | .chain(v => { 28 | if (skipAssertJSONValue) { 29 | return valita.ok(v as ReadonlyJSONObject); 30 | } 31 | const rv = isJSONObject(v, path) 32 | ? valita.ok(v) 33 | : valita.err({ 34 | message: `Not a JSON object`, 35 | path: path.slice(), 36 | }); 37 | path.length = 0; 38 | return rv; 39 | }); 40 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/logging-test-utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Context, 3 | LogContext, 4 | type LogLevel, 5 | type LogSink, 6 | } from '@rocicorp/logger'; 7 | 8 | export class TestLogSink implements LogSink { 9 | messages: [LogLevel, Context | undefined, unknown[]][] = []; 10 | flushCallCount = 0; 11 | 12 | log(level: LogLevel, context: Context | undefined, ...args: unknown[]): void { 13 | this.messages.push([level, context, args]); 14 | } 15 | 16 | flush() { 17 | this.flushCallCount++; 18 | return Promise.resolve(); 19 | } 20 | } 21 | 22 | export class SilentLogSink implements LogSink { 23 | log(_l: LogLevel, _c: Context | undefined, ..._args: unknown[]): void { 24 | return; 25 | } 26 | } 27 | 28 | export function createSilentLogContext() { 29 | return new LogContext('error', undefined, new SilentLogSink()); 30 | } 31 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/mirror/is-supported-semver-range.ts: -------------------------------------------------------------------------------- 1 | import type {Comparator, Range} from 'semver'; 2 | 3 | export function isSupportedSemverRange(range: Range): boolean { 4 | if (range.set.length === 1) { 5 | const comparators = range.set[0]; 6 | if (comparators.length === 1) { 7 | const comparator = comparators[0]; 8 | const {operator} = comparator; 9 | if (operator === '<' || operator === '<=') { 10 | const {semver} = comparator; 11 | return semver.patch === 0 && (semver.major === 0 || semver.minor === 0); 12 | } 13 | return operator === '>=' || operator === '>'; 14 | } 15 | 16 | if (comparators.length === 2) { 17 | return isComparatorOK(comparators[0]) && isComparatorOK(comparators[1]); 18 | } 19 | } 20 | 21 | return false; 22 | 23 | function isComparatorOK(comparator: Comparator): boolean { 24 | const {operator} = comparator; 25 | if (operator === '<' || operator === '<=') { 26 | if (comparator.semver.major === 0) { 27 | return true; 28 | } 29 | return comparator.semver.minor === 0 && comparator.semver.patch === 0; 30 | } 31 | return operator === '>' || operator === '>='; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/mod.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/must.ts: -------------------------------------------------------------------------------- 1 | export function must(v: T | undefined | null, msg?: string): T { 2 | // eslint-disable-next-line eqeqeq 3 | if (v == null) { 4 | throw new Error(msg ?? `Unexpected ${v} value`); 5 | } 6 | return v; 7 | } 8 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/navigator.ts: -------------------------------------------------------------------------------- 1 | type Navigator = { 2 | onLine: boolean; 3 | userAgent: string; 4 | // add more as needed 5 | }; 6 | 7 | const localNavigator: Navigator | undefined = 8 | typeof navigator !== 'undefined' ? navigator : undefined; 9 | 10 | export {localNavigator as navigator}; 11 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/parse-big-int.test.ts: -------------------------------------------------------------------------------- 1 | import fc from 'fast-check'; 2 | import {expect, test} from 'vitest'; 3 | import {parseBigInt} from './parse-big-int.js'; 4 | 5 | const cases = [ 6 | ['0', 10, 0n], 7 | ['1', 10, 1n], 8 | ['10', 10, 10n], 9 | ['10', 16, 16n], 10 | ['100', 10, 100n], 11 | ['100', 16, 256n], 12 | ['10', 36, 36n], 13 | ['100', 36, 1296n], 14 | ] as const; 15 | 16 | test.each(cases)('parseBigInt(%s, %s, %d)', (s, radix, expected) => { 17 | const actual = parseBigInt(s, radix); 18 | expect(actual).toBe(expected); 19 | }); 20 | 21 | test('random using fast check', () => { 22 | fc.assert( 23 | fc.property( 24 | fc.bigInt({min: 0n}), 25 | fc.integer({min: 2, max: 36}), 26 | (n, radix) => { 27 | const s = n.toString(radix); 28 | const actual = parseBigInt(s, radix); 29 | expect(actual).toBe(n); 30 | }, 31 | ), 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/parse-big-int.ts: -------------------------------------------------------------------------------- 1 | // Until there's BigInt.fromString(val, radix) ... https://github.com/tc39/proposal-number-fromstring 2 | export function parseBigInt(val: string, radix: number): bigint { 3 | const base = BigInt(radix); 4 | let result = 0n; 5 | for (let i = 0; i < val.length; i++) { 6 | result *= base; 7 | result += BigInt(parseInt(val[i], radix)); 8 | } 9 | return result; 10 | } 11 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/rand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param min Inclusive minimum of the result. 3 | * @param max Inclusive maximum of the result. 4 | * @returns A random integer in the (inclusive) range of [`min`, `max`]. 5 | */ 6 | export function randInt(min: number, max: number) { 7 | min = Math.ceil(min); 8 | max = Math.floor(max); 9 | return Math.floor(Math.random() * (max - min + 1) + min); 10 | } 11 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/random-uint64.ts: -------------------------------------------------------------------------------- 1 | export function randomUint64(): bigint { 2 | // Generate two random 32-bit unsigned integers using Math.random() 3 | const high = Math.floor(Math.random() * 0xffffffff); // High 32 bits 4 | const low = Math.floor(Math.random() * 0xffffffff); // Low 32 bits 5 | 6 | // Combine the high and low parts to form a 64-bit unsigned integer 7 | return (BigInt(high) << 32n) | BigInt(low); 8 | } 9 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/random-values.ts: -------------------------------------------------------------------------------- 1 | export function getNonCryptoRandomValues(array: Uint8Array) { 2 | if (array === null) { 3 | throw new TypeError('array cannot be null'); 4 | } 5 | 6 | // Fill the array with random values 7 | for (let i = 0; i < array.length; i++) { 8 | array[i] = Math.floor(Math.random() * 256); // Random byte (0-255) 9 | } 10 | 11 | return array; 12 | } 13 | 14 | export function randomCharacters(length: number) { 15 | let result = ''; 16 | const characters = 17 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 18 | const charactersLength = characters.length; 19 | let counter = 0; 20 | while (counter < length) { 21 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 22 | counter += 1; 23 | } 24 | return result; 25 | } 26 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/read-json-file.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import {readFile} from 'node:fs/promises'; 4 | 5 | /** 6 | * @typedef {{ 7 | * [key: string]: any; 8 | * name: string; 9 | * version: string; 10 | * }} PackageJSON 11 | */ 12 | 13 | /** 14 | * @param {string} pathLike 15 | * @returns {Promise} 16 | */ 17 | export async function readJSONFile(pathLike) { 18 | const s = await readFile(pathLike, 'utf-8'); 19 | return JSON.parse(s); 20 | } 21 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/resolved-promises.ts: -------------------------------------------------------------------------------- 1 | export const promiseTrue = Promise.resolve(true); 2 | export const promiseFalse = Promise.resolve(false); 3 | export const promiseUndefined = Promise.resolve(undefined); 4 | export const promiseVoid = Promise.resolve(); 5 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/reverse-string.ts: -------------------------------------------------------------------------------- 1 | export function reverseString(str: string): string { 2 | let reversed = ''; 3 | for (let i = str.length - 1; i >= 0; i--) { 4 | reversed += str[i]; 5 | } 6 | return reversed; 7 | } 8 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/sleep.ts: -------------------------------------------------------------------------------- 1 | import {resolver} from '@rocicorp/resolver'; 2 | import {AbortError} from './abort-error.js'; 3 | 4 | const promiseVoid = Promise.resolve(); 5 | const promiseNever = new Promise(() => undefined); 6 | 7 | /** 8 | * Creates a promise that resolves after `ms` milliseconds. Note that if you 9 | * pass in `0` no `setTimeout` is used and the promise resolves immediately. In 10 | * other words no macro task is used in that case. 11 | * 12 | * Pass in an AbortSignal to clear the timeout. 13 | */ 14 | export function sleep(ms: number, signal?: AbortSignal): Promise { 15 | const newAbortError = () => new AbortError('Aborted'); 16 | 17 | if (signal?.aborted) { 18 | return Promise.reject(newAbortError()); 19 | } 20 | 21 | if (ms === 0) { 22 | return promiseVoid; 23 | } 24 | 25 | return new Promise((resolve, reject) => { 26 | let handleAbort: () => void; 27 | if (signal) { 28 | handleAbort = () => { 29 | clearTimeout(id); 30 | reject(newAbortError()); 31 | }; 32 | signal.addEventListener('abort', handleAbort, {once: true}); 33 | } 34 | 35 | const id = setTimeout(() => { 36 | resolve(); 37 | signal?.removeEventListener('abort', handleAbort); 38 | }, ms); 39 | }); 40 | } 41 | 42 | /** 43 | * Returns a pair of promises. The first promise resolves after `ms` milliseconds 44 | * unless the AbortSignal is aborted. The second promise resolves when the AbortSignal 45 | * is aborted. 46 | */ 47 | export function sleepWithAbort( 48 | ms: number, 49 | signal: AbortSignal, 50 | ): [ok: Promise, aborted: Promise] { 51 | if (ms === 0) { 52 | return [promiseVoid, promiseNever]; 53 | } 54 | 55 | const {promise: abortedPromise, resolve: abortedResolve} = resolver(); 56 | 57 | const sleepPromise = new Promise(resolve => { 58 | const handleAbort = () => { 59 | clearTimeout(id); 60 | abortedResolve(); 61 | }; 62 | 63 | const id = setTimeout(() => { 64 | resolve(); 65 | signal.removeEventListener('abort', handleAbort); 66 | }, ms); 67 | 68 | signal.addEventListener('abort', handleAbort, {once: true}); 69 | }); 70 | 71 | return [sleepPromise, abortedPromise]; 72 | } 73 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/sorted-entries.ts: -------------------------------------------------------------------------------- 1 | import {stringCompare} from './string-compare.js'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export function sortedEntries>( 5 | object: T, 6 | ): [keyof T & string, T[keyof T]][] { 7 | return Object.entries(object).sort((a, b) => stringCompare(a[0], b[0])); 8 | } 9 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/string-compare.ts: -------------------------------------------------------------------------------- 1 | export function stringCompare(a: string, b: string): number { 2 | if (a === b) { 3 | return 0; 4 | } 5 | if (a < b) { 6 | return -1; 7 | } 8 | return 1; 9 | } 10 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/timed.ts: -------------------------------------------------------------------------------- 1 | type LogFunction = ((...args: unknown[]) => void) | undefined; 2 | 3 | declare const performance: { 4 | now(): number; 5 | }; 6 | 7 | /** 8 | * Times some async function and writes the result to a provided log function. 9 | * The log function can be undefined to simplify use with OptionalLogger. 10 | * @param log Log function to write to (ie LogContext.log) 11 | * @param label Label to write at start and end of function 12 | * @param fn Function to time 13 | * @returns The result of fn 14 | */ 15 | export async function timed( 16 | log: LogFunction | undefined, 17 | label: string, 18 | fn: () => Promise, 19 | ): Promise { 20 | log?.(`Starting ${label}`); 21 | const clock = typeof performance !== 'undefined' ? performance : Date; 22 | const t0 = clock.now(); 23 | try { 24 | return await fn(); 25 | } finally { 26 | const t1 = clock.now(); 27 | log?.(`Finished ${label} in ${t1 - t0}ms`); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/tool/get-external-from-package-json.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /* eslint-env es2022 */ 4 | 5 | import {readFile} from 'node:fs/promises'; 6 | import {pkgUp} from 'pkg-up'; 7 | import {isInternalPackage} from './internal-packages.js'; 8 | 9 | /** 10 | * @param {string} basePath 11 | * @param {boolean} includePeerDeps 12 | * @returns {Promise} 13 | */ 14 | export async function getExternalFromPackageJSON(basePath, includePeerDeps) { 15 | const path = await pkgUp({cwd: basePath}); 16 | if (!path) { 17 | throw new Error('Could not find package.json'); 18 | } 19 | const x = await readFile(path, 'utf-8'); 20 | const pkg = JSON.parse(x); 21 | 22 | const deps = new Set(); 23 | for (const dep of Object.keys({ 24 | ...pkg.dependencies, 25 | ...(includePeerDeps ? pkg.peerDependencies : {}), 26 | })) { 27 | if (isInternalPackage(dep)) { 28 | for (const depDep of await getRecursiveExternals(dep, includePeerDeps)) { 29 | deps.add(depDep); 30 | } 31 | } else { 32 | deps.add(dep); 33 | } 34 | } 35 | return [...deps]; 36 | } 37 | 38 | /** 39 | * @param {string} name 40 | * @param {boolean} includePeerDeps 41 | */ 42 | function getRecursiveExternals(name, includePeerDeps) { 43 | if (name === 'shared') { 44 | return getExternalFromPackageJSON( 45 | new URL(import.meta.url).pathname, 46 | includePeerDeps, 47 | ); 48 | } 49 | const depPath = new URL(`../../../${name}/package.json`, import.meta.url) 50 | .pathname; 51 | return getExternalFromPackageJSON(depPath, includePeerDeps); 52 | } 53 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/tool/inject-require.js: -------------------------------------------------------------------------------- 1 | function createRandomIdentifier(name) { 2 | return `${name}_${Math.random() * 10000}`.replace('.', ''); 3 | } 4 | 5 | /** 6 | * Injects a global `require` function into the bundle. This is sometimes needed 7 | * if the dependencies are incorrectly using require. 8 | * 9 | * @returns {esbuild.BuildOptions} 10 | */ 11 | export function injectRequire() { 12 | const createRequireAlias = createRandomIdentifier('createRequire'); 13 | return `import {createRequire as ${createRequireAlias}} from 'node:module'; 14 | var require = ${createRequireAlias}(import.meta.url); 15 | `; 16 | } 17 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/tool/internal-packages.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /* eslint-env node, es2022 */ 4 | 5 | import * as fs from 'node:fs'; 6 | import * as path from 'node:path'; 7 | import {fileURLToPath} from 'node:url'; 8 | 9 | /** 10 | * Map from name to packages/name 11 | * @type {Map} 12 | */ 13 | export const internalPackagesMap = new Map(); 14 | 15 | const monoRootPath = fileURLToPath(new URL('../../../../', import.meta.url)); 16 | 17 | for (const p of ['packages']) { 18 | for (const f of fs.readdirSync(path.join(monoRootPath, p))) { 19 | const stat = fs.statSync(path.join(monoRootPath, p, f)); 20 | if (stat.isDirectory()) { 21 | // Also ensure that there is a package.json in that directory 22 | const packageJSONPath = path.join(monoRootPath, p, f, 'package.json'); 23 | 24 | if (fs.existsSync(packageJSONPath)) { 25 | const packageJSON = JSON.parse( 26 | fs.readFileSync(packageJSONPath, 'utf-8'), 27 | ); 28 | if (packageJSON.private) { 29 | internalPackagesMap.set(f, `${p}/${f}`); 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | export const internalPackages = [...internalPackagesMap.keys()]; 37 | 38 | /** 39 | * @param {string} name 40 | */ 41 | export function isInternalPackage(name) { 42 | return internalPackagesMap.has(name); 43 | } 44 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/tool/vitest-config.ts: -------------------------------------------------------------------------------- 1 | import {argv} from 'node:process'; 2 | import {makeDefine} from '../build.js'; 3 | 4 | const define = { 5 | ...makeDefine(), 6 | ['TESTING']: 'true', 7 | }; 8 | 9 | /** 10 | * Find name from process.argv. 11 | * 12 | * The argv has --browser.name=chromium which 13 | * overrides the test.browser.name in the config but there is no way to read it 14 | * at this level so we have to do it manually. 15 | */ 16 | function findBrowserName() { 17 | for (const arg of argv) { 18 | const m = arg.match(/--browser\.name=(.+)/); 19 | if (m) { 20 | return m[1]; 21 | } 22 | } 23 | return undefined; 24 | } 25 | 26 | const logSilenceMessages = [ 27 | 'Skipping license check for TEST_LICENSE_KEY.', 28 | 'REPLICACHE LICENSE NOT VALID', 29 | 'enableAnalytics false', 30 | 'no such entity', 31 | 'Zero starting up with no server URL', 32 | 'PokeHandler clearing due to unexpected poke error', 33 | 'Not indexing value', 34 | 'Zero starting up with no server URL', 35 | ]; 36 | export default { 37 | // https://github.com/vitest-dev/vitest/issues/5332#issuecomment-1977785593 38 | optimizeDeps: { 39 | include: ['vitest > @vitest/expect > chai'], 40 | exclude: ['wa-sqlite'], 41 | }, 42 | define, 43 | esbuild: { 44 | define, 45 | }, 46 | 47 | test: { 48 | name: findBrowserName(), 49 | onConsoleLog(log: string) { 50 | for (const message of logSilenceMessages) { 51 | if (log.includes(message)) { 52 | return false; 53 | } 54 | } 55 | return undefined; 56 | }, 57 | include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'], 58 | browser: { 59 | enabled: true, 60 | provider: 'playwright', 61 | headless: true, 62 | name: 'chromium', 63 | screenshotFailures: false, 64 | }, 65 | typecheck: { 66 | enabled: false, 67 | }, 68 | testTimeout: 10_000, 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/types.ts: -------------------------------------------------------------------------------- 1 | export type MaybePromise = T | Promise; 2 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/writable.ts: -------------------------------------------------------------------------------- 1 | export type Writable = { 2 | -readonly [P in keyof T]: T[P]; 3 | }; 4 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/shared/xxhash.ts: -------------------------------------------------------------------------------- 1 | import xxhash from 'xxhash-wasm'; 2 | 3 | const {create64, h32, h64} = await xxhash(); 4 | 5 | export {create64, h32, h64}; 6 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/svelte-view.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { QueryType, Smash } from '@rocicorp/zero'; 2 | import { 3 | applyChange, 4 | type Change, 5 | type Format, 6 | type Input, 7 | type Output, 8 | type View, 9 | type Entry, 10 | type TableSchema 11 | } from '@rocicorp/zero/advanced'; 12 | import type { Query } from 'pg'; 13 | 14 | export class SvelteView implements Output { 15 | readonly #input: Input; 16 | readonly #format: Format; 17 | readonly #onDestroy: () => void; 18 | 19 | // Synthetic "root" entry that has a single "" relationship, so that we can 20 | // treat all changes, including the root change, generically. 21 | readonly #root: Entry = $state({ '': undefined }); 22 | 23 | constructor( 24 | input: Input, 25 | format: Format = { singular: false, relationships: {} }, 26 | onDestroy: () => void = () => {} 27 | ) { 28 | this.#input = input; 29 | this.#format = format; 30 | this.#onDestroy = onDestroy; 31 | this.#root = { 32 | '': format.singular ? undefined : [] 33 | }; 34 | input.setOutput(this); 35 | for (const node of input.fetch({})) { 36 | applyChange(this.#root, { type: 'add', node }, input.getSchema(), '', this.#format); 37 | } 38 | } 39 | 40 | get data() { 41 | return this.#root[''] as V; 42 | } 43 | 44 | destroy() { 45 | this.#onDestroy(); 46 | } 47 | 48 | push(change: Change): void { 49 | applyChange(this.#root, change, this.#input.getSchema(), '', this.#format); 50 | } 51 | } 52 | 53 | export function svelteViewFactory( 54 | _query: Query, 55 | input: Input, 56 | format: Format, 57 | onDestroy: () => void 58 | ): SvelteView> { 59 | const v = new SvelteView>(input, format, onDestroy); 60 | 61 | return v; 62 | } 63 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type BaseQuestionConfig = { 2 | required: boolean; 3 | }; 4 | 5 | export type TextConfig = BaseQuestionConfig & { 6 | minLength?: number; 7 | maxLength?: number; 8 | validationPattern?: string; 9 | }; 10 | 11 | export type RatingConfig = BaseQuestionConfig & { 12 | max: number; 13 | kind: 'start' | 'number'; 14 | labels?: { 15 | min: string; 16 | max: string; 17 | }; 18 | }; 19 | 20 | export type ChoiceConfig = BaseQuestionConfig & { 21 | options: Array<{ 22 | id: string; 23 | label: string; 24 | value: string; 25 | }>; 26 | allowMultiple?: boolean; 27 | minSelections?: number; 28 | maxSelections?: number; 29 | allowOther?: boolean; 30 | }; 31 | 32 | export type QuestionConfig = TextConfig | RatingConfig | ChoiceConfig; 33 | -------------------------------------------------------------------------------- /examples/zero-survey/src/lib/z.svelte.ts: -------------------------------------------------------------------------------- 1 | import { PUBLIC_SERVER } from '$env/static/public'; 2 | import { schema } from '../schema'; 3 | import { get_login } from '@drop-in/pass/client'; 4 | 5 | export function get_z_options() { 6 | const a = get_login(); 7 | 8 | return { 9 | userID: a.sub ?? 'anon', 10 | server: PUBLIC_SERVER, 11 | schema, 12 | auth: a.jwt 13 | } as const; 14 | } 15 | -------------------------------------------------------------------------------- /examples/zero-survey/src/routes/(app)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 21 | 22 |
23 | 24 | {#if z.current.userID} 25 |
26 |
27 | {@render children()} 28 |
29 |
30 | {/if} 31 | 32 |