@zuzjs/flare 0.2.22 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -0
- package/dist/{index-CRByt4Qw.d.ts → index-CXCOH5iN.d.ts} +36 -1
- package/dist/{index-DOaU2Hxo.d.cts → index-D53u7tRA.d.cts} +36 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -238,6 +238,189 @@ stop();
|
|
|
238
238
|
messageStream.close();
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
+
### Pagination Patterns (Stream vs Manual)
|
|
242
|
+
|
|
243
|
+
Use one of these based on UX needs:
|
|
244
|
+
|
|
245
|
+
- Stream page window: realtime for the current page; rebuild stream when page changes.
|
|
246
|
+
- Manual cursor paging: deterministic `load more` and stable history.
|
|
247
|
+
- Hybrid: stream first page (latest data) and fetch older pages manually.
|
|
248
|
+
|
|
249
|
+
#### 1) Stream Page Window (offset-based)
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
253
|
+
import { collection, Collections, useLiveQuery } from '@zuzjs/flare';
|
|
254
|
+
|
|
255
|
+
const PAGE_SIZE = 25;
|
|
256
|
+
|
|
257
|
+
export function ContactsPagedStream() {
|
|
258
|
+
const [rows, setRows] = useState<any[]>([]);
|
|
259
|
+
const [loading, setLoading] = useState(true);
|
|
260
|
+
const [page, setPage] = useState(0);
|
|
261
|
+
|
|
262
|
+
const contacts = useLiveQuery({
|
|
263
|
+
onData: (data, meta) => {
|
|
264
|
+
setLoading(!meta.ready);
|
|
265
|
+
setRows(data as any[]);
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const query = useMemo(() => {
|
|
270
|
+
return collection(Collections.Contacts)
|
|
271
|
+
.where({ sheet: '!= null' })
|
|
272
|
+
.orderBy('_seq', 'desc')
|
|
273
|
+
.limit(PAGE_SIZE)
|
|
274
|
+
.offset(page * PAGE_SIZE);
|
|
275
|
+
}, [page]);
|
|
276
|
+
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
contacts.buildStream(query);
|
|
279
|
+
return () => contacts.closeStream();
|
|
280
|
+
}, [contacts, query]);
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div>
|
|
284
|
+
<div>{loading ? 'Loading...' : `Rows: ${rows.length}`}</div>
|
|
285
|
+
<button onClick={() => setPage((p) => Math.max(0, p - 1))}>Prev</button>
|
|
286
|
+
<button onClick={() => setPage((p) => p + 1)}>Next</button>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Notes:
|
|
293
|
+
|
|
294
|
+
- This keeps only one live page at a time.
|
|
295
|
+
- Realtime inserts can shift offset pages; this is expected for live data.
|
|
296
|
+
|
|
297
|
+
#### 2) Manual Cursor Pagination (recommended for stable "load more")
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
import { useEffect, useState } from 'react';
|
|
301
|
+
import { collection, Collections } from '@zuzjs/flare';
|
|
302
|
+
|
|
303
|
+
const PAGE_SIZE = 25;
|
|
304
|
+
|
|
305
|
+
export function ContactsManualPagination() {
|
|
306
|
+
const [rows, setRows] = useState<any[]>([]);
|
|
307
|
+
const [loading, setLoading] = useState(false);
|
|
308
|
+
const [cursor, setCursor] = useState<number | null>(null);
|
|
309
|
+
const [hasMore, setHasMore] = useState(true);
|
|
310
|
+
|
|
311
|
+
const loadInitial = async () => {
|
|
312
|
+
setLoading(true);
|
|
313
|
+
const page = await collection(Collections.Contacts)
|
|
314
|
+
.where({ sheet: '!= null' })
|
|
315
|
+
.orderBy('_seq', 'desc')
|
|
316
|
+
.limit(PAGE_SIZE)
|
|
317
|
+
.get();
|
|
318
|
+
|
|
319
|
+
setRows(page as any[]);
|
|
320
|
+
const last = (page as any[])[(page as any[]).length - 1];
|
|
321
|
+
setCursor(last?._seq ?? null);
|
|
322
|
+
setHasMore((page as any[]).length === PAGE_SIZE);
|
|
323
|
+
setLoading(false);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const loadMore = async () => {
|
|
327
|
+
if (!hasMore || cursor == null) return;
|
|
328
|
+
setLoading(true);
|
|
329
|
+
|
|
330
|
+
const page = await collection(Collections.Contacts)
|
|
331
|
+
.where({ sheet: '!= null' })
|
|
332
|
+
.orderBy('_seq', 'desc')
|
|
333
|
+
.startAfter(cursor)
|
|
334
|
+
.limit(PAGE_SIZE)
|
|
335
|
+
.get();
|
|
336
|
+
|
|
337
|
+
const next = page as any[];
|
|
338
|
+
setRows((prev) => [...prev, ...next]);
|
|
339
|
+
const last = next[next.length - 1];
|
|
340
|
+
setCursor(last?._seq ?? null);
|
|
341
|
+
setHasMore(next.length === PAGE_SIZE);
|
|
342
|
+
setLoading(false);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
void loadInitial();
|
|
347
|
+
}, []);
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<div>
|
|
351
|
+
<div>{loading ? 'Loading...' : `Rows: ${rows.length}`}</div>
|
|
352
|
+
<button onClick={loadMore} disabled={!hasMore || loading}>
|
|
353
|
+
{hasMore ? 'Load more' : 'No more'}
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### 3) Hybrid (stream latest page + manual history)
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
364
|
+
import { collection, Collections, useLiveQuery } from '@zuzjs/flare';
|
|
365
|
+
|
|
366
|
+
const PAGE_SIZE = 25;
|
|
367
|
+
|
|
368
|
+
export function ContactsHybridPagination() {
|
|
369
|
+
const [liveRows, setLiveRows] = useState<any[]>([]);
|
|
370
|
+
const [historyRows, setHistoryRows] = useState<any[]>([]);
|
|
371
|
+
const [historyCursor, setHistoryCursor] = useState<number | null>(null);
|
|
372
|
+
const [ready, setReady] = useState(false);
|
|
373
|
+
|
|
374
|
+
const live = useLiveQuery({
|
|
375
|
+
onData: (data, meta) => {
|
|
376
|
+
setReady(meta.ready);
|
|
377
|
+
setLiveRows(data as any[]);
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const liveQuery = useMemo(() => {
|
|
382
|
+
return collection(Collections.Contacts)
|
|
383
|
+
.where({ sheet: '!= null' })
|
|
384
|
+
.orderBy('_seq', 'desc')
|
|
385
|
+
.limit(PAGE_SIZE);
|
|
386
|
+
}, []);
|
|
387
|
+
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
live.buildStream(liveQuery);
|
|
390
|
+
return () => live.closeStream();
|
|
391
|
+
}, [live, liveQuery]);
|
|
392
|
+
|
|
393
|
+
const loadOlder = async () => {
|
|
394
|
+
const anchor = historyCursor ?? liveRows[liveRows.length - 1]?._seq;
|
|
395
|
+
if (anchor == null) return;
|
|
396
|
+
|
|
397
|
+
const page = await collection(Collections.Contacts)
|
|
398
|
+
.where({ sheet: '!= null' })
|
|
399
|
+
.orderBy('_seq', 'desc')
|
|
400
|
+
.startAfter(anchor)
|
|
401
|
+
.limit(PAGE_SIZE)
|
|
402
|
+
.get();
|
|
403
|
+
|
|
404
|
+
const next = page as any[];
|
|
405
|
+
setHistoryRows((prev) => [...prev, ...next]);
|
|
406
|
+
setHistoryCursor(next[next.length - 1]?._seq ?? anchor);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const allRows = [...liveRows, ...historyRows];
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<div>
|
|
413
|
+
<div>{ready ? `Rows: ${allRows.length}` : 'Loading live page...'}</div>
|
|
414
|
+
<button onClick={loadOlder}>Load older</button>
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Tip:
|
|
421
|
+
|
|
422
|
+
- For feeds/chat, hybrid mode usually gives the best UX: live top of list + stable older history.
|
|
423
|
+
|
|
241
424
|
#### React.js Example
|
|
242
425
|
|
|
243
426
|
```tsx
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { b0 as WhereCondition, aT as SubscriptionCallback, aw as QueryConfig, J as DocUpdatedCallback, I as DocDeletedCallback, H as DocChangedCallback, ay as QueryPresetMap, az as QueryPresetParams, aA as QueryPresetRow, a as AggregateSpec, ae as HavingClause, ah as JoinClause, a_ as VectorSearchClause, aR as StructuredQuery, aX as SubscriptionHandle, u as CollectionStreamOptions, r as CollectionStream, q as CollectionExternalStore, G as DocAddedCallback, m as BulkWriteOptions, o as BulkWriteResult, aY as UpdateManyItem, F as FlareConfig, aS as SubscribeOptions, aW as SubscriptionErrorCallback, aV as SubscriptionError, v as ConnectionState, ap as PresenceCallback, aq as PresenceJoinCallback, ar as PresenceLeaveCallback, aZ as VectorFieldConfig, aB as QueryPresetSpec, a8 as FlareStorageTransferManagerConfig, aN as StorageProgress, aL as StorageBucketInput, aK as StorageBucket, k as BucketPolicyInput, a1 as FlareStorageRulesPolicy, j as BucketCorsRule, a0 as FlareStorageRulesHistoryResult, au as PutObjectInput, av as PutObjectResult, aa as GetObjectInput, ab as GetObjectResult, ac as GetObjectUrlInput, L as DownloadObjectInput, M as DownloadObjectResult, af as HeadObjectInput, aM as StorageObjectMeta, ag as HeadObjectsInput, aj as ListObjectsInput, ak as ListObjectsResult, w as CopyObjectInput, z as DeleteObjectInput, E as DeleteObjectsInput, aO as StorageSignedUrlInput, a7 as FlareStorageSignedUrlResult, P as FlareAuthConfig, f as AuthStateListener, d as AuthConfigListener, U as FlareAuthSession, V as FlareAuthUser, Q as FlareAuthHydrationInput, R as FlareAuthHydrationOptions, i as BrowserPushTokenOptions, B as BrowserPushRegistrationOptions, aD as RegisterPushTokenInput, aI as SendPushNotificationInput, at as PushSendResult, e as AuthResult } from './index-18tMqAtM.js';
|
|
2
|
-
import { AuthToken } from '@zuzjs/auth';
|
|
2
|
+
import { AuthGuard, AuthToken, ProviderId } from '@zuzjs/auth';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Parse ORM-style where condition: { age: ">= 25", role: "admin" }
|
|
@@ -729,6 +729,7 @@ declare class FlareAuth<TPresetMap extends QueryPresetMap = {}> extends FlareBas
|
|
|
729
729
|
protected authBootstrapAttempted: boolean;
|
|
730
730
|
protected authBootstrapPromise?: Promise<void>;
|
|
731
731
|
protected socketAuthSyncPromise?: Promise<void>;
|
|
732
|
+
protected authGuard?: AuthGuard;
|
|
732
733
|
/** In-memory CSRF token extracted from the `x-flare-csrf` response header */
|
|
733
734
|
protected csrfToken?: string;
|
|
734
735
|
protected csrfBootstrapAttempted: boolean;
|
|
@@ -1064,6 +1065,40 @@ declare class FlareAuth<TPresetMap extends QueryPresetMap = {}> extends FlareBas
|
|
|
1064
1065
|
email: string;
|
|
1065
1066
|
sessionsRevoked?: number;
|
|
1066
1067
|
}>;
|
|
1068
|
+
signIn(providerId: ProviderId, options?: {
|
|
1069
|
+
returnTo?: string;
|
|
1070
|
+
metaTag?: string;
|
|
1071
|
+
}): Promise<any>;
|
|
1072
|
+
signIn(authGuard: Pick<AuthGuard, 'signIn'>, providerId: ProviderId, options?: {
|
|
1073
|
+
returnTo?: string;
|
|
1074
|
+
metaTag?: string;
|
|
1075
|
+
}): Promise<any>;
|
|
1076
|
+
signInWithGoogle(options?: {
|
|
1077
|
+
returnTo?: string;
|
|
1078
|
+
metaTag?: string;
|
|
1079
|
+
}): Promise<any>;
|
|
1080
|
+
signInWithGitHub(options?: {
|
|
1081
|
+
returnTo?: string;
|
|
1082
|
+
metaTag?: string;
|
|
1083
|
+
}): Promise<any>;
|
|
1084
|
+
signInWithFacebook(options?: {
|
|
1085
|
+
returnTo?: string;
|
|
1086
|
+
metaTag?: string;
|
|
1087
|
+
}): Promise<any>;
|
|
1088
|
+
signInWithDropbox(options?: {
|
|
1089
|
+
returnTo?: string;
|
|
1090
|
+
metaTag?: string;
|
|
1091
|
+
}): Promise<any>;
|
|
1092
|
+
handleSignInRedirect(autoRedirect?: boolean): Promise<(AuthResult & {
|
|
1093
|
+
authToken: AuthToken;
|
|
1094
|
+
provider?: ProviderId;
|
|
1095
|
+
}) | null>;
|
|
1096
|
+
handleSignInRedirect(authGuard: Pick<AuthGuard, 'handleRedirect'>, autoRedirect?: boolean): Promise<(AuthResult & {
|
|
1097
|
+
authToken: AuthToken;
|
|
1098
|
+
provider?: ProviderId;
|
|
1099
|
+
}) | null>;
|
|
1100
|
+
private exchangeProviderToken;
|
|
1101
|
+
protected getAuthGuard(): Promise<AuthGuard>;
|
|
1067
1102
|
signOut(): Promise<void>;
|
|
1068
1103
|
protected registerWithEmail(email: string, password: string, options?: {
|
|
1069
1104
|
scope?: string[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { b0 as WhereCondition, aT as SubscriptionCallback, aw as QueryConfig, J as DocUpdatedCallback, I as DocDeletedCallback, H as DocChangedCallback, ay as QueryPresetMap, az as QueryPresetParams, aA as QueryPresetRow, a as AggregateSpec, ae as HavingClause, ah as JoinClause, a_ as VectorSearchClause, aR as StructuredQuery, aX as SubscriptionHandle, u as CollectionStreamOptions, r as CollectionStream, q as CollectionExternalStore, G as DocAddedCallback, m as BulkWriteOptions, o as BulkWriteResult, aY as UpdateManyItem, F as FlareConfig, aS as SubscribeOptions, aW as SubscriptionErrorCallback, aV as SubscriptionError, v as ConnectionState, ap as PresenceCallback, aq as PresenceJoinCallback, ar as PresenceLeaveCallback, aZ as VectorFieldConfig, aB as QueryPresetSpec, a8 as FlareStorageTransferManagerConfig, aN as StorageProgress, aL as StorageBucketInput, aK as StorageBucket, k as BucketPolicyInput, a1 as FlareStorageRulesPolicy, j as BucketCorsRule, a0 as FlareStorageRulesHistoryResult, au as PutObjectInput, av as PutObjectResult, aa as GetObjectInput, ab as GetObjectResult, ac as GetObjectUrlInput, L as DownloadObjectInput, M as DownloadObjectResult, af as HeadObjectInput, aM as StorageObjectMeta, ag as HeadObjectsInput, aj as ListObjectsInput, ak as ListObjectsResult, w as CopyObjectInput, z as DeleteObjectInput, E as DeleteObjectsInput, aO as StorageSignedUrlInput, a7 as FlareStorageSignedUrlResult, P as FlareAuthConfig, f as AuthStateListener, d as AuthConfigListener, U as FlareAuthSession, V as FlareAuthUser, Q as FlareAuthHydrationInput, R as FlareAuthHydrationOptions, i as BrowserPushTokenOptions, B as BrowserPushRegistrationOptions, aD as RegisterPushTokenInput, aI as SendPushNotificationInput, at as PushSendResult, e as AuthResult } from './index-18tMqAtM.cjs';
|
|
2
|
-
import { AuthToken } from '@zuzjs/auth';
|
|
2
|
+
import { AuthGuard, AuthToken, ProviderId } from '@zuzjs/auth';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Parse ORM-style where condition: { age: ">= 25", role: "admin" }
|
|
@@ -729,6 +729,7 @@ declare class FlareAuth<TPresetMap extends QueryPresetMap = {}> extends FlareBas
|
|
|
729
729
|
protected authBootstrapAttempted: boolean;
|
|
730
730
|
protected authBootstrapPromise?: Promise<void>;
|
|
731
731
|
protected socketAuthSyncPromise?: Promise<void>;
|
|
732
|
+
protected authGuard?: AuthGuard;
|
|
732
733
|
/** In-memory CSRF token extracted from the `x-flare-csrf` response header */
|
|
733
734
|
protected csrfToken?: string;
|
|
734
735
|
protected csrfBootstrapAttempted: boolean;
|
|
@@ -1064,6 +1065,40 @@ declare class FlareAuth<TPresetMap extends QueryPresetMap = {}> extends FlareBas
|
|
|
1064
1065
|
email: string;
|
|
1065
1066
|
sessionsRevoked?: number;
|
|
1066
1067
|
}>;
|
|
1068
|
+
signIn(providerId: ProviderId, options?: {
|
|
1069
|
+
returnTo?: string;
|
|
1070
|
+
metaTag?: string;
|
|
1071
|
+
}): Promise<any>;
|
|
1072
|
+
signIn(authGuard: Pick<AuthGuard, 'signIn'>, providerId: ProviderId, options?: {
|
|
1073
|
+
returnTo?: string;
|
|
1074
|
+
metaTag?: string;
|
|
1075
|
+
}): Promise<any>;
|
|
1076
|
+
signInWithGoogle(options?: {
|
|
1077
|
+
returnTo?: string;
|
|
1078
|
+
metaTag?: string;
|
|
1079
|
+
}): Promise<any>;
|
|
1080
|
+
signInWithGitHub(options?: {
|
|
1081
|
+
returnTo?: string;
|
|
1082
|
+
metaTag?: string;
|
|
1083
|
+
}): Promise<any>;
|
|
1084
|
+
signInWithFacebook(options?: {
|
|
1085
|
+
returnTo?: string;
|
|
1086
|
+
metaTag?: string;
|
|
1087
|
+
}): Promise<any>;
|
|
1088
|
+
signInWithDropbox(options?: {
|
|
1089
|
+
returnTo?: string;
|
|
1090
|
+
metaTag?: string;
|
|
1091
|
+
}): Promise<any>;
|
|
1092
|
+
handleSignInRedirect(autoRedirect?: boolean): Promise<(AuthResult & {
|
|
1093
|
+
authToken: AuthToken;
|
|
1094
|
+
provider?: ProviderId;
|
|
1095
|
+
}) | null>;
|
|
1096
|
+
handleSignInRedirect(authGuard: Pick<AuthGuard, 'handleRedirect'>, autoRedirect?: boolean): Promise<(AuthResult & {
|
|
1097
|
+
authToken: AuthToken;
|
|
1098
|
+
provider?: ProviderId;
|
|
1099
|
+
}) | null>;
|
|
1100
|
+
private exchangeProviderToken;
|
|
1101
|
+
protected getAuthGuard(): Promise<AuthGuard>;
|
|
1067
1102
|
signOut(): Promise<void>;
|
|
1068
1103
|
protected registerWithEmail(email: string, password: string, options?: {
|
|
1069
1104
|
scope?: string[];
|