-
Notifications
You must be signed in to change notification settings - Fork 14
Thenable query #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Tyler-Petrov
wants to merge
21
commits into
get-convex:main
Choose a base branch
from
Tyler-Petrov:thenable-query
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+567
−7
Open
Thenable query #32
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
8a1f7e8
Ignore pnpm lock file
Tyler-Petrov 5431649
Upgraded installed packages
Tyler-Petrov 9a4f5e7
Add the query handler
Tyler-Petrov cf22c15
Add the example
Tyler-Petrov 42f4e04
Add error handling to the svelte:boundary
Tyler-Petrov 35ac057
Add the query handler
Tyler-Petrov ba219cb
Add the example
Tyler-Petrov c39f26b
Add error handling to the svelte:boundary
Tyler-Petrov e198b77
convert spaces to tabs
thomasballinger 5cd205b
Merge remote-tracking branch 'refs/remotes/origin/thenable-query' int…
Tyler-Petrov e308b8d
Updated the convexQuery api to resemble Sveltekits Remote Function api
Tyler-Petrov b76ea52
Renamed module exports
Tyler-Petrov 7380e7d
Updated the return type for convexQuery
Tyler-Petrov ee0abfd
Updated types. Added the initialData option
Tyler-Petrov 6234286
Added skip to query options to prevent the query from updating
Tyler-Petrov 3650b0a
Rewrite to convexQuery handler with a customized implementation of th…
Tyler-Petrov daf0091
fixed type inferance and imports
Tyler-Petrov dfc39ae
Added initialData as an option
Tyler-Petrov c000d2e
Removed options
Tyler-Petrov 7cdedbb
spaces to tabs
Tyler-Petrov 7ea226f
Solved async reactivity loss error by using the value from the promise
Tyler-Petrov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| import { useConvexClient } from "./client.svelte"; | ||
| import { getFunctionName, type FunctionReference } from "convex/server"; | ||
| import { convexToJson } from "convex/values"; | ||
| import { tick } from "svelte"; | ||
|
|
||
|
|
||
| export type ConvexQueryOptions<Query extends FunctionReference<'query', 'public'>> = { | ||
| // Use this data and assume it is up to date (typically for SSR and hydration) | ||
| initialData?: Query['_returnType']; | ||
| }; | ||
|
|
||
| export function generateCacheKey< | ||
| Query extends FunctionReference<'query', 'public'> | ||
| >( | ||
| query: Query, | ||
| args: Query['_args'] | ||
| ) { | ||
| return getFunctionName(query) | ||
| + JSON.stringify(convexToJson(args)) | ||
| } | ||
|
|
||
| export class ConvexQuery< | ||
| Query extends FunctionReference<'query', 'public'>, | ||
| T = Query['_returnType'], | ||
| > { | ||
| _key: string; | ||
| #init = false; | ||
| #fn: () => Promise<T>; | ||
| #loading = $state(true); | ||
| unsubscribe: () => void; | ||
| #args: Query['_args']; | ||
|
|
||
| #ready = $state(false); | ||
| #raw = $state.raw<T | undefined>(undefined); | ||
| #promise: Promise<T>; | ||
|
|
||
| #error = $state.raw<Error | undefined>(undefined); | ||
|
|
||
| #then = $derived.by(() => { | ||
| const p = this.#promise; | ||
|
|
||
| return async (resolve?: (value: T) => void, reject?: (reason: any) => void) => { | ||
| try { | ||
| // svelte-ignore await_reactivity_loss | ||
| const value = await p; | ||
| // svelte-ignore await_reactivity_loss | ||
| await tick(); | ||
| resolve?.(value as T); | ||
| } catch (error) { | ||
| reject?.(error); | ||
| } | ||
| }; | ||
| }); | ||
|
|
||
| constructor(query: Query, args: Query['_args']) { | ||
| const client = useConvexClient(); | ||
|
|
||
| this._key = generateCacheKey(query, args); | ||
| this.#args = args; | ||
|
|
||
| this.#fn = () => client.query(query, this.#args); | ||
| this.#promise = $state.raw(this.#run()); | ||
|
|
||
| this.unsubscribe = client.onUpdate(query, this.#args, (result: Query['_returnType']) => { | ||
| // The first value is resolved by the promise, so we don't need to update the query here | ||
| if (!this.#ready) return; | ||
|
|
||
| this.set(result); | ||
| }, (error) => { | ||
| this.#fn = () => Promise.reject(error); | ||
| this.#promise = this.#run(); | ||
| }); | ||
| } | ||
|
|
||
| #run(): Promise<T> { | ||
| // Prevent state_unsafe_mutation error on first run when the resource is created within the template | ||
| if (this.#init) { | ||
| this.#loading = true; | ||
| } else { | ||
| this.#init = true; | ||
| } | ||
|
|
||
| // Don't use Promise.withResolvers, it's too new still | ||
| let resolve: (value: T) => void; | ||
| let reject: (e?: any) => void; | ||
| const promise: Promise<T> = new Promise<T>((res, rej) => { | ||
| resolve = res as any; | ||
| reject = rej; | ||
| }); | ||
|
|
||
| Promise.resolve(this.#fn()) | ||
| .then((value) => { | ||
| this.#ready = true; | ||
| this.#loading = false; | ||
| this.#raw = value; | ||
| this.#error = undefined; | ||
|
|
||
| resolve!(value); | ||
| }) | ||
| .catch((e) => { | ||
| this.#error = e; | ||
| this.#loading = false; | ||
| reject!(e); | ||
| }); | ||
|
|
||
| return promise; | ||
| } | ||
|
|
||
| get then() { | ||
| return this.#then; | ||
| } | ||
|
|
||
| get catch() { | ||
| this.#then; | ||
| return (reject: (reason: any) => void) => { | ||
| return this.#then(undefined, reject); | ||
| }; | ||
| } | ||
|
|
||
| get finally() { | ||
| this.#then; | ||
| return (fn: () => void) => { | ||
| return this.#then( | ||
| () => fn(), | ||
| () => fn() | ||
| ); | ||
| }; | ||
| } | ||
|
|
||
| get current(): T | undefined { | ||
| return this.#raw; | ||
| } | ||
|
|
||
| get data(): T | undefined { | ||
| return this.#raw; | ||
| } | ||
|
|
||
| get error(): Error | undefined { | ||
| return this.#error; | ||
| } | ||
|
|
||
| /** | ||
| * Returns true if the resource is loading or reloading. | ||
| */ | ||
| get loading(): boolean { | ||
| return this.#loading; | ||
| } | ||
|
|
||
| /** | ||
| * Returns true once the resource has been loaded for the first time. | ||
| */ | ||
| get ready(): boolean { | ||
| return this.#ready; | ||
| } | ||
|
|
||
| set(value: T): void { | ||
| this.#ready = true; | ||
| this.#loading = false; | ||
| this.#error = undefined; | ||
| this.#raw = value; | ||
| this.#promise = Promise.resolve(value); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| type CacheEntry = { count: number, resource: ConvexQuery<any> }; | ||
| const queryCache = new Map<string, CacheEntry>(); | ||
|
|
||
| function removeUnusedCachedValues(cacheKey: string, entry: CacheEntry) { | ||
| void tick().then(() => { | ||
| if (!entry.count && entry === queryCache.get(cacheKey)) { | ||
| entry.resource.unsubscribe(); | ||
| queryCache.delete(cacheKey); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| export const convexQuery = <Query extends FunctionReference<'query', 'public'>>( | ||
| query: Query, | ||
| args: Query['_args'] | ||
| ) => { | ||
| const cacheKey = generateCacheKey(query, args); | ||
| let entry = queryCache.get(cacheKey); | ||
|
|
||
| let tracking = true; | ||
| try { | ||
| $effect.pre(() => { | ||
| if (entry) entry.count++; | ||
| return () => { | ||
| const entry = queryCache.get(cacheKey); | ||
| if (entry) { | ||
| entry.count--; | ||
| removeUnusedCachedValues(cacheKey, entry); | ||
| } | ||
| }; | ||
| }); | ||
| } catch { | ||
| tracking = false; | ||
| } | ||
|
|
||
| let resource = entry?.resource; | ||
| if (!resource) { | ||
| resource = new ConvexQuery(query, args); | ||
| queryCache.set(cacheKey, | ||
| (entry = { | ||
| count: tracking ? 1 : 0, | ||
| resource, | ||
| }) | ||
| ); | ||
| resource | ||
| .then(() => { | ||
| removeUnusedCachedValues(cacheKey, entry!); | ||
| }) | ||
| .catch(() => { | ||
| queryCache.delete(cacheKey); | ||
| }); | ||
| } | ||
|
|
||
| return resource as ConvexQuery<Query>; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| <p>Hello</p> | ||
| <a href="/tests/thenable-dev">Thenable Dev</a> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| <script lang="ts"> | ||
| import { api } from '$convex/_generated/api.js'; | ||
| import { convexQuery } from '$lib/async.svelte.js'; | ||
|
|
||
| let fail = $state(false); | ||
| let skip = $state(false); | ||
|
|
||
| const firstMessage = $derived(await convexQuery(api.messages.firstMessage, { fail })); | ||
|
|
||
| const firstMessageQuery = $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> --> | ||
| <div> | ||
| <label for="fail">should fail</label> | ||
| <input id="fail" type="checkbox" bind:checked={fail} /> | ||
| <label for="skip">should skip</label> | ||
| <input id="skip" type="checkbox" bind:checked={skip} /> | ||
| </div> | ||
| <svelte:boundary> | ||
| <pre>Result: {JSON.stringify(firstMessage, 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 firstMessageQuery.loading} | ||
| <div>Loading...</div> | ||
| {:else if firstMessageQuery.error} | ||
| <div>Error: {firstMessageQuery.error}</div> | ||
| {:else} | ||
| <pre>Result: {JSON.stringify(firstMessageQuery.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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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