@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 +259 -0
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +96 -24
- package/dist/index.d.ts +96 -24
- package/dist/index.js +2 -2
- package/package.json +1 -1
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)
|