@zuzjs/flare 0.2.6 → 0.2.8

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
@@ -158,6 +158,198 @@ await app.sendPushNotification({
158
158
  await app.unregisterPushToken(token);
159
159
  ```
160
160
 
161
+ ### Burst Stream API (Chat, Group Chat, Activity Feeds)
162
+
163
+ Use `stream()` to keep a live in-memory list with batched updates.
164
+ This avoids one render per incoming change during message bursts.
165
+
166
+ ```ts
167
+ const messageStream = app
168
+ .collection('messages')
169
+ .where({ roomId: 'room-1' })
170
+ .latest()
171
+ .limit(200)
172
+ .stream({
173
+ flushMs: 24, // collapse burst updates into short windows
174
+ maxBatchSize: 200, // force flush when queue gets large
175
+ insertAt: 'start', // keep latest-first lists stable for chat UIs
176
+ maxDocs: 200,
177
+ });
178
+
179
+ const stop = messageStream.subscribe((rows, meta) => {
180
+ console.log('rows', rows.length, 'ready', meta.ready, 'reason', meta.reason);
181
+ });
182
+
183
+ // Read current snapshot any time (works well with external-store patterns)
184
+ const currentRows = messageStream.getSnapshot();
185
+
186
+ // Optional subscription-level error hooks
187
+ messageStream
188
+ .onError((err) => console.error('stream error', err))
189
+ .onPermissionDenied((err) => console.error('permission denied', err));
190
+
191
+ // Cleanup
192
+ stop();
193
+ messageStream.close();
194
+ ```
195
+
196
+ #### React.js Example
197
+
198
+ ```tsx
199
+ import { useEffect, useMemo, useState } from 'react';
200
+ import { connectApp } from '@zuzjs/flare';
201
+
202
+ type Message = {
203
+ id: string;
204
+ roomId: string;
205
+ text: string;
206
+ createdAt: number;
207
+ };
208
+
209
+ const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });
210
+
211
+ export function RoomMessages({ roomId }: { roomId: string }) {
212
+ const [rows, setRows] = useState<readonly Message[]>([]);
213
+ const [ready, setReady] = useState(false);
214
+
215
+ const stream = useMemo(() => {
216
+ return app
217
+ .collection<Message>('messages')
218
+ .where({ roomId })
219
+ .latest()
220
+ .limit(200)
221
+ .stream({ flushMs: 20, maxBatchSize: 250, insertAt: 'start', maxDocs: 200 });
222
+ }, [roomId]);
223
+
224
+ useEffect(() => {
225
+ const stop = stream.subscribe((nextRows, meta) => {
226
+ setRows(nextRows);
227
+ setReady(meta.ready);
228
+ });
229
+
230
+ return () => {
231
+ stop();
232
+ stream.close();
233
+ };
234
+ }, [stream]);
235
+
236
+ if (!ready) return <p>Loading messages...</p>;
237
+
238
+ return (
239
+ <ul>
240
+ {rows.map((m) => (
241
+ <li key={m.id}>{m.text}</li>
242
+ ))}
243
+ </ul>
244
+ );
245
+ }
246
+ ```
247
+
248
+ #### Next.js Example (Client Component)
249
+
250
+ ```tsx
251
+ 'use client';
252
+
253
+ import { useSyncExternalStore } from 'react';
254
+ import { connectApp } from '@zuzjs/flare';
255
+
256
+ type Message = { id: string; roomId: string; text: string; createdAt: number };
257
+
258
+ const app = connectApp({
259
+ endpoint: process.env.NEXT_PUBLIC_FLARE_ENDPOINT!,
260
+ appId: process.env.NEXT_PUBLIC_FLARE_APP_ID!,
261
+ apiKey: process.env.NEXT_PUBLIC_FLARE_API_KEY,
262
+ });
263
+
264
+ export default function RoomStream({ roomId }: { roomId: string }) {
265
+ const store = app
266
+ .collection<Message>('messages')
267
+ .where({ roomId })
268
+ .latest()
269
+ .limit(200)
270
+ .asStore({ flushMs: 20, maxBatchSize: 250, insertAt: 'start', maxDocs: 200 });
271
+
272
+ const rows = useSyncExternalStore(
273
+ store.subscribe,
274
+ store.getSnapshot,
275
+ store.getServerSnapshot,
276
+ );
277
+
278
+ return (
279
+ <section>
280
+ {rows.map((m) => (
281
+ <p key={m.id}>{m.text}</p>
282
+ ))}
283
+ </section>
284
+ );
285
+ }
286
+ ```
287
+
288
+ #### Redux Example
289
+
290
+ ```ts
291
+ import { createSlice, PayloadAction, configureStore } from '@reduxjs/toolkit';
292
+ import { connectApp } from '@zuzjs/flare';
293
+
294
+ type Message = { id: string; roomId: string; text: string; createdAt: number };
295
+
296
+ const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });
297
+
298
+ const messagesSlice = createSlice({
299
+ name: 'messages',
300
+ initialState: [] as Message[],
301
+ reducers: {
302
+ replaceMessages: (_state, action: PayloadAction<readonly Message[]>) => [...action.payload],
303
+ },
304
+ });
305
+
306
+ export const { replaceMessages } = messagesSlice.actions;
307
+ export const store = configureStore({ reducer: { messages: messagesSlice.reducer } });
308
+
309
+ export function startRoomMessageStream(roomId: string): () => void {
310
+ const stream = app
311
+ .collection<Message>('messages')
312
+ .where({ roomId })
313
+ .latest()
314
+ .limit(200)
315
+ .stream({ flushMs: 20, maxBatchSize: 250, insertAt: 'start', maxDocs: 200 });
316
+
317
+ const stop = stream.subscribe((rows) => {
318
+ store.dispatch(replaceMessages(rows));
319
+ });
320
+
321
+ return () => {
322
+ stop();
323
+ stream.close();
324
+ };
325
+ }
326
+ ```
327
+
328
+ ### External Store Bridge (No Framework Dependency)
329
+
330
+ The client also exposes `asStore()` so app code can plug into external-store hooks
331
+ while keeping this package free of framework dependencies.
332
+
333
+ ```ts
334
+ const messageStore = app
335
+ .collection('messages')
336
+ .where({ roomId: 'room-1' })
337
+ .latest()
338
+ .limit(200)
339
+ .asStore({ flushMs: 24, maxBatchSize: 200, insertAt: 'start' });
340
+
341
+ // In app code, pass these to your UI store hook:
342
+ messageStore.subscribe; // (onStoreChange) => unsubscribe
343
+ messageStore.getSnapshot; // () => readonly rows
344
+ messageStore.getServerSnapshot; // () => []
345
+
346
+ // Optional advanced access
347
+ messageStore.stream.onError((err) => console.error(err));
348
+
349
+ // Cleanup
350
+ messageStore.destroy();
351
+ ```
352
+
161
353
  ### Direct Query Helpers (Knex-Style)
162
354
 
163
355
  `collection()` query chaining uses object-based logical steps and dedicated operator families:
@@ -203,6 +395,73 @@ const boardAccess = await app
203
395
  .get();
204
396
  ```
