@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 +271 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +108 -8
- package/dist/index.d.ts +108 -8
- package/dist/index.js +2 -2
- package/package.json +1 -1
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`.
|