Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8a1f7e8
Ignore pnpm lock file
Tyler-Petrov Aug 14, 2025
5431649
Upgraded installed packages
Tyler-Petrov Aug 14, 2025
9a4f5e7
Add the query handler
Tyler-Petrov Aug 14, 2025
cf22c15
Add the example
Tyler-Petrov Aug 14, 2025
42f4e04
Add error handling to the svelte:boundary
Tyler-Petrov Aug 14, 2025
35ac057
Add the query handler
Tyler-Petrov Aug 14, 2025
ba219cb
Add the example
Tyler-Petrov Aug 14, 2025
c39f26b
Add error handling to the svelte:boundary
Tyler-Petrov Aug 14, 2025
e198b77
convert spaces to tabs
thomasballinger Aug 18, 2025
5cd205b
Merge remote-tracking branch 'refs/remotes/origin/thenable-query' int…
Tyler-Petrov Aug 18, 2025
e308b8d
Updated the convexQuery api to resemble Sveltekits Remote Function api
Tyler-Petrov Aug 21, 2025
b76ea52
Renamed module exports
Tyler-Petrov Aug 21, 2025
7380e7d
Updated the return type for convexQuery
Tyler-Petrov Aug 21, 2025
ee0abfd
Updated types. Added the initialData option
Tyler-Petrov Aug 21, 2025
6234286
Added skip to query options to prevent the query from updating
Tyler-Petrov Aug 21, 2025
3650b0a
Rewrite to convexQuery handler with a customized implementation of th…
Tyler-Petrov Aug 26, 2025
daf0091
fixed type inferance and imports
Tyler-Petrov Aug 26, 2025
dfc39ae
Added initialData as an option
Tyler-Petrov Aug 26, 2025
c000d2e
Removed options
Tyler-Petrov Aug 26, 2025
7cdedbb
spaces to tabs
Tyler-Petrov Aug 26, 2025
7ea226f
Solved async reactivity loss error by using the value from the promise
Tyler-Petrov Oct 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"scripts": {
"dev": "npm-run-all dev:init --parallel dev:server dev:client",
"dev:client": "vite dev --open",
"dev:client": "vite dev --open --host",
"dev:server": "convex dev",
"dev:init": "convex dev --until-success --run messages:seed",
"build": "vite build && npm run package",
Expand Down
13 changes: 13 additions & 0 deletions src/convex/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ export const send = mutation({
}
});

