├── .nvmrc ├── .npmrc ├── convex.json ├── static └── favicon.png ├── src ├── lib │ ├── index.ts │ └── client.svelte.ts ├── index.test.ts ├── app.d.ts ├── convex │ ├── schema.ts │ ├── _generated │ │ ├── api.js │ │ ├── api.d.ts │ │ ├── dataModel.d.ts │ │ ├── server.js │ │ └── server.d.ts │ ├── numbers.ts │ ├── tsconfig.json │ ├── seed_messages.ts │ ├── messages.ts │ └── README.md ├── app.html └── routes │ ├── +page.server.ts │ ├── inputs │ ├── +page.svelte │ └── Inputs.svelte │ ├── +page.svelte │ ├── tests │ ├── skip-query │ │ └── +page.svelte │ └── always-errors │ │ └── +page.svelte │ ├── +layout.svelte │ └── Chat.svelte ├── .prettierignore ├── .stackblitzrc ├── .eslintignore ├── .prettierrc ├── vite.config.ts ├── e2e ├── useQueryReturn.test.ts └── skipQuery.test.ts ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── publish-every-commit.yml │ └── publish-pr.yml ├── .eslintrc.cjs ├── svelte.config.js ├── package.json ├── playwright.config.ts ├── README.md └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /convex.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": "src/convex/" 3 | } 4 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/convex-svelte/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // Reexport your entry components here 2 | 3 | export { useConvexClient, setupConvex, useQuery, setConvexClientContext } from './client.svelte.js'; 4 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | 6 | // gets reformatted by convex 7 | convex.json 8 | src/convex/_generated/ 9 | -------------------------------------------------------------------------------- /.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": false, 3 | "startCommand": "npm install --force; npm run dev:client", 4 | "env": { 5 | "PUBLIC_CONVEX_URL": "https://clever-gnat-613.convex.cloud" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | test: { 7 | include: ['src/**/*.{test,spec}.{js,ts}'] 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /e2e/useQueryReturn.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('always-errors page loads', async ({ page }) => { 4 | await page.goto('/tests/always-errors'); 5 | await expect(page.locator('h1')).toBeVisible(); 6 | await new Promise((r) => setTimeout(r, 1000)); 7 | }); 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | 13 | # Playwright 14 | /test-results/ 15 | /playwright-report/ 16 | /blob-report/ 17 | /playwright/.cache/ 18 | pnpm-lock.yaml 19 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from 'convex/server'; 2 | import { v } from 'convex/values'; 3 | 4 | export default defineSchema({ 5 | messages: defineTable({ 6 | author: v.string(), 7 | body: v.string() 8 | }), 9 | numbers: defineTable({ 10 | a: v.number(), 11 | b: v.number(), 12 | c: v.number() 13 | }) 14 | }); 15 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "module": "NodeNext", 13 | "moduleResolution": "NodeNext" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { ConvexHttpClient } from 'convex/browser'; 2 | import type { PageServerLoad } from './$types.js'; 3 | import { PUBLIC_CONVEX_URL } from '$env/static/public'; 4 | import { api } from '../convex/_generated/api.js'; 5 | 6 | export const load = (async () => { 7 | const client = new ConvexHttpClient(PUBLIC_CONVEX_URL!); 8 | return { 9 | messages: await client.query(api.messages.list, { muteWords: [] }) 10 | }; 11 | }) satisfies PageServerLoad; 12 | -------------------------------------------------------------------------------- /src/convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { anyApi } from "convex/server"; 12 | 13 | /** 14 | * A utility for referencing Convex functions in your app's API. 15 | * 16 | * Usage: 17 | * ```js 18 | * const myFunctionReference = api.myModule.myFunction; 19 | * ``` 20 | */ 21 | export const api = anyApi; 22 | export const internal = anyApi; 23 | -------------------------------------------------------------------------------- /.github/workflows/publish-every-commit.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | # from https://github.com/stackblitz-labs/pkg.pr.new?tab=readme-ov-file 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | 13 | - run: corepack enable 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 20 17 | cache: 'npm' 18 | 19 | - name: Install dependencies 20 | run: npm install 21 | 22 | - run: npx pkg-pr-new publish --template '.' 23 | -------------------------------------------------------------------------------- /src/routes/inputs/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Home 7 | 8 | 9 | 10 |
11 |

Modifying several inputs

12 |

Any user have complete control over these inputs but might change them quickly.

13 | 14 | 15 |
16 | 17 | 30 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Home 9 | 10 | 11 | 12 |
13 |

Welcome to SvelteKit with Convex

14 | 15 | 16 |
17 | 18 | 31 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/convex/numbers.ts: -------------------------------------------------------------------------------- 1 | import { v } from 'convex/values'; 2 | import { query, mutation } from './_generated/server.js'; 3 | 4 | export const get = query(async (ctx) => { 5 | const numbers = await ctx.db.query('numbers').first(); 6 | return { 7 | a: numbers?.a || 0, 8 | b: numbers?.b || 0, 9 | c: numbers?.c || 0 10 | }; 11 | }); 12 | 13 | export const update = mutation({ 14 | args: { 15 | a: v.number(), 16 | b: v.number(), 17 | c: v.number() 18 | }, 19 | handler: async (ctx, { a, b, c }) => { 20 | const existing = await ctx.db.query('numbers').first(); 21 | let id = existing?._id; 22 | if (!id) { 23 | id = await ctx.db.insert('numbers', { a: 0, b: 0, c: 0 }); 24 | } 25 | await ctx.db.replace(id, { a, b, c }); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter() 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /src/convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "esnext", 17 | "moduleResolution": "bundler", 18 | "isolatedModules": true, 19 | "skipLibCheck": true, 20 | "noEmit": true 21 | }, 22 | "include": ["./**/*"], 23 | "exclude": ["./_generated"] 24 | } 25 | -------------------------------------------------------------------------------- /e2e/skipQuery.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('skipQuery prevents query execution', async ({ page }) => { 4 | await page.goto('/tests/skip-query'); 5 | 6 | // Initially, query should run and load data 7 | await expect(page.getByTestId('loading')).toBeVisible(); 8 | await expect(page.getByTestId('data')).toBeVisible({ timeout: 5000 }); 9 | 10 | // Check the skip checkbox 11 | await page.getByTestId('skip-checkbox').check(); 12 | 13 | // When skipped, should show "No data" (not loading, no error, no data) 14 | await expect(page.getByTestId('no-data')).toBeVisible(); 15 | 16 | // Uncheck to verify it resumes 17 | await page.getByTestId('skip-checkbox').uncheck(); 18 | 19 | // Should load again 20 | await expect(page.getByTestId('data')).toBeVisible({ timeout: 5000 }); 21 | }); -------------------------------------------------------------------------------- /src/convex/seed_messages.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | _creationTime: 1691013604325.0217, 4 | _id: '2w1tjesf0ng55yvwzv5gkehv9hsy20r', 5 | author: 'Tom', 6 | body: "Let's talk about vim vs VS Code!" 7 | }, 8 | { 9 | _creationTime: 1691013604325.0137, 10 | _id: '2w4w1wx9f0bpgm1adt0w6j689hspwq8', 11 | author: 'Sujay', 12 | body: 'What about emacs?' 13 | }, 14 | 15 | { 16 | _creationTime: 1691013604325.0095, 17 | _id: '2w6q24ygyjhsb0mxq619y8v09hsxyeg', 18 | author: 'Tom', 19 | body: 'I spend so much time customizing my vim config, and now I need to learn Lua to configure neovim!' 20 | }, 21 | { 22 | _creationTime: 1691013604325.0125, 23 | _id: '2w7c8t54p3be7gd212vhnabw9hsqwdr', 24 | author: 'James', 25 | body: 'I use the defaults settings for everything.' 26 | }, 27 | { 28 | _creationTime: 1691013604325.0205, 29 | _id: '2w7d7p8ysmtr6njf4yejj0jy9hsjar0', 30 | author: 'Arnold', 31 | body: 'While you were talking I added a feature to the product' 32 | } 33 | ]; 34 | -------------------------------------------------------------------------------- /src/routes/tests/skip-query/+page.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |

Skip Query Test

15 | 16 | 20 | 21 |
22 | {#if messages.isLoading} 23 |

Loading

24 | {:else if messages.error} 25 |

Error: {messages.error.message}

26 | {:else if messages.data} 27 |

Data: {messages.data.length} messages

28 | {:else} 29 |

No data

30 | {/if} 31 |
32 |
-------------------------------------------------------------------------------- /src/convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type { 12 | ApiFromModules, 13 | FilterApi, 14 | FunctionReference, 15 | } from "convex/server"; 16 | import type * as messages from "../messages.js"; 17 | import type * as numbers from "../numbers.js"; 18 | import type * as seed_messages from "../seed_messages.js"; 19 | 20 | /** 21 | * A utility for referencing Convex functions in your app's API. 22 | * 23 | * Usage: 24 | * ```js 25 | * const myFunctionReference = api.myModule.myFunction; 26 | * ``` 27 | */ 28 | declare const fullApi: ApiFromModules<{ 29 | messages: typeof messages; 30 | numbers: typeof numbers; 31 | seed_messages: typeof seed_messages; 32 | }>; 33 | export declare const api: FilterApi< 34 | typeof fullApi, 35 | FunctionReference 36 | >; 37 | export declare const internal: FilterApi< 38 | typeof fullApi, 39 | FunctionReference 40 | >; 41 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 | {@render children()} 12 |
13 | 14 | 20 |
21 | 22 | 58 | -------------------------------------------------------------------------------- /src/convex/messages.ts: -------------------------------------------------------------------------------- 1 | import { ConvexError, v } from 'convex/values'; 2 | import { query, mutation, internalMutation } from './_generated/server.js'; 3 | import type { Doc } from './_generated/dataModel.js'; 4 | 5 | export const error = query(() => { 6 | throw new ConvexError('this is a Convex error'); 7 | }); 8 | 9 | export const list = query(async (ctx, { muteWords = [] }: { muteWords?: string[] }) => { 10 | const messages = await ctx.db.query('messages').collect(); 11 | const filteredMessages = messages.filter( 12 | ({ body }) => !muteWords.some((word) => body.toLowerCase().includes(word.toLowerCase())) 13 | ); 14 | return filteredMessages.reverse(); 15 | }); 16 | 17 | export const send = mutation({ 18 | args: { body: v.string(), author: v.string() }, 19 | handler: async (ctx, { body, author }) => { 20 | const message = { body, author }; 21 | await ctx.db.insert('messages', message); 22 | } 23 | }); 24 | 25 | import seedMessages from './seed_messages.js'; 26 | export const seed = internalMutation({ 27 | handler: async (ctx) => { 28 | if ((await ctx.db.query('messages').collect()).length >= seedMessages.length) return; 29 | 30 | for (const message of seedMessages as Doc<'messages'>[]) { 31 | const { _id, _creationTime, ...withoutSystemFields } = message; 32 | console.log('ignoring', _id, _creationTime); 33 | await ctx.db.insert('messages', withoutSystemFields); 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /.github/workflows/publish-pr.yml: -------------------------------------------------------------------------------- 1 | name: Publish Approved Pull Requests 2 | # from https://github.com/stackblitz-labs/pkg.pr.new?tab=readme-ov-file 3 | on: 4 | pull_request_review: 5 | types: [submitted] 6 | 7 | jobs: 8 | check: 9 | # First, trigger a permissions check on the user approving the pull request. 10 | if: github.event.review.state == 'approved' 11 | runs-on: ubuntu-latest 12 | outputs: 13 | has-permissions: ${{ steps.checkPermissions.outputs.require-result }} 14 | steps: 15 | - name: Check permissions 16 | id: checkPermissions 17 | uses: actions-cool/check-user-permission@v2 18 | with: 19 | # In this example, the approver must have the write access 20 | # to the repository to trigger the package preview. 21 | require: 'write' 22 | 23 | publish: 24 | needs: check 25 | # Publish the preview package only if the permissions check passed. 26 | if: needs.check.outputs.has-permissions == 'true' 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | 32 | - run: corepack enable 33 | - uses: actions/setup-node@v4 34 | with: 35 | node-version: 20 36 | cache: 'npm' 37 | 38 | - name: Install dependencies 39 | run: npm install 40 | 41 | - run: npx pkg-pr-new publish 42 | - run: npx pkg-pr-new publish --template '.' 43 | -------------------------------------------------------------------------------- /src/routes/tests/always-errors/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 |

This query always errors

18 | 19 | {#if foo.data} 20 |

query has data.

21 | {/if} 22 | {#if foo.error} 23 |

query errored.

24 | {/if} 25 | {#if foo.isLoading} 26 |

query is loading.

27 | {/if} 28 | {#if foo.error && foo.isLoading} 29 |

{fail('query errored and is loading. (impossible state unless useStale were true)')}

30 | {/if} 31 | {#if foo.data && foo.isLoading} 32 |

{fail('query has data and is loading. (impossible state unless useStale were true)')}

33 | {/if} 34 | {#if foo.data && foo.error} 35 |

query errored and has data. (impossible state)

36 | {/if} 37 | {#if !foo.isLoading && !foo.error && !foo.data} 38 |

{fail('query is not loading and did not error and has no data. (impossible state)')}

39 | {/if} 40 | {#if foo.isLoading && foo.error && foo.data} 41 |

{fail('query is loading and has error and has data. (impossible state)')}

42 | {/if} 43 | 44 | {#if foo.error}

error message:

45 |
 {foo.error.message} 
46 | {/if} 47 |
48 | -------------------------------------------------------------------------------- /src/convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type { 12 | DataModelFromSchemaDefinition, 13 | DocumentByName, 14 | TableNamesInDataModel, 15 | SystemTableNames, 16 | } from "convex/server"; 17 | import type { GenericId } from "convex/values"; 18 | import schema from "../schema.js"; 19 | 20 | /** 21 | * The names of all of your Convex tables. 22 | */ 23 | export type TableNames = TableNamesInDataModel; 24 | 25 | /** 26 | * The type of a document stored in Convex. 27 | * 28 | * @typeParam TableName - A string literal type of the table name (like "users"). 29 | */ 30 | export type Doc = DocumentByName< 31 | DataModel, 32 | TableName 33 | >; 34 | 35 | /** 36 | * An identifier for a document in Convex. 37 | * 38 | * Convex documents are uniquely identified by their `Id`, which is accessible 39 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 40 | * 41 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 42 | * 43 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 44 | * strings when type checking. 45 | * 46 | * @typeParam TableName - A string literal type of the table name (like "users"). 47 | */ 48 | export type Id = 49 | GenericId; 50 | 51 | /** 52 | * A type describing your Convex data model. 53 | * 54 | * This type includes information about what tables you have, the type of 55 | * documents stored in those tables, and the indexes defined on them. 56 | * 57 | * This type is used to parameterize methods like `queryGeneric` and 58 | * `mutationGeneric` to make them type-safe. 59 | */ 60 | export type DataModel = DataModelFromSchemaDefinition; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex-svelte", 3 | "author": "Convex, Inc. ", 4 | "version": "0.0.12", 5 | "license": "Apache-2.0", 6 | "homepage": "https://convex.dev", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/get-convex/convex-svelte.git" 10 | }, 11 | "scripts": { 12 | "dev": "npm-run-all dev:init --parallel dev:server dev:client", 13 | "dev:client": "vite dev --open", 14 | "dev:server": "convex dev", 15 | "dev:init": "convex dev --until-success --run messages:seed", 16 | "build": "vite build && npm run package", 17 | "preview": "vite preview", 18 | "package": "svelte-kit sync && svelte-package && publint", 19 | "prepublishOnly": "npm run package", 20 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 21 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 22 | "format": "prettier --write .", 23 | "lint": "prettier --check . && eslint .", 24 | "test:unit": "vitest", 25 | "test": "npm run test:unit -- --run && npm run test:e2e", 26 | "test:e2e": "playwright test" 27 | }, 28 | "files": [ 29 | "dist", 30 | "!dist/**/*.test.*", 31 | "!dist/**/*.spec.*" 32 | ], 33 | "sideEffects": [ 34 | "**/*.css" 35 | ], 36 | "svelte": "./dist/index.js", 37 | "types": "./dist/index.d.ts", 38 | "type": "module", 39 | "exports": { 40 | ".": { 41 | "types": "./dist/index.d.ts", 42 | "svelte": "./dist/index.js" 43 | } 44 | }, 45 | "peerDependencies": { 46 | "convex": "^1.10.0", 47 | "svelte": "^5.0.0" 48 | }, 49 | "devDependencies": { 50 | "@playwright/test": "^1.54.2", 51 | "@sveltejs/adapter-auto": "^3.3.1", 52 | "@sveltejs/kit": "^2.30.0", 53 | "@sveltejs/package": "^2.4.1", 54 | "@sveltejs/vite-plugin-svelte": "^4.0.4", 55 | "@types/eslint": "^9.6.1", 56 | "@types/node": "^22.17.1", 57 | "convex": "^1.25.4", 58 | "eslint": "^9.33.0", 59 | "eslint-config-prettier": "^9.1.2", 60 | "eslint-plugin-svelte": "^2.46.1", 61 | "globals": "^15.15.0", 62 | "npm-run-all": "^4.1.5", 63 | "prettier": "^3.6.2", 64 | "prettier-plugin-svelte": "^3.4.0", 65 | "publint": "^0.2.12", 66 | "svelte": "^5.38.1", 67 | "svelte-check": "^4.3.1", 68 | "typescript": "^5.9.2", 69 | "typescript-eslint": "^8.39.1", 70 | "vite": "^5.4.19", 71 | "vitest": "^2.1.9" 72 | }, 73 | "dependencies": { 74 | "esm-env": "^1.2.2", 75 | "runed": "^0.31.1" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // import path from 'path'; 9 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 10 | 11 | /** 12 | * See https://playwright.dev/docs/test-configuration. 13 | */ 14 | export default defineConfig({ 15 | testDir: './e2e', 16 | 17 | /* Run tests in files in parallel */ 18 | fullyParallel: false, 19 | 20 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 21 | forbidOnly: !!process.env.CI, 22 | 23 | /* Retry on CI only */ 24 | retries: process.env.CI ? 2 : 0, 25 | 26 | /* Opt out of parallel tests on CI. */ 27 | workers: process.env.CI ? 1 : undefined, 28 | 29 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 30 | reporter: 'html', 31 | 32 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 33 | use: { 34 | /* Base URL to use in actions like `await page.goto('/')`. */ 35 | // baseURL: 'http://127.0.0.1:3000', 36 | 37 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 38 | trace: 'on-first-retry' 39 | }, 40 | 41 | /* Configure projects for major browsers */ 42 | // webServer: { 43 | // command: 'npm run start', 44 | // url: 'http://127.0.0.1:3000', 45 | // reuseExistingServer: !process.env.CI, 46 | // }, 47 | projects: [ 48 | { 49 | name: 'chromium', 50 | use: { ...devices['Desktop Chrome'] } 51 | } 52 | /* 53 | { 54 | name: 'firefox', 55 | use: { ...devices['Desktop Firefox'] }, 56 | }, 57 | 58 | { 59 | name: 'webkit', 60 | use: { ...devices['Desktop Safari'] }, 61 | }, 62 | */ 63 | 64 | /* Test against mobile viewports. */ 65 | // { 66 | // name: 'Mobile Chrome', 67 | // use: { ...devices['Pixel 5'] }, 68 | // }, 69 | // { 70 | // name: 'Mobile Safari', 71 | // use: { ...devices['iPhone 12'] }, 72 | // }, 73 | 74 | /* Test against branded browsers. */ 75 | // { 76 | // name: 'Microsoft Edge', 77 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 78 | // }, 79 | // { 80 | // name: 'Google Chrome', 81 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 82 | // }, 83 | ], 84 | 85 | /* Run your local dev server before starting the tests */ webServer: { 86 | command: 'npm run build && npm run preview', 87 | port: 4173 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /src/routes/inputs/Inputs.svelte: -------------------------------------------------------------------------------- 1 | 52 | 53 |
54 | {#if serverNumbers.isLoading || !numbers} 55 |
56 |

Loading values...

57 |
58 | {:else} 59 |
60 | 61 | handleNumericInput('a', e)} value={numbers.a} /> 62 |
63 | 64 |
65 | 66 | handleNumericInput('b', e)} value={numbers.b} /> 67 |
68 | 69 |
70 | 71 | handleNumericInput('c', e)} value={numbers.c} /> 72 |
73 | 74 |
75 |

Local values:

76 |
    77 |
  • a: {numbers.a}
  • 78 |
  • b: {numbers.b}
  • 79 |
  • c: {numbers.c}
  • 80 |
81 |
82 | 83 |
84 |

Server values:

85 |
    86 |
  • a: {serverNumbers.data.a}
  • 87 |
  • b: {serverNumbers.data.b}
  • 88 |
  • c: {serverNumbers.data.c}
  • 89 |
90 |
91 | {/if} 92 |
93 | -------------------------------------------------------------------------------- /src/convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. 4 | See https://docs.convex.dev/functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from './_generated/server'; 11 | import { v } from 'convex/values'; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string() 18 | }, 19 | 20 | // Function implementation. 21 | handler: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query('tablename').collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | } 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: 'hello' 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from './_generated/server'; 50 | import { v } from 'convex/values'; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string() 57 | }, 58 | 59 | // Function implementation. 60 | handler: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert('messages', message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | } 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: 'Hello!', second: 'me' }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: 'Hello!', second: 'me' }).then((result) => console.log(result)); 83 | } 84 | ``` 85 | 86 | Use the Convex CLI to push your functions to a deployment. See everything 87 | the Convex CLI can do by running `npx convex -h` in your project root 88 | directory. To learn more, launch the docs with `npx convex docs`. 89 | -------------------------------------------------------------------------------- /src/convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { 12 | actionGeneric, 13 | httpActionGeneric, 14 | queryGeneric, 15 | mutationGeneric, 16 | internalActionGeneric, 17 | internalMutationGeneric, 18 | internalQueryGeneric, 19 | } from "convex/server"; 20 | 21 | /** 22 | * Define a query in this Convex app's public API. 23 | * 24 | * This function will be allowed to read your Convex database and will be accessible from the client. 25 | * 26 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 27 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 28 | */ 29 | export const query = queryGeneric; 30 | 31 | /** 32 | * Define a query that is only accessible from other Convex functions (but not from the client). 33 | * 34 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 35 | * 36 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 37 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 38 | */ 39 | export const internalQuery = internalQueryGeneric; 40 | 41 | /** 42 | * Define a mutation in this Convex app's public API. 43 | * 44 | * This function will be allowed to modify your Convex database and will be accessible from the client. 45 | * 46 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 47 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 48 | */ 49 | export const mutation = mutationGeneric; 50 | 51 | /** 52 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 53 | * 54 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 55 | * 56 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 57 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 58 | */ 59 | export const internalMutation = internalMutationGeneric; 60 | 61 | /** 62 | * Define an action in this Convex app's public API. 63 | * 64 | * An action is a function which can execute any JavaScript code, including non-deterministic 65 | * code and code with side-effects, like calling third-party services. 66 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 67 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 68 | * 69 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 70 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 71 | */ 72 | export const action = actionGeneric; 73 | 74 | /** 75 | * Define an action that is only accessible from other Convex functions (but not from the client). 76 | * 77 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 78 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 79 | */ 80 | export const internalAction = internalActionGeneric; 81 | 82 | /** 83 | * Define a Convex HTTP action. 84 | * 85 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 86 | * as its second. 87 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 88 | */ 89 | export const httpAction = httpActionGeneric; 90 | -------------------------------------------------------------------------------- /src/routes/Chat.svelte: -------------------------------------------------------------------------------- 1 | 42 | 43 |
44 | 45 | 52 |
53 | 54 | 55 |
56 |
57 | 58 | 59 |
60 |
61 | 62 | 63 | 64 |
65 | 66 | {#if messages.isLoading} 67 | Loading... 68 | {:else if messages.error} 69 | failed to load {messages.error} 70 | {:else} 71 |
    72 |
      73 | {#each messages.data as message} 74 |
    • 75 | {message.author} 76 | {message.body} 77 | {formatDate(message._creationTime)} 78 |
    • 79 | {/each} 80 |
    81 |
82 | {/if} 83 |
84 | 85 | 167 | -------------------------------------------------------------------------------- /src/convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { 12 | ActionBuilder, 13 | HttpActionBuilder, 14 | MutationBuilder, 15 | QueryBuilder, 16 | GenericActionCtx, 17 | GenericMutationCtx, 18 | GenericQueryCtx, 19 | GenericDatabaseReader, 20 | GenericDatabaseWriter, 21 | } from "convex/server"; 22 | import type { DataModel } from "./dataModel.js"; 23 | 24 | /** 25 | * Define a query in this Convex app's public API. 26 | * 27 | * This function will be allowed to read your Convex database and will be accessible from the client. 28 | * 29 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 30 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 31 | */ 32 | export declare const query: QueryBuilder; 33 | 34 | /** 35 | * Define a query that is only accessible from other Convex functions (but not from the client). 36 | * 37 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 38 | * 39 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 40 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 41 | */ 42 | export declare const internalQuery: QueryBuilder; 43 | 44 | /** 45 | * Define a mutation in this Convex app's public API. 46 | * 47 | * This function will be allowed to modify your Convex database and will be accessible from the client. 48 | * 49 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 50 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 51 | */ 52 | export declare const mutation: MutationBuilder; 53 | 54 | /** 55 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 56 | * 57 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 58 | * 59 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 60 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 61 | */ 62 | export declare const internalMutation: MutationBuilder; 63 | 64 | /** 65 | * Define an action in this Convex app's public API. 66 | * 67 | * An action is a function which can execute any JavaScript code, including non-deterministic 68 | * code and code with side-effects, like calling third-party services. 69 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 70 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 71 | * 72 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 73 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 74 | */ 75 | export declare const action: ActionBuilder; 76 | 77 | /** 78 | * Define an action that is only accessible from other Convex functions (but not from the client). 79 | * 80 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 81 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 82 | */ 83 | export declare const internalAction: ActionBuilder; 84 | 85 | /** 86 | * Define an HTTP action. 87 | * 88 | * This function will be used to respond to HTTP requests received by a Convex 89 | * deployment if the requests matches the path and method where this action 90 | * is routed. Be sure to route your action in `convex/http.js`. 91 | * 92 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 93 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 94 | */ 95 | export declare const httpAction: HttpActionBuilder; 96 | 97 | /** 98 | * A set of services for use within Convex query functions. 99 | * 100 | * The query context is passed as the first argument to any Convex query 101 | * function run on the server. 102 | * 103 | * This differs from the {@link MutationCtx} because all of the services are 104 | * read-only. 105 | */ 106 | export type QueryCtx = GenericQueryCtx; 107 | 108 | /** 109 | * A set of services for use within Convex mutation functions. 110 | * 111 | * The mutation context is passed as the first argument to any Convex mutation 112 | * function run on the server. 113 | */ 114 | export type MutationCtx = GenericMutationCtx; 115 | 116 | /** 117 | * A set of services for use within Convex action functions. 118 | * 119 | * The action context is passed as the first argument to any Convex action 120 | * function run on the server. 121 | */ 122 | export type ActionCtx = GenericActionCtx; 123 | 124 | /** 125 | * An interface to read from the database within Convex query functions. 126 | * 127 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 128 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 129 | * building a query. 130 | */ 131 | export type DatabaseReader = GenericDatabaseReader; 132 | 133 | /** 134 | * An interface to read from and write to the database within Convex mutation 135 | * functions. 136 | * 137 | * Convex guarantees that all writes within a single mutation are 138 | * executed atomically, so you never have to worry about partial writes leaving 139 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 140 | * for the guarantees Convex provides your functions. 141 | */ 142 | export type DatabaseWriter = GenericDatabaseWriter; 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Convex](https://www.convex.dev/) is the typesafe backend-as-a-service with realtime updates, server functions, crons and scheduled jobs, file storage, vector search, and more. 2 | 3 | [Quickstart](https://docs.convex.dev/quickstart/svelte) 4 | 5 | # convex-svelte 6 | 7 | Receive live updates to Convex query subscriptions and call mutations and actions from Svelte with `convex-svelte`. 8 | 9 | To install: 10 | 11 | ``` 12 | npm install convex convex-svelte 13 | ``` 14 | 15 | Run `npx convex init` to get started with Convex. 16 | 17 | See the [example app live](https://convex-svelte.vercel.app/). 18 | 19 | `convex-svelte` provides a `setupConvex()` function which takes a Convex deployment URL, 20 | a `useConvexClient()` which returns a [ConvexClient](https://docs.convex.dev/api/classes/browser.ConvexClient) 21 | used to set authentication credentials and run Convex mutations and actions, 22 | and a `useQuery()` function for subscribing to Convex queries. 23 | 24 | ### Example 25 | 26 | Call `setupConvex()` in a component above the components that need to Convex queries 27 | and use `useQuery()` components where you need to listen to the query. 28 | 29 | See [+layout.svelte](src/routes/+layout.svelte) for `setupConvex()` 30 | 31 | ```svelte 32 | 37 | ``` 38 | 39 | and [Chat.svelte](src/routes/Chat.svelte) for how to use `useQuery()` 40 | 41 | ```svelte 42 | 47 | 48 | ... 49 | {#if query.isLoading} 50 | Loading... 51 | {:else if query.error != null} 52 | failed to load: {query.error.toString()} 53 | {:else} 54 |
    55 | {#each query.data as message} 56 |
  • 57 | {message.author} 58 | {message.body} 59 |
  • 60 | {/each} 61 |
62 | {/if} 63 | ``` 64 | 65 | Running a mutation looks like 66 | 67 | ```svelte 68 | 86 | 87 |
88 | 89 | 90 | 91 |
92 | ``` 93 | 94 | ### Conditionally skipping queries 95 | 96 | You can conditionally skip a query by returning the string `'skip'` from the arguments function. 97 | This is useful when a query depends on some condition, like authentication state or user input. 98 | 99 | ```svelte 100 | 111 | 112 | {#if activeUserResponse.isLoading} 113 | Loading user... 114 | {:else if activeUserResponse.error} 115 | Error: {activeUserResponse.error} 116 | {:else if activeUserResponse.data} 117 | Welcome, {activeUserResponse.data.name}! 118 | {/if} 119 | ``` 120 | 121 | When a query is skipped, `isLoading` will be `false`, `error` will be `null`, and `data` will be `undefined`. 122 | 123 | ### Server-side rendering 124 | 125 | `useQuery()` accepts an `initialData` option in its third argument. 126 | By defining a `load()` function in a +page.server.ts file 127 | that uses the `ConvexHttpClient` to request the same query to get initial data 128 | and passing that through to the `initialData` option of a useQuery call you can avoid an initial loading state. 129 | 130 | ```ts 131 | // +page.server.ts 132 | import { ConvexHttpClient } from 'convex/browser'; 133 | import type { PageServerLoad } from './$types.js'; 134 | import { PUBLIC_CONVEX_URL } from '$env/static/public'; 135 | import { api } from '../convex/_generated/api.js'; 136 | 137 | export const load = (async () => { 138 | const client = new ConvexHttpClient(PUBLIC_CONVEX_URL!); 139 | return { 140 | messages: await client.query(api.messages.list, { muteWords: [] }) 141 | }; 142 | }) satisfies PageServerLoad; 143 | ``` 144 | 145 | ```svelte 146 | 160 | ``` 161 | 162 | Combining specifying `initialData` and either setting the `keepPreviousData` option to true or never modifying the arguments passed to a query should be enough to avoid ever seeing a loading state for a `useQuery()`. 163 | 164 | ### Troubleshooting 165 | 166 | #### effect_in_teardown Error 167 | 168 | If you encounter `effect_in_teardown` errors when using `useQuery` in components that can be conditionally rendered (like dialogs, modals, or popups), this is caused by wrapping `useQuery` in a `$derived` block that depends on reactive state. 169 | 170 | When `useQuery` is wrapped in `$derived`, state changes during component cleanup can trigger re-evaluation of the `$derived`, which attempts to create a new `useQuery` instance. Since `useQuery` internally creates a `$effect`, and effects cannot be created during cleanup, this throws an error. 171 | 172 | Use [Conditionally skipping queries](#conditionally-skipping-queries) instead. By calling `useQuery` unconditionally at the top level and passing a function that returns `'skip'`, the function is evaluated inside `useQuery`'s own effect tracking, preventing query recreation during cleanup. 173 | 174 | ### Deploying a Svelte App 175 | 176 | In production build pipelines use the build command 177 | 178 | ```bash 179 | npx convex deploy --cmd-url-env-var-name PUBLIC_CONVEX_URL --cmd 'npm run build' 180 | ``` 181 | 182 | to build your Svelte app and deploy Convex functions. 183 | 184 | # Trying out this library 185 | 186 | Clone this repo and install dependencies with `npm install` then start a development server: 187 | 188 | ```bash 189 | npm run dev 190 | ``` 191 | 192 | This will run you through creating a Convex account and a deployment. 193 | 194 | Everything inside `src/lib` is part of the library, everything inside `src/routes` is an example app. 195 | 196 | # Developing this library 197 | 198 | To build the library: 199 | 200 | ```bash 201 | npm run package 202 | ``` 203 | 204 | To create a production version of the showcase app: 205 | 206 | ```bash 207 | npm run build 208 | ``` 209 | 210 | You can preview the production build with `npm run preview`. 211 | 212 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 213 | 214 | ## Publishing 215 | 216 | Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)). 217 | 218 | To publish your library to [npm](https://www.npmjs.com): 219 | 220 | ```bash 221 | npm publish 222 | ``` 223 | -------------------------------------------------------------------------------- /src/lib/client.svelte.ts: -------------------------------------------------------------------------------- 1 | import { getContext, setContext, untrack } from 'svelte'; 2 | import { ConvexClient, type ConvexClientOptions } from 'convex/browser'; 3 | import { 4 | type FunctionReference, 5 | type FunctionArgs, 6 | type FunctionReturnType, 7 | getFunctionName 8 | } from 'convex/server'; 9 | import { convexToJson, type Value } from 'convex/values'; 10 | import { BROWSER } from 'esm-env'; 11 | 12 | const _contextKey = '$$_convexClient'; 13 | 14 | export const useConvexClient = (): ConvexClient => { 15 | const client = getContext(_contextKey) as ConvexClient | undefined; 16 | if (!client) { 17 | throw new Error( 18 | 'No ConvexClient was found in Svelte context. Did you forget to call setupConvex() in a parent component?' 19 | ); 20 | } 21 | return client; 22 | }; 23 | 24 | export const setConvexClientContext = (client: ConvexClient): void => { 25 | setContext(_contextKey, client); 26 | }; 27 | 28 | export const setupConvex = (url: string, options: ConvexClientOptions = {}) => { 29 | if (!url || typeof url !== 'string') { 30 | throw new Error('Expected string url property for setupConvex'); 31 | } 32 | const optionsWithDefaults = { disabled: !BROWSER, ...options }; 33 | 34 | const client = new ConvexClient(url, optionsWithDefaults); 35 | setConvexClientContext(client); 36 | $effect(() => () => client.close()); 37 | }; 38 | 39 | // Internal sentinel for "skip" so we don't pass the literal string through everywhere 40 | const SKIP = Symbol('convex.useQuery.skip'); 41 | type Skip = typeof SKIP; 42 | 43 | type UseQueryOptions> = { 44 | // Use this data and assume it is up to date (typically for SSR and hydration) 45 | initialData?: FunctionReturnType; 46 | // Instead of loading, render result from outdated args 47 | keepPreviousData?: boolean; 48 | }; 49 | 50 | type UseQueryReturn> = 51 | | { data: undefined; error: undefined; isLoading: true; isStale: false } 52 | | { data: undefined; error: Error; isLoading: false; isStale: boolean } 53 | | { data: FunctionReturnType; error: undefined; isLoading: false; isStale: boolean }; 54 | 55 | // Note that swapping out the current Convex client is not supported. 56 | /** 57 | * Subscribe to a Convex query and return a reactive query result object. 58 | * Pass reactive args object or a closure returning args to update args reactively. 59 | * 60 | * Supports React-style `"skip"` to avoid subscribing: 61 | * useQuery(api.users.get, () => (isAuthed ? {} : 'skip')) 62 | * 63 | * @param query - a FunctionReference like `api.dir1.dir2.filename.func`. 64 | * @param args - Arguments object / closure, or the string `"skip"` (or a closure returning it). 65 | * @param options - UseQueryOptions like `initialData` and `keepPreviousData`. 66 | * @returns an object containing data, isLoading, error, and isStale. 67 | */ 68 | export function useQuery>( 69 | query: Query, 70 | args: FunctionArgs | 'skip' | (() => FunctionArgs | 'skip') = {}, 71 | options: UseQueryOptions | (() => UseQueryOptions) = {} 72 | ): UseQueryReturn { 73 | const client = useConvexClient(); 74 | if (typeof query === 'string') { 75 | throw new Error('Query must be a functionReference object, not a string'); 76 | } 77 | 78 | const state: { 79 | result: FunctionReturnType | Error | undefined; 80 | // The last result we actually received, if this query has ever received one. 81 | lastResult: FunctionReturnType | Error | undefined; 82 | // The args (query key) of the last result that was received. 83 | argsForLastResult: FunctionArgs | Skip | undefined; 84 | // If the args have never changed, fine to use initialData if provided. 85 | haveArgsEverChanged: boolean; 86 | } = $state({ 87 | result: parseOptions(options).initialData, 88 | lastResult: undefined, 89 | argsForLastResult: undefined, 90 | haveArgsEverChanged: false 91 | }); 92 | 93 | // When args change we need to unsubscribe to the old query and subscribe 94 | // to the new one. 95 | $effect(() => { 96 | const argsObject = parseArgs(args); 97 | 98 | // If skipped, don't create any subscription 99 | if (argsObject === SKIP) { 100 | // Clear transient result to mimic React: not loading, no data 101 | state.result = undefined; 102 | state.argsForLastResult = SKIP; 103 | return; 104 | } 105 | 106 | const unsubscribe = client.onUpdate( 107 | query, 108 | argsObject, 109 | (dataFromServer) => { 110 | const copy = structuredClone(dataFromServer); 111 | state.result = copy; 112 | state.argsForLastResult = argsObject; 113 | state.lastResult = copy; 114 | }, 115 | (e: Error) => { 116 | state.result = e; 117 | state.argsForLastResult = argsObject; 118 | const copy = structuredClone(e); 119 | state.lastResult = copy; 120 | } 121 | ); 122 | 123 | // Cleanup on args change/unmount 124 | return unsubscribe; 125 | }); 126 | 127 | /* 128 | ** staleness & args tracking ** 129 | * Are the args (the query key) the same as the last args we received a result for? 130 | */ 131 | const currentArgs = $derived(parseArgs(args)); 132 | const initialArgs = parseArgs(args); 133 | 134 | const sameArgsAsLastResult = $derived( 135 | state.argsForLastResult !== undefined && 136 | currentArgs !== SKIP && 137 | state.argsForLastResult !== SKIP && 138 | jsonEqualArgs( 139 | state.argsForLastResult as Record, 140 | currentArgs as Record 141 | ) 142 | ); 143 | 144 | const staleAllowed = $derived(!!(parseOptions(options).keepPreviousData && state.lastResult)); 145 | const isSkipped = $derived(currentArgs === SKIP); 146 | 147 | // Once args change, move off of initialData. 148 | $effect(() => { 149 | if (!untrack(() => state.haveArgsEverChanged)) { 150 | const curr = parseArgs(args); 151 | if (!argsKeyEqual(initialArgs, curr)) { 152 | state.haveArgsEverChanged = true; 153 | const opts = parseOptions(options); 154 | if (opts.initialData !== undefined) { 155 | state.argsForLastResult = initialArgs === SKIP ? SKIP : $state.snapshot(initialArgs); 156 | state.lastResult = opts.initialData; 157 | } 158 | } 159 | } 160 | }); 161 | 162 | /* 163 | ** compute sync result ** 164 | * Return value or undefined; never an error object. 165 | */ 166 | const syncResult: FunctionReturnType | undefined = $derived.by(() => { 167 | if (isSkipped) return undefined; 168 | 169 | const opts = parseOptions(options); 170 | if (opts.initialData && !state.haveArgsEverChanged) { 171 | return state.result; 172 | } 173 | 174 | let value; 175 | try { 176 | value = client.disabled 177 | ? undefined 178 | : client.client.localQueryResult( 179 | getFunctionName(query), 180 | currentArgs as Record 181 | ); 182 | } catch (e) { 183 | if (!(e instanceof Error)) { 184 | console.error('threw non-Error instance', e); 185 | throw e; 186 | } 187 | value = e; 188 | } 189 | // Touch reactive state.result so updates retrigger computations 190 | state.result; 191 | return value; 192 | }); 193 | 194 | const result = $derived.by(() => { 195 | return syncResult !== undefined ? syncResult : staleAllowed ? state.lastResult : undefined; 196 | }); 197 | 198 | const isStale = $derived( 199 | !isSkipped && 200 | syncResult === undefined && 201 | staleAllowed && 202 | !sameArgsAsLastResult && 203 | result !== undefined 204 | ); 205 | 206 | const data = $derived.by(() => { 207 | if (result instanceof Error) return undefined; 208 | return result; 209 | }); 210 | 211 | const error = $derived.by(() => { 212 | if (result instanceof Error) return result; 213 | return undefined; 214 | }); 215 | 216 | /* 217 | ** public shape ** 218 | * This TypeScript cast promises data is not undefined if error and isLoading are checked first. 219 | */ 220 | return { 221 | get data() { 222 | return data; 223 | }, 224 | get isLoading() { 225 | return isSkipped ? false : error === undefined && data === undefined; 226 | }, 227 | get error() { 228 | return error; 229 | }, 230 | get isStale() { 231 | return isSkipped ? false : isStale; 232 | } 233 | } as UseQueryReturn; 234 | } 235 | 236 | /** 237 | * args can be an object, "skip", or a closure returning either 238 | **/ 239 | function parseArgs( 240 | args: Record | 'skip' | (() => Record | 'skip') 241 | ): Record | Skip { 242 | if (typeof args === 'function') { 243 | args = args(); 244 | } 245 | if (args === 'skip') return SKIP; 246 | return $state.snapshot(args); 247 | } 248 | 249 | // options can be an object or a closure 250 | function parseOptions>( 251 | options: UseQueryOptions | (() => UseQueryOptions) 252 | ): UseQueryOptions { 253 | if (typeof options === 'function') { 254 | options = options(); 255 | } 256 | return $state.snapshot(options); 257 | } 258 | 259 | function jsonEqualArgs(a: Record, b: Record): boolean { 260 | return JSON.stringify(convexToJson(a)) === JSON.stringify(convexToJson(b)); 261 | } 262 | 263 | function argsKeyEqual(a: Record | Skip, b: Record | Skip): boolean { 264 | if (a === SKIP && b === SKIP) return true; 265 | if (a === SKIP || b === SKIP) return false; 266 | return jsonEqualArgs(a, b); 267 | } 268 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2024 Convex, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------