205
397
 
398
+ ### Advanced Joins And Relations
399
+
400
+ Use `join()` when you want direct field mapping, including array/object-path sources.
401
+
402
+ ```ts
403
+ const boardWithLists = await app
404
+ .collection('boards')
405
+ .where({ id: boardId, uid: me.uid })
406
+ .join('lists', {
407
+ source: 'id',
408
+ target: 'boardId',
409
+ as: 'lists',
410
+ })
411
+ .limit(1)
412
+ .get();
413
+
414
+ const boardWithTeamMembers = await app
415
+ .collection('boards')
416
+ .where({ id: boardId, uid: me.uid })
417
+ .join('users', {
418
+ source: 'team.uid', // team: [{ uid, role }]
419
+ target: 'id',
420
+ as: 'teamMembers',
421
+ })
422
+ .limit(1)
423
+ .get();
424
+ ```
425
+
426
+ Use `withRelation()` for SQL-style shorthand.
427
+
428
+ ```ts
429
+ const boardWithTeamMembers = await app
430
+ .collection('boards')
431
+ .where({ id: boardId, uid: me.uid })
432
+ .withRelation('team.uid->users.id', { as: 'teamMembers' })
433
+ .limit(1)
434
+ .get();
435
+
436
+ const boardWithInlineAlias = await app
437
+ .collection('boards')
438
+ .where({ id: boardId, uid: me.uid })
439
+ .withRelation('team.uid->users.id as teamMembers')
440
+ .limit(1)
441
+ .get();
442
+
443
+ const boardWithMultipleJoins = await app
444
+ .collection('boards')
445
+ .where({ id: boardId, uid: me.uid })
446
+ .join('lists', { source: 'id', target: 'boardId', as: 'lists' })
447
+ .join('users', { source: 'team.uid', target: 'id', as: 'teamMembers' })
448
+ .limit(1)
449
+ .get();
450
+
451
+ const boardWithNestedJoinChain = await app
452
+ .collection('boards')
453
+ .join('lists', { source: 'id', target: 'boardId', as: 'lists' })
454
+ .joinNested('lists', 'cards', { source: 'id', target: 'listId', as: 'cards' })
455
+ .joinNested('cards', 'comments', { source: 'id', target: 'cardId', as: 'comments' })
456
+ .get();
457
+ ```
458
+
459
+ Nested join options are supported per relation (`where`, `orderBy`, `limit`, `offset`, `select`, and nested `joins`).
460
+
461
+ Join alias note:
462
+ - `.join('users', ...)` and `.join('_users', ...)` both resolve to the app auth-users collection (`_flare_auth_users`) on server.
463
+ - Use `_users` when you want an explicit auth-collection relation in query code.
464
+
206
465
  ### Template-Based Email APIs
207
466
 
208
467
  ### Security Rules Example (Boards Owner Or Team Member)