export const firstMessage = query({
args: {
fail: v.boolean()
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in for dev purposes for now, and won't be in the final PR

handler: async (ctx, args) => {
if (args.fail) {
console.log('fail', args.fail);
throw new Error('test error');
}
return ctx.db.query('messages').first();
}
});

import seedMessages from './seed_messages.js';
export const seed = internalMutation({
handler: async (ctx) => {
Expand Down
8 changes: 7 additions & 1 deletion src/convex/seed_messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,11 @@ export default [
_id: '2w7d7p8ysmtr6njf4yejj0jy9hsjar0',
author: 'Arnold',
body: 'While you were talking I added a feature to the product'
}
},
{
_creationTime: 1755750014686.7732,
_id: "j572hze30nnbvzcph9836fetmh7p3zgk",
author: "Tyler Petrov",
body: "We have the new Remote Functions based API!",
}
];
105 changes: 105 additions & 0 deletions src/lib/async.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useConvexClient } from "$lib/client.svelte.js";
import { type DefaultFunctionArgs, type FunctionReference } from "convex/server";

export type ConvexQuery<T> = {
then: (
onfulfilled: (value: T) => void,
onrejected: (reason: any) => void
) => void;
current: T | undefined;
error: Error | undefined;
loading: boolean;
[Symbol.dispose]: () => void;
}


export type ConvexQueryOptions<Query extends FunctionReference<'query'>> = {
// Use this data and assume it is up to date (typically for SSR and hydration)
initialData?: Query['_returnType'];
};

/**
* Subscribe to a Convex query and return a reactive query result object that can be awaited.
*
* @experimental API is experimental and could change.
* @param queryFunc - a FunctionRefernece like `api.dir1.dir2.filename.func`.
* @param args - The arguments to the query function.
* @param options - ConvexQueryOptions like `initialData`.
* @returns a thenable object. Also contains `current`, `error`, and `loading` properties.
*/
export function convexQuery<
Query extends FunctionReference<'query', 'public'>,
Args extends Query['_args']
>(
queryFunc: Query,
args: Args,
options: ConvexQueryOptions<Query> = {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only option is initialData now. Should initialData be a top level parameter

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No let's leave it as options in case something else comes up, we're always considering all kinds of options like how long to stay subscribed after the subscription is no longer needed, whether to wait for auth, etc

): ConvexQuery<Query['_returnType']> {
const client = useConvexClient();

const state: {
current: Query['_returnType'] | undefined,
error: Error | undefined,
} = $state({
current: options.initialData,
error: undefined,
});

const loading: boolean = $derived(
state.current === undefined && state.error === undefined
);

/* Get the value from Convex and subscribe to it */
const unsubscribe = client.onUpdate(
queryFunc,
args,
(result) => {
state.current = result;
state.error = undefined;
},
(err) => {
state.current = undefined;
state.error = err;
}
);

/* Unsubscribe from the query when the parent component is destroyed */
$effect(() => unsubscribe);

return {
get then() {
const value = state.current;
try {
return (
resolve: (value: Query['_returnType']) => void,
reject: (reason: any) => void,
) => {
/* If there is initial data then resolve immediately */
if (value !== undefined) {
resolve(value);
return;
}
/* If the query is already in the cache, return the cached value */
client.query(queryFunc, args).then((result) => {
resolve(value ?? result);
}).catch((err) => {
reject(err);
throw err;
});
}
} catch (err) {
state.error = err as Error;
return (
resolve: (value: Query['_returnType']) => void,
reject: (reason: any) => void,
) => {
reject(err);
}
}
},
get current() { return state.current; },
get error() { return state.error; },
get loading() { return loading; },
[Symbol.dispose]: unsubscribe
};
}
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Reexport your entry components here

export { useConvexClient, setupConvex, useQuery, setConvexClientContext } from './client.svelte.js';
export * as async from './async.svelte.js';
20 changes: 17 additions & 3 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,27 @@

<div class="app">
<main>
{@render children()}
<svelte:boundary onerror={(e) => {
console.error(e);
}}>
{@render children()}

{#snippet pending()}
<div>Loading...</div>
{/snippet}
{#snippet failed(error, reset)}
<div>Error: {error}</div>
{/snippet}
</svelte:boundary>
</main>

<footer>
<p>
visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit and
<a href="https://docs.convex.dev">docs.convex.dev</a> to learn Convex
The project uses
<a href="https://github.com/get-convex/convex-svelte">convex-svelte</a>,
<a href="https://docs.convex.dev">Convex</a>,
and
<a href="https://kit.svelte.dev">SvelteKit</a>
</p>
</footer>
</div>
Expand Down
53 changes: 53 additions & 0 deletions src/routes/tests/thenable-dev/+page.svelte
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For dev on this feature. Won't be included in final PR

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script lang="ts">
import { api } from '$convex/_generated/api.js';
import { convexQuery } from '$lib/async.svelte.js';

let fail = $state(false);

const convexQueryResult = $derived(convexQuery(api.messages.firstMessage, { fail }));
</script>

<svelte:head>
<title>Home</title>
<meta name="description" content="Svelte demo app" />
</svelte:head>

<section>
<h1>Welcome to SvelteKit with Convex</h1>
<a href="/tests">Tests</a>

<button onclick={() => fail = !fail}>{fail ? 'Fail' : 'Success'}</button>
<svelte:boundary>
<pre>Result: {JSON.stringify(await convexQueryResult, null, 2)}</pre>
<pre>Result: {JSON.stringify(await convexQuery(api.messages.firstMessage, { fail }), null, 2)}</pre>
{#snippet pending()}
<div>Loading...</div>
{/snippet}
{#snippet failed(error, retry)}
<div>Error: {error}</div>
<button onclick={retry}>Retry</button>
{/snippet}
</svelte:boundary>

{#if convexQueryResult.loading}
<div>Loading...</div>
{:else if convexQueryResult.error}
<div>Error: {convexQueryResult.error}</div>
{:else}
<pre>Result: {JSON.stringify(convexQueryResult.current, null, 2)}</pre>
{/if}
</section>

<style>
section {
display: flex;
flex-direction: column;
align-items: center;
flex: 0.6;
}

h1 {
width: 100%;
text-align: center;
}
</style>
18 changes: 18 additions & 0 deletions src/routes/thenable/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
import type { LayoutProps } from './$types';

let { data, children }: LayoutProps = $props();
</script>
<svelte:boundary onerror={(e) => {
console.error(e)
}}>
{@render children()}

{#snippet pending()}
<div>Loading...</div>
{/snippet}
{#snippet failed(error, reset)}
<p>Error: {error}</p>
<button onclick={reset}>oops! try again</button>
{/snippet}
</svelte:boundary>
Loading