@zuzjs/flare 0.2.5 → 0.2.7

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,8 +158,279 @@ 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
+
353
+ ### Direct Query Helpers (Knex-Style)
354
+
355
+ `collection()` query chaining uses object-based logical steps and dedicated operator families:
356
+
357
+ - Logic: `where({...})`, `and({...})`, `or({...})`
358
+ - `in` family: `in`, `andIn`, `orIn`
359
+ - `notIn` family: `notIn`, `andNotIn`, `orNotIn`
360
+ - `arrayContains` family: `arrayContains`, `andArrayContains`, `orArrayContains`
361
+ - `arrayContainsAny` family: `arrayContainsAny`, `andArrayContainsAny`, `orArrayContainsAny`
362
+ - `some` family (array of objects): `some`, `andSome`, `orSome`
363
+ - `like` family: `like`, `andLike`, `orLike`
364
+ - `notLike` family: `notLike`, `andNotLike`, `orNotLike`
365
+ - `exists` family: `exists`, `andExists`, `orExists`
366
+ - `notExists` family: `notExists`, `andNotExists`, `orNotExists`
367
+
368
+ ```ts
369
+ const uid = 'bDEgnSqsEDT5qdDdtOX1';
370
+
371
+ const boards = await app
372
+ .collection('boards')
373
+ .where({ uid })
374
+ .orArrayContains('team', uid)
375
+ .orderBy('createdAt', 'desc')
376
+ .limit(20)
377
+ .get();
378
+
379
+ const sameBoards = await app
380
+ .collection('boards')
381
+ .where({ uid })
382
+ .orArrayContains('team', uid)
383
+ .get();
384
+
385
+ const active = await app
386
+ .collection('tasks')
387
+ .in('status', ['todo', 'doing'])
388
+ .andArrayContainsAny('labels', ['urgent', 'backend'])
389
+ .andLike('title', '%bug%')
390
+ .get();
391
+
392
+ const boardAccess = await app
393
+ .collection('boards')
394
+ .some('team', { uid: 'xyz', role: 1 })
395
+ .get();
396
+ ```
397
+
161
398
  ### Template-Based Email APIs
162
399
 
400
+ ### Security Rules Example (Boards Owner Or Team Member)
401
+
402
+ For a `boards` document shape like:
403
+
404
+ ```json
405
+ {
406
+ "uid": "ownerUid",
407
+ "team": ["memberUid1", "memberUid2"]
408
+ }
409
+ ```
410
+
411
+ Use this DSL to allow read for owner or team member, and allow write only for owner:
412
+
413
+ ```txt
414
+ service cloud.firestore {
415
+ match /databases/{database}/documents {
416
+ function isOwner(ownerUid) {
417
+ return auth != null && auth.uid == ownerUid;
418
+ }
419
+
420
+ function isTeamMember(teamUids) {
421
+ return auth != null && auth.uid in teamUids;
422
+ }
423
+
424
+ match /boards/{boardId} {
425
+ allow read: if isOwner(resourceData.uid) || isTeamMember(resourceData.team);
426
+ allow create, update, delete: if isOwner(resourceData.uid);
427
+ }
428
+ }
429
+ }
430
+ ```
431
+
432
+ Tip: if you set owner uid at create time, prefer checking `requestData.uid` on create and `resourceData.uid` on update/delete.
433
+
163
434
  Emails are sent only through app-level templates stored in `_flare_email_templates`.
164
435
 
165
436
  Template placeholders use `{key}` syntax and are replaced from `values`.