'use client'; // InstantSuspenseProvider can only be used in a client context so this prevents errors from trying to use it in a server component. import { FrameworkClient, InstantConfig, InstantSchemaDef, InstaQLResponse, PageInfoResponse, RuleParams, User, ValidQuery, } from '@instantdb/core'; import InstantReactWebDatabase from '../InstantReactWebDatabase.ts'; import { createHydrationStreamProvider, isServer, } from './HydrationStreamProvider.tsx'; import { createContext, useCallback, useContext, useMemo, useRef, useState, useEffect, } from 'react'; import { InstantReactAbstractDatabase, useQueryInternal, } from '@instantdb/react-common'; type InstantSuspenseProviderProps< Schema extends InstantSchemaDef, > = { nonce?: string; children: React.ReactNode; db: InstantReactWebDatabase; user?: User | null; }; const stream = createHydrationStreamProvider(); type SuspenseQueryContextValue = { useSuspenseQuery: (query: any, opts?: SuspenseQueryOpts) => any; ssrUser: User | null | undefined; }; export const SuspsenseQueryContext = createContext(null); // Creates a typed useSuspense hook export const createUseSuspenseQuery = < Schema extends InstantSchemaDef, UseDates extends boolean, >( _db: InstantReactWebDatabase, ): (>( q: Q, opts?: { ruleParams: RuleParams; }, ) => { data: InstaQLResponse>; pageInfo?: PageInfoResponse; }) => { return >(q: any, opts: any) => { const ctx = useContext(SuspsenseQueryContext); if (!ctx) { throw new Error( 'useSuspenseQuery must be used within a SuspenseQueryProvider', ); } return ctx.useSuspenseQuery(q, opts) as any; }; }; type SuspenseQueryOpts = { ruleParams: RuleParams; }; function makeUseSuspenseQueryServer(client: FrameworkClient) { return function useSuspenseQueryServer(query: any, opts: SuspenseQueryOpts) { let entry = client.getExistingResultForQuery(query, opts); if (!entry) { entry = client.query(query, opts); } if (entry.status === 'pending') { throw entry.promise; } if (entry.status === 'error') { throw entry.error; } if (entry.status === 'success') { switch (entry.type) { case 'session': { return entry.data; } case 'http': { const data = entry.data; const result = client.completeIsomorphic( query, data.triples, data.attrs, data.pageInfo, ); return result; } } } }; } function makeUseSuspenseQueryClient( db: InstantReactAbstractDatabase, client: FrameworkClient, ) { function getEntry(query: any, opts: SuspenseQueryOpts, allowFetch: boolean) { const entry = client.getExistingResultForQuery(query, opts); if (entry?.status === 'pending') { throw entry.promise; } if (entry?.status === 'error') { throw entry.error; } if (entry?.status === 'success') { switch (entry.type) { case 'session': { return entry.data; } case 'http': { const data = entry.data; const result = client.completeIsomorphic( query, data.triples, data.attrs, data.pageInfo, ); return result; } } } if (allowFetch) { const promise = client.queryClient(query, opts); throw promise; } } return function useSuspenseQueryClient(query: any, opts: SuspenseQueryOpts) { const useQueryResult = useQueryInternal( db.core, query, opts, // Returns the server result for useSyncExternalStore () => { try { const res = getEntry(query, opts, false); return res; } catch (throwable) { return { error: throwable }; } }, ); const hasData = !!useQueryResult.state.data; const queryHash = useQueryResult.queryHash; useEffect(() => { if (hasData) { // We have a newer result, so remove the cached SSR or suspended // result from the framework client cache client.removeCachedQueryResult(queryHash); } }, [hasData, queryHash]); if (useQueryResult.state.data) { return { data: useQueryResult.state.data, pageInfo: useQueryResult.state.pageInfo, }; } if (useQueryResult.state.error) { throw useQueryResult.state.error; } return getEntry(query, opts, true); }; } function createFrameworkClient( db: InstantReactAbstractDatabase, user: User | null | undefined, ) { if (isServer) { if (user && !user.refresh_token) { throw new Error( 'User must have a refresh_token field. Recieved: ' + JSON.stringify(user, null, 2), ); } return new FrameworkClient({ token: user?.refresh_token, db: db.core, }); } // On the client, make sure we only have a single framework // in case our suspense provider gets unmounted const existing = db.core._reactor._frameworkClient; if (existing) { return existing; } const client = new FrameworkClient({ db: db.core }); db.core._reactor.setFrameworkClient(client); return client; } export const InstantSuspenseProvider = ( props: InstantSuspenseProviderProps, ) => { if (!props.db) { throw new Error('Must provide db to InstantSuspenseProvider'); } const db = props.db; const [trackedKeys] = useState(() => new Set()); const clientRef = useRef( createFrameworkClient(props.db, props.user), ); if (isServer) { clientRef.current.subscribe((result) => { const { queryHash } = result; trackedKeys.add(queryHash); }); } const useSuspenseQuery = useCallback( isServer ? makeUseSuspenseQueryServer(clientRef.current) : makeUseSuspenseQueryClient(db, clientRef.current), [], ); const contextValue = useMemo(() => { return { useSuspenseQuery, ssrUser: props.user }; }, [useSuspenseQuery, props.user]); return ( { const toSend: { queryKey: string; value: any }[] = []; for (const [key, value] of clientRef.current!.resultMap.entries()) { if (trackedKeys.has(key) && value.status === 'success') { toSend.push({ queryKey: key, value: value.data, }); } } trackedKeys.clear(); return toSend; }} onEntries={(entries) => { entries.forEach((entry) => { clientRef.current!.addQueryResult(entry.queryKey, entry.value); }); }} > {props.children} ); };