@@ -3,17 +3,25 @@ import {
33 BaseConvexClient ,
44 BaseConvexClientOptions ,
55 MutationOptions ,
6+ PaginatedQueryToken ,
67 QueryToken ,
78 UserIdentityAttributes ,
89} from "./index.js" ;
910import {
1011 FunctionArgs ,
1112 FunctionReference ,
1213 FunctionReturnType ,
14+ PaginationResult ,
1315} from "../server/index.js" ;
1416import { getFunctionName } from "../server/api.js" ;
1517import { AuthTokenFetcher } from "./sync/authentication_manager.js" ;
1618import { ConnectionState } from "./sync/client.js" ;
19+ import {
20+ ExtendedTransition ,
21+ PaginatedQueryClient ,
22+ } from "./sync/paginated_query_client.js" ;
23+ import { PaginatedQueryResult } from "./sync/pagination.js" ;
24+ import { serializedQueryTokenIsPaginated } from "./sync/udf_path_utils.js" ;
1725
1826// In Node.js builds this points to a bundled WebSocket implementation. If no
1927// WebSocket implementation is manually specified or globally available,
@@ -75,6 +83,7 @@ export type Unsubscribe<T> = {
7583export class ConvexClient {
7684 private listeners : Set < QueryInfo > ;
7785 private _client : BaseConvexClient | undefined ;
86+ private _paginatedClient : PaginatedQueryClient | undefined ;
7887 // A synthetic server event to run callbacks the first time
7988 private callNewListenersWithCurrentValuesTimer :
8089 | ReturnType < typeof setTimeout >
@@ -91,6 +100,13 @@ export class ConvexClient {
91100 if ( this . _client ) return this . _client ;
92101 throw new Error ( "ConvexClient is disabled" ) ;
93102 }
103+ /**
104+ * @internal
105+ */
106+ get paginatedClient ( ) : PaginatedQueryClient {
107+ if ( this . _paginatedClient ) return this . _paginatedClient ;
108+ throw new Error ( "ConvexClient is disabled" ) ;
109+ }
94110 get disabled ( ) : boolean {
95111 return this . _disabled ;
96112 }
@@ -123,9 +139,13 @@ export class ConvexClient {
123139 if ( ! this . disabled ) {
124140 this . _client = new BaseConvexClient (
125141 address ,
126- ( updatedQueries ) => this . _transition ( updatedQueries ) ,
142+ ( ) => { } , // NOP, let the paginated query client do it all
127143 baseOptions ,
128144 ) ;
145+ this . _paginatedClient = new PaginatedQueryClient (
146+ this . _client ,
147+ ( transition ) => this . _transition ( transition ) ,
148+ ) ;
129149 }
130150 this . listeners = new Set ( ) ;
131151 }
@@ -169,18 +189,7 @@ export class ConvexClient {
169189 onError ?: ( e : Error ) => unknown ,
170190 ) : Unsubscribe < Query [ "_returnType" ] > {
171191 if ( this . disabled ) {
172- const disabledUnsubscribe = ( ( ) => { } ) as Unsubscribe <
173- Query [ "_returnType" ]
174- > ;
175- const unsubscribeProps : RemoveCallSignature <
176- Unsubscribe < Query [ "_returnType" ] >
177- > = {
178- unsubscribe : disabledUnsubscribe ,
179- getCurrentValue : ( ) => undefined ,
180- getQueryLogs : ( ) => undefined ,
181- } ;
182- Object . assign ( disabledUnsubscribe , unsubscribeProps ) ;
183- return disabledUnsubscribe ;
192+ return this . createDisabledUnsubscribe < Query [ "_returnType" ] > ( ) ;
184193 }
185194
186195 // BaseConvexClient takes care of deduplicating queries subscriptions...
@@ -198,6 +207,7 @@ export class ConvexClient {
198207 hasEverRun : false ,
199208 query,
200209 args,
210+ paginationOptions : undefined ,
201211 } ;
202212 this . listeners . add ( queryInfo ) ;
203213
@@ -235,22 +245,132 @@ export class ConvexClient {
235245 return ret ;
236246 }
237247
248+ /**
249+ * Call a callback whenever a new result for a paginated query is received.
250+ *
251+ * This is an experimental preview: the final API may change.
252+ * In particular, caching behavior, page splitting, and required paginated query options
253+ * may change.
254+ *
255+ * @param query - A {@link server.FunctionReference} for the public query to run.
256+ * @param args - The arguments to run the query with.
257+ * @param options - Options for the paginated query including initialNumItems and id.
258+ * @param callback - Function to call when the query result updates.
259+ * @param onError - Function to call when the query result updates with an error.
260+ *
261+ * @return an {@link Unsubscribe} function to stop calling the callback.
262+ */
263+ onPaginatedUpdate_experimental < Query extends FunctionReference < "query" > > (
264+ query : Query ,
265+ args : FunctionArgs < Query > ,
266+ options : { initialNumItems : number } ,
267+ callback : ( result : PaginationResult < FunctionReturnType < Query > > ) => unknown ,
268+ onError ?: ( e : Error ) => unknown ,
269+ ) : Unsubscribe < PaginatedQueryResult < FunctionReturnType < Query > [ ] > > {
270+ if ( this . disabled ) {
271+ return this . createDisabledUnsubscribe <
272+ PaginatedQueryResult < FunctionReturnType < Query > >
273+ > ( ) ;
274+ }
275+
276+ const paginationOptions = {
277+ initialNumItems : options . initialNumItems ,
278+ id : - 1 ,
279+ } ;
280+
281+ const { paginatedQueryToken, unsubscribe } = this . paginatedClient . subscribe (
282+ getFunctionName ( query ) ,
283+ args ,
284+ // Simple client doesn't use IDs, there's no expectation that these queries remain separate.
285+ paginationOptions ,
286+ ) ;
287+
288+ const queryInfo : QueryInfo = {
289+ queryToken : paginatedQueryToken ,
290+ callback,
291+ onError,
292+ unsubscribe,
293+ hasEverRun : false ,
294+ query,
295+ args,
296+ paginationOptions,
297+ } ;
298+ this . listeners . add ( queryInfo ) ;
299+
300+ // If the callback is registered for a query with a result immediately available
301+ // schedule a fake transition to call the callback soon instead of waiting for
302+ // a new server update (which could take seconds or days).
303+ if (
304+ ! ! this . paginatedClient . localQueryResultByToken ( paginatedQueryToken ) &&
305+ this . callNewListenersWithCurrentValuesTimer === undefined
306+ ) {
307+ this . callNewListenersWithCurrentValuesTimer = setTimeout (
308+ ( ) => this . callNewListenersWithCurrentValues ( ) ,
309+ 0 ,
310+ ) ;
311+ }
312+
313+ const unsubscribeProps : RemoveCallSignature <
314+ Unsubscribe < PaginatedQueryResult < FunctionReturnType < Query > [ ] > >
315+ > = {
316+ unsubscribe : ( ) => {
317+ if ( this . closed ) {
318+ // all unsubscribes already ran
319+ return ;
320+ }
321+ this . listeners . delete ( queryInfo ) ;
322+ unsubscribe ( ) ;
323+ } ,
324+ getCurrentValue : ( ) => {
325+ const result = this . paginatedClient . localQueryResult (
326+ getFunctionName ( query ) ,
327+ args ,
328+ paginationOptions ,
329+ ) ;
330+ // cast to apply the specific function type
331+ return result as
332+ | PaginatedQueryResult < FunctionReturnType < Query > >
333+ | undefined ;
334+ } ,
335+ getQueryLogs : ( ) => [ ] , // Paginated queries don't aggregate their logs
336+ } ;
337+ const ret = unsubscribeProps . unsubscribe as Unsubscribe <
338+ PaginatedQueryResult < FunctionReturnType < Query > >
339+ > ;
340+ Object . assign ( ret , unsubscribeProps ) ;
341+ return ret ;
342+ }
343+
238344 // Run all callbacks that have never been run before if they have a query
239345 // result available now.
240346 private callNewListenersWithCurrentValues ( ) {
241347 this . callNewListenersWithCurrentValuesTimer = undefined ;
242- this . _transition ( [ ] , true ) ;
348+ this . _transition ( { queries : [ ] , paginatedQueries : [ ] } , true ) ;
243349 }
244350
245351 private queryResultReady ( queryToken : QueryToken ) : boolean {
246352 return this . client . hasLocalQueryResultByToken ( queryToken ) ;
247353 }
248354
355+ private createDisabledUnsubscribe < T > ( ) : Unsubscribe < T > {
356+ const disabledUnsubscribe = ( ( ) => { } ) as Unsubscribe < T > ;
357+ const unsubscribeProps : RemoveCallSignature < Unsubscribe < T > > = {
358+ unsubscribe : disabledUnsubscribe ,
359+ getCurrentValue : ( ) => undefined ,
360+ getQueryLogs : ( ) => undefined ,
361+ } ;
362+ Object . assign ( disabledUnsubscribe , unsubscribeProps ) ;
363+ return disabledUnsubscribe ;
364+ }
365+
249366 async close ( ) {
250367 if ( this . disabled ) return ;
251368 // prevent pending updates
252369 this . listeners . clear ( ) ;
253370 this . _closed = true ;
371+ if ( this . _paginatedClient ) {
372+ this . _paginatedClient = undefined ;
373+ }
254374 return this . client . close ( ) ;
255375 }
256376
@@ -298,22 +418,43 @@ export class ConvexClient {
298418 /**
299419 * @internal
300420 */
301- _transition ( updatedQueries : QueryToken [ ] , callNewListeners = false ) {
421+ _transition (
422+ {
423+ queries,
424+ paginatedQueries,
425+ } : Pick < ExtendedTransition , "queries" | "paginatedQueries" > ,
426+ callNewListeners = false ,
427+ ) {
428+ const updatedQueries = [
429+ ...queries . map ( ( q ) => q . token ) ,
430+ ...paginatedQueries . map ( ( q ) => q . token ) ,
431+ ] ;
432+
302433 // Deduping subscriptions happens in the BaseConvexClient, so not much to do here.
303434
304435 // Call all callbacks in the order they were registered
305436 for ( const queryInfo of this . listeners ) {
306437 const { callback, queryToken, onError, hasEverRun } = queryInfo ;
438+ const isPaginatedQuery = serializedQueryTokenIsPaginated ( queryToken ) ;
439+
440+ // What does it mean to have a paginated query result ready? I think it's
441+ // always going to fire immediately.
442+ const hasResultReady = isPaginatedQuery
443+ ? ! ! this . paginatedClient . localQueryResultByToken ( queryToken )
444+ : this . client . hasLocalQueryResultByToken ( queryToken ) ;
445+
307446 if (
308447 updatedQueries . includes ( queryToken ) ||
309- ( callNewListeners &&
310- ! hasEverRun &&
311- this . client . hasLocalQueryResultByToken ( queryToken ) )
448+ ( callNewListeners && ! hasEverRun && hasResultReady )
312449 ) {
313450 queryInfo . hasEverRun = true ;
314451 let newValue ;
315452 try {
316- newValue = this . client . localQueryResultByToken ( queryToken ) ;
453+ if ( isPaginatedQuery ) {
454+ newValue = this . paginatedClient . localQueryResultByToken ( queryToken ) ;
455+ } else {
456+ newValue = this . client . localQueryResultByToken ( queryToken ) ;
457+ }
317458 } catch ( error ) {
318459 if ( ! ( error instanceof Error ) ) throw error ;
319460 if ( onError ) {
@@ -437,11 +578,13 @@ type QueryInfo = {
437578 callback : ( result : any , meta : unknown ) => unknown ;
438579 onError : ( ( e : Error , meta : unknown ) => unknown ) | undefined ;
439580 unsubscribe : ( ) => void ;
440- queryToken : QueryToken ;
581+ queryToken : QueryToken | PaginatedQueryToken ;
441582 hasEverRun : boolean ;
442- // query and args are just here for debugging, the queryToken is authoritative
583+ // query, args and paginationOptions are just here for debugging, the queryToken is authoritative
443584 query : FunctionReference < "query" > ;
444585 args : any ;
586+ paginationOptions : { initialNumItems : number ; id : number } | undefined ;
445587} ;
446588
589+ // helps to construct objects with a call signature
447590type RemoveCallSignature < T > = Omit < T , never > ;
0 commit comments