@zuzjs/flare 0.2.21 → 0.2.23

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 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
@@ -593,6 +776,10 @@ const uploaded = await storage.putObject({
593
776
  },
594
777
  });
595
778
 
779
+ console.log(uploaded.key); // users/alice.png
780
+ console.log(uploaded.access); // public (default)
781
+ console.log(uploaded.url); // https://.../storage/public/<appId>/avatars/users%2Falice.png
782
+
596
783
  // ── Base64 upload (opt-in, small files only) ─────────────────────────────────
597
784
  // Pass `base64: true` to use the legacy base64-over-JSON path.
598
785
  // If the payload exceeds `base64MaxBytes` (default 4 MiB), the SDK
@@ -602,6 +789,7 @@ const uploaded2 = await storage.putObject({
602
789
  key: 'users/thumb.png',
603
790
  body: smallFileBytes,
604
791
  contentType: 'image/png',
792
+ access: 'private', // override the default public access
605
793
  base64: true, // prefer base64 path
606
794
  base64MaxBytes: 2 * 1024 * 1024, // cap at 2 MiB; larger → raw upload
607
795
  });
@@ -612,12 +800,17 @@ const uploaded3 = await storage.putObject({
612
800
  key: 'users/icon.png',
613
801
  contentBase64: alreadyEncodedString,
614
802
  contentType: 'image/png',
803
+ // encrypt defaults to false when omitted
615
804
  });
616
805
 
617
806
  const head = await storage.headObject({ bucket: 'avatars', key: uploaded.key });
807
+ console.log(head.access, head.url);
808
+
618
809
  const file = await storage.getObject({ bucket: 'avatars', key: uploaded.key });
619
810
 
620
811
  const page1 = await storage.listObjects({ bucket: 'avatars', prefix: 'users/', limit: 100 });
812
+ console.log(page1.objects[0]?.access, page1.objects[0]?.url);
813
+
621
814
  const page2 = page1.cursor
622
815
  ? await storage.listObjects({ bucket: 'avatars', prefix: 'users/', limit: 100, cursor: page1.cursor })
623
816
  : { objects: [] };
@@ -640,6 +833,7 @@ const signedUpload = await storage.createSignedUrl({
640
833
  action: FlareStorageSignedAction.Upload,
641
834
  expiresInSeconds: 300,
642
835
  contentType: 'video/mp4',
836
+ access: 'private',
643
837
  encrypt: true,
644
838
  });
645
839
 
@@ -723,6 +917,9 @@ const embedUrl = await storage.getObjectUrl({
723
917
  ```
724
918
 
725
919
  Notes:
920
+ - `putObject()` defaults to `encrypt: false` and `access: 'public'`.
921
+ - `uploaded.url` is the stable public object URL shape. Anonymous reads still depend on stored `access` and any storage rules configured on the app.
922
+ - `headObject()` and `listObjects()` also return `access` and `url` metadata.
726
923
  - `forceDownload` and `embedOnly` are mutually exclusive.
727
924
  - `allowedOrigins` defaults to `['*']` when omitted.
728
925
  - `embedOnly` is valid only for download signed URLs.
package/dist/grpc.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as FlareConfig, aR as StructuredQuery } from './index-DlQgWDy-.cjs';
1
+ import { F as FlareConfig, aR as StructuredQuery } from './index-18tMqAtM.cjs';
2
2
  import '@zuzjs/auth';
3
3
 
4
4
  declare function runGrpcQuery<T = Record<string, unknown>>(config: FlareConfig, collection: string, query: StructuredQuery): Promise<T[] | null>;
package/dist/grpc.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as FlareConfig, aR as StructuredQuery } from './index-DlQgWDy-.js';
1
+ import { F as FlareConfig, aR as StructuredQuery } from './index-18tMqAtM.js';
2
2
  import '@zuzjs/auth';
3
3
 
4
4
  declare function runGrpcQuery<T = Record<string, unknown>>(config: FlareConfig, collection: string, query: StructuredQuery): Promise<T[] | null>;
@@ -264,6 +264,8 @@ interface FlareStorageObjectResult {
264
264
  ok: boolean;
265
265
  path: string;
266
266
  key: string;
267
+ access?: "public" | "private";
268
+ url?: string;
267
269
  encrypted?: boolean;
268
270
  size?: number;
269
271
  contentBase64?: string;
@@ -282,6 +284,7 @@ interface FlareStorageSignedUrlInput {
282
284
  expiresInSeconds?: number;
283
285
  sizeBytes?: number;
284
286
  contentType?: string;
287
+ access?: "public" | "private";
285
288
  encrypt?: boolean;
286
289
  decrypt?: boolean;
287
290
  forceDownload?: boolean;
@@ -370,6 +373,8 @@ interface StorageObjectMeta {
370
373
  bucket: string;
371
374
  size: number;
372
375
  contentType: string;
376
+ access?: "public" | "private";
377
+ url?: string;
373
378
  encrypted: boolean;
374
379
  createdAt?: unknown;
375
380
  updatedAt?: unknown;
@@ -402,7 +407,9 @@ interface PutObjectInput {
402
407
  */
403
408
  base64MaxBytes?: number;
404
409
  contentType?: string;
405
- /** Encrypt at rest with AES-256-GCM. Defaults to true. */
410
+ /** Public/private object access. Defaults to public. */
411
+ access?: "public" | "private";
412
+ /** Encrypt at rest with AES-256-GCM. Defaults to false. */
406
413
  encrypt?: boolean;
407
414
  /** Upload progress callback. Only fires in browser environments. */
408
415
  onProgress?: (progress: StorageProgress) => void;
@@ -411,6 +418,8 @@ interface PutObjectResult {
411
418
  ok: boolean;
412
419
  bucket: string;
413
420
  key: string;
421
+ access: "public" | "private";
422
+ url?: string;
414
423
  size: number;
415
424
  encrypted: boolean;
416
425
  }
@@ -503,6 +512,7 @@ interface StorageSignedUrlInput {
503
512
  expiresInSeconds?: number;
504
513
  sizeBytes?: number;
505
514
  contentType?: string;
515
+ access?: "public" | "private";
506
516
  encrypt?: boolean;
507
517
  decrypt?: boolean;
508
518
  forceDownload?: boolean;
@@ -264,6 +264,8 @@ interface FlareStorageObjectResult {
264
264
  ok: boolean;
265
265
  path: string;
266
266
  key: string;
267
+ access?: "public" | "private";
268
+ url?: string;
267
269
  encrypted?: boolean;
268
270
  size?: number;
269
271
  contentBase64?: string;
@@ -282,6 +284,7 @@ interface FlareStorageSignedUrlInput {
282
284
  expiresInSeconds?: number;
283
285
  sizeBytes?: number;
284
286
  contentType?: string;
287
+ access?: "public" | "private";
285
288
  encrypt?: boolean;
286
289
  decrypt?: boolean;
287
290
  forceDownload?: boolean;
@@ -370,6 +373,8 @@ interface StorageObjectMeta {
370
373
  bucket: string;
371
374
  size: number;
372
375
  contentType: string;
376
+ access?: "public" | "private";
377
+ url?: string;
373
378
  encrypted: boolean;
374
379
  createdAt?: unknown;
375
380
  updatedAt?: unknown;
@@ -402,7 +407,9 @@ interface PutObjectInput {
402
407
  */
403
408
  base64MaxBytes?: number;
404
409
  contentType?: string;
405
- /** Encrypt at rest with AES-256-GCM. Defaults to true. */
410
+ /** Public/private object access. Defaults to public. */
411
+ access?: "public" | "private";
412
+ /** Encrypt at rest with AES-256-GCM. Defaults to false. */
406
413
  encrypt?: boolean;
407
414
  /** Upload progress callback. Only fires in browser environments. */
408
415
  onProgress?: (progress: StorageProgress) => void;
@@ -411,6 +418,8 @@ interface PutObjectResult {
411
418
  ok: boolean;
412
419
  bucket: string;
413
420
  key: string;
421
+ access: "public" | "private";
422
+ url?: string;
414
423
  size: number;
415
424
  encrypted: boolean;
416
425
  }
@@ -503,6 +512,7 @@ interface StorageSignedUrlInput {
503
512
  expiresInSeconds?: number;
504
513
  sizeBytes?: number;
505
514
  contentType?: string;
515
+ access?: "public" | "private";
506
516
  encrypt?: boolean;
507
517
  decrypt?: boolean;
508
518
  forceDownload?: boolean;
@@ -1,4 +1,4 @@
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-DlQgWDy-.js';
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
2
  import { AuthToken } from '@zuzjs/auth';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
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-DlQgWDy-.cjs';
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
2
  import { AuthToken } from '@zuzjs/auth';
3
3
 
4
4
  /**