@foir/sdk 0.9.0 → 0.11.0

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +719 -27
  2. package/README.md +121 -2
  3. package/dist/auth/auth.d.ts +72 -1
  4. package/dist/auth/hosted.d.ts +71 -4
  5. package/dist/auth/index.d.ts +3 -3
  6. package/dist/auth/operations.d.ts +84 -0
  7. package/dist/auth/ops.d.ts +18 -0
  8. package/dist/auth.js +209 -47
  9. package/dist/client.js +101 -13
  10. package/dist/codegen/block-registry.d.ts +1 -1
  11. package/dist/codegen/block-scaffold.d.ts +1 -1
  12. package/dist/codegen/index.d.ts +1 -1
  13. package/dist/codegen/item-registry.d.ts +1 -1
  14. package/dist/codegen/item-scaffold.d.ts +1 -1
  15. package/dist/codegen/preset.d.ts +3 -2
  16. package/dist/content/content-renderer.d.ts +4 -5
  17. package/dist/content/index.d.ts +1 -1
  18. package/dist/content/richtext.d.ts +1 -1
  19. package/dist/data/builtins.d.ts +81 -13
  20. package/dist/data/client.d.ts +92 -9
  21. package/dist/data/decimal.d.ts +212 -0
  22. package/dist/data/document.d.ts +14 -1
  23. package/dist/data/index.d.ts +22 -10
  24. package/dist/data/manifest.d.ts +40 -4
  25. package/dist/data/schema.d.ts +18 -11
  26. package/dist/data/selection.d.ts +15 -0
  27. package/dist/data/subscriptions.d.ts +36 -10
  28. package/dist/data/transport.d.ts +363 -24
  29. package/dist/data/ws-transport.d.ts +8 -2
  30. package/dist/data.js +591 -64
  31. package/dist/graphql/cdn-route.d.ts +1 -1
  32. package/dist/graphql/foir-request.d.ts +1 -1
  33. package/dist/index.js +0 -9
  34. package/dist/live/coalesce.d.ts +57 -0
  35. package/dist/live/drift.d.ts +58 -0
  36. package/dist/live/events.d.ts +13 -0
  37. package/dist/live/headers.d.ts +53 -0
  38. package/dist/live/index.d.ts +54 -0
  39. package/dist/live/keys.d.ts +72 -0
  40. package/dist/live/manager.d.ts +77 -0
  41. package/dist/live/react/index.d.ts +143 -0
  42. package/dist/live/reconcile.d.ts +82 -0
  43. package/dist/live/resync.d.ts +59 -0
  44. package/dist/live/store.d.ts +134 -0
  45. package/dist/live/tagIndex.d.ts +29 -0
  46. package/dist/live/tanstack.d.ts +127 -0
  47. package/dist/live/writetoken.d.ts +86 -0
  48. package/dist/live-react.js +395 -0
  49. package/dist/live.js +845 -0
  50. package/dist/next/cacheTags.d.ts +245 -0
  51. package/dist/next/index.d.ts +7 -0
  52. package/dist/next.js +260 -34
  53. package/dist/push.js +163 -36
  54. package/dist/react.js +157 -33
  55. package/dist/realtime/index.d.ts +4 -5
  56. package/dist/server/events-client.d.ts +54 -4
  57. package/dist/server/index.d.ts +4 -2
  58. package/dist/server/operation-types.d.ts +86 -58
  59. package/dist/server/read-dispatch.d.ts +53 -0
  60. package/dist/server/require-scoped-token.d.ts +20 -1
  61. package/dist/server.js +602 -10
  62. package/dist/transport/foir-http.d.ts +168 -15
  63. package/package.json +10 -2
@@ -0,0 +1,395 @@
1
+ "use client";
2
+ "use client";
3
+
4
+ // src/live/react/index.tsx
5
+ import {
6
+ createContext,
7
+ createElement,
8
+ useCallback,
9
+ useContext,
10
+ useEffect,
11
+ useMemo,
12
+ useRef,
13
+ useSyncExternalStore
14
+ } from "react";
15
+
16
+ // src/live/resync.ts
17
+ var FOIR_RESYNC_ERROR_CODES = ["cursor_expired", "backlog_too_large"];
18
+ function foirRequiresResync(error) {
19
+ const code = error.code;
20
+ return typeof code === "string" && FOIR_RESYNC_ERROR_CODES.includes(code);
21
+ }
22
+
23
+ // src/live/manager.ts
24
+ function createFoirLiveManager(options) {
25
+ const watches = /* @__PURE__ */ new Map();
26
+ function open(modelKey, watch) {
27
+ const after = watch.cursor ?? void 0;
28
+ watch.subscription = options.subscribe(
29
+ modelKey,
30
+ after === void 0 ? {} : { after },
31
+ {
32
+ onData: (event) => {
33
+ if (typeof event.seq === "number" && (watch.cursor === null || event.seq > watch.cursor)) {
34
+ watch.cursor = event.seq;
35
+ }
36
+ options.onChange(event);
37
+ },
38
+ onError: (error) => {
39
+ if (foirRequiresResync(error)) {
40
+ watch.cursor = null;
41
+ options.onResync?.(error, modelKey);
42
+ return;
43
+ }
44
+ options.onError?.(error, modelKey);
45
+ },
46
+ onReconnect: () => {
47
+ resume(modelKey);
48
+ }
49
+ }
50
+ );
51
+ }
52
+ function resume(modelKey) {
53
+ const watch = watches.get(modelKey);
54
+ if (!watch || watch.count === 0 || watch.resuming) return;
55
+ watch.resuming = true;
56
+ try {
57
+ watch.subscription?.unsubscribe();
58
+ watch.subscription = null;
59
+ open(modelKey, watch);
60
+ } finally {
61
+ watch.resuming = false;
62
+ }
63
+ options.reconcile?.(modelKey);
64
+ }
65
+ return {
66
+ retain(modelKey) {
67
+ let watch = watches.get(modelKey);
68
+ if (!watch) {
69
+ watch = { count: 0, subscription: null, cursor: null, resuming: false };
70
+ watches.set(modelKey, watch);
71
+ }
72
+ watch.count += 1;
73
+ if (watch.count === 1) open(modelKey, watch);
74
+ let released = false;
75
+ return () => {
76
+ if (released) return;
77
+ released = true;
78
+ const current = watches.get(modelKey);
79
+ if (!current) return;
80
+ current.count -= 1;
81
+ if (current.count > 0) return;
82
+ current.subscription?.unsubscribe();
83
+ current.subscription = null;
84
+ };
85
+ },
86
+ cursorFor(modelKey) {
87
+ return watches.get(modelKey)?.cursor ?? null;
88
+ },
89
+ watchers(modelKey) {
90
+ return watches.get(modelKey)?.count ?? 0;
91
+ },
92
+ get watching() {
93
+ return [...watches.entries()].filter(([, w]) => w.subscription !== null).map(([k]) => k);
94
+ },
95
+ dispose() {
96
+ for (const watch of watches.values()) {
97
+ watch.subscription?.unsubscribe();
98
+ watch.subscription = null;
99
+ watch.count = 0;
100
+ }
101
+ watches.clear();
102
+ }
103
+ };
104
+ }
105
+
106
+ // src/live/coalesce.ts
107
+ var DEFAULT_WINDOW_MS = 25;
108
+ var DEFAULT_MAX_CONCURRENT = 4;
109
+ function createFoirCoalescer(options) {
110
+ const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
111
+ const maxConcurrent = Math.max(1, options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT);
112
+ const timers = options.timers ?? {
113
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
114
+ clearTimeout: (handle) => clearTimeout(handle)
115
+ };
116
+ const waiting = /* @__PURE__ */ new Set();
117
+ const running = /* @__PURE__ */ new Set();
118
+ const superseded = /* @__PURE__ */ new Set();
119
+ let timer = null;
120
+ let idle = [];
121
+ let disposed = false;
122
+ function settleIdle() {
123
+ if (waiting.size > 0 || running.size > 0 || timer !== null) return;
124
+ const waiters = idle;
125
+ idle = [];
126
+ for (const resolve of waiters) resolve();
127
+ }
128
+ function pump() {
129
+ while (running.size < maxConcurrent && waiting.size > 0) {
130
+ const key = waiting.values().next().value;
131
+ waiting.delete(key);
132
+ running.add(key);
133
+ void start(key);
134
+ }
135
+ settleIdle();
136
+ }
137
+ async function start(key) {
138
+ try {
139
+ await options.run(key);
140
+ } catch (error) {
141
+ options.onError?.(error, key);
142
+ } finally {
143
+ running.delete(key);
144
+ if (superseded.delete(key) && !disposed) {
145
+ waiting.add(key);
146
+ }
147
+ pump();
148
+ }
149
+ }
150
+ function arm() {
151
+ if (timer !== null || disposed) return;
152
+ timer = timers.setTimeout(() => {
153
+ timer = null;
154
+ pump();
155
+ }, windowMs);
156
+ }
157
+ return {
158
+ invalidate(key) {
159
+ if (disposed) return;
160
+ if (running.has(key)) {
161
+ superseded.add(key);
162
+ return;
163
+ }
164
+ waiting.add(key);
165
+ arm();
166
+ },
167
+ async flush() {
168
+ if (timer !== null) {
169
+ timers.clearTimeout(timer);
170
+ timer = null;
171
+ }
172
+ pump();
173
+ if (waiting.size === 0 && running.size === 0) return;
174
+ await new Promise((resolve) => idle.push(resolve));
175
+ },
176
+ get pending() {
177
+ return waiting.size;
178
+ },
179
+ get inFlight() {
180
+ return running.size;
181
+ },
182
+ dispose() {
183
+ disposed = true;
184
+ if (timer !== null) {
185
+ timers.clearTimeout(timer);
186
+ timer = null;
187
+ }
188
+ waiting.clear();
189
+ superseded.clear();
190
+ settleIdle();
191
+ }
192
+ };
193
+ }
194
+
195
+ // src/live/tanstack.ts
196
+ var FOIR_QUERY_KEY_ROOT = "foir";
197
+ function foirModelQueryKey(modelKey) {
198
+ return [FOIR_QUERY_KEY_ROOT, modelKey];
199
+ }
200
+ function applyFoirRecordChange(queryClient, event, options = {}) {
201
+ const mode = options.mode ?? "auto";
202
+ const idField = options.idField ?? "_id";
203
+ const effect = {
204
+ invalidated: [],
205
+ removed: [],
206
+ created: event.action === "created",
207
+ totalCountStale: event.action === "created" || event.action === "deleted"
208
+ };
209
+ if (mode === "off" || mode === "notify") return effect;
210
+ const modelKey = foirModelQueryKey(event.modelKey);
211
+ if (event.action === "deleted") {
212
+ const removed = [];
213
+ queryClient.setQueriesData({ queryKey: modelKey }, (previous) => {
214
+ const next = withoutRecord(previous, event.recordId, idField);
215
+ return next.changed ? next.value : previous;
216
+ });
217
+ for (const [key, value] of queryClient.getQueriesData({ queryKey: modelKey })) {
218
+ if (!containsRecord(value, event.recordId, idField)) removed.push(key);
219
+ }
220
+ return { ...effect, removed };
221
+ }
222
+ if (event.action === "updated") {
223
+ const invalidated = [];
224
+ const invalidate = options.invalidate ?? ((key) => {
225
+ queryClient.invalidateQueries({ queryKey: key, exact: true });
226
+ });
227
+ for (const [key, value] of queryClient.getQueriesData({ queryKey: modelKey })) {
228
+ if (!containsRecord(value, event.recordId, idField)) continue;
229
+ invalidate(key);
230
+ invalidated.push(key);
231
+ }
232
+ return { ...effect, invalidated };
233
+ }
234
+ return effect;
235
+ }
236
+ function ids(value, idField) {
237
+ const page = value;
238
+ if (!page || typeof page !== "object") return [];
239
+ const out = [];
240
+ for (const edge of page.edges ?? []) {
241
+ const id = edge?.node?.[idField];
242
+ if (typeof id === "string") out.push(id);
243
+ }
244
+ for (const node of page.nodes ?? []) {
245
+ const id = node?.[idField];
246
+ if (typeof id === "string") out.push(id);
247
+ }
248
+ const own = page[idField];
249
+ if (typeof own === "string") out.push(own);
250
+ return out;
251
+ }
252
+ function containsRecord(value, recordId, idField) {
253
+ return ids(value, idField).includes(recordId);
254
+ }
255
+ function withoutRecord(value, recordId, idField) {
256
+ const page = value;
257
+ if (!page || typeof page !== "object") return { value, changed: false };
258
+ let changed = false;
259
+ const next = { ...page };
260
+ if (Array.isArray(page.edges)) {
261
+ const edges = page.edges.filter(
262
+ (edge) => edge?.node?.[idField] !== recordId
263
+ );
264
+ if (edges.length !== page.edges.length) {
265
+ next.edges = edges;
266
+ changed = true;
267
+ }
268
+ }
269
+ if (Array.isArray(page.nodes)) {
270
+ const nodes = page.nodes.filter(
271
+ (node) => node?.[idField] !== recordId
272
+ );
273
+ if (nodes.length !== page.nodes.length) {
274
+ next.nodes = nodes;
275
+ changed = true;
276
+ }
277
+ }
278
+ return changed ? { value: next, changed: true } : { value, changed: false };
279
+ }
280
+
281
+ // src/live/react/index.tsx
282
+ var EMPTY_SNAPSHOT = {
283
+ newCounts: {},
284
+ totalCountStale: [],
285
+ lastEvent: {}
286
+ };
287
+ var FoirLiveContext = createContext(null);
288
+ function FoirLive(props) {
289
+ const {
290
+ subscribe,
291
+ queryClient,
292
+ mode = "auto",
293
+ idField,
294
+ onResync,
295
+ onError,
296
+ onEffect,
297
+ reconcile,
298
+ coalesceWindowMs,
299
+ maxConcurrentRefetches,
300
+ children
301
+ } = props;
302
+ const latest = useRef({ queryClient, mode, idField, onResync, onError, onEffect, reconcile });
303
+ latest.current = { queryClient, mode, idField, onResync, onError, onEffect, reconcile };
304
+ const snapshotRef = useRef(EMPTY_SNAPSHOT);
305
+ const listeners = useRef(/* @__PURE__ */ new Set()).current;
306
+ const emit = useCallback(
307
+ (next) => {
308
+ snapshotRef.current = next;
309
+ for (const listener of listeners) listener();
310
+ },
311
+ [listeners]
312
+ );
313
+ const subscribeRef = useRef(subscribe);
314
+ subscribeRef.current = subscribe;
315
+ const coalescerRef = useRef(null);
316
+ if (coalescerRef.current === null) {
317
+ coalescerRef.current = createFoirCoalescer({
318
+ run: (key) => latest.current.queryClient.invalidateQueries({
319
+ queryKey: JSON.parse(key),
320
+ exact: true
321
+ }),
322
+ windowMs: coalesceWindowMs,
323
+ maxConcurrent: maxConcurrentRefetches,
324
+ onError: (error) => latest.current.onError?.(error, "")
325
+ });
326
+ }
327
+ const manager = useMemo(
328
+ () => createFoirLiveManager({
329
+ subscribe: (modelKey, options, handlers) => subscribeRef.current(modelKey, options, handlers),
330
+ onChange: (event) => {
331
+ const current = latest.current;
332
+ const effect = applyFoirRecordChange(current.queryClient, event, {
333
+ mode: current.mode,
334
+ idField: current.idField,
335
+ // The key is carried as JSON because a coalescer keys on a string —
336
+ // two structurally identical query keys are different objects, and
337
+ // a burst on one query would otherwise coalesce nothing at all.
338
+ invalidate: (key) => coalescerRef.current?.invalidate(JSON.stringify(key))
339
+ });
340
+ const previous = snapshotRef.current;
341
+ emit({
342
+ newCounts: effect.created ? { ...previous.newCounts, [event.modelKey]: (previous.newCounts[event.modelKey] ?? 0) + 1 } : previous.newCounts,
343
+ totalCountStale: effect.totalCountStale ? [.../* @__PURE__ */ new Set([...previous.totalCountStale, event.modelKey])] : previous.totalCountStale,
344
+ lastEvent: { ...previous.lastEvent, [event.modelKey]: event }
345
+ });
346
+ current.onEffect?.(effect, event);
347
+ },
348
+ onResync: (error, modelKey) => latest.current.onResync?.(error, modelKey),
349
+ onError: (error, modelKey) => latest.current.onError?.(error, modelKey),
350
+ reconcile: (modelKey) => latest.current.reconcile?.(modelKey)
351
+ }),
352
+ [emit]
353
+ );
354
+ useEffect(
355
+ () => () => {
356
+ manager.dispose();
357
+ coalescerRef.current?.dispose();
358
+ },
359
+ [manager]
360
+ );
361
+ const value = useMemo(
362
+ () => ({
363
+ manager,
364
+ retain: (modelKey) => manager.retain(modelKey),
365
+ subscribeEffects: (listener) => {
366
+ listeners.add(listener);
367
+ return () => listeners.delete(listener);
368
+ },
369
+ snapshot: () => snapshotRef.current
370
+ }),
371
+ [manager, listeners]
372
+ );
373
+ return createElement(FoirLiveContext.Provider, { value }, children);
374
+ }
375
+ function useFoirLiveContext(hook) {
376
+ const value = useContext(FoirLiveContext);
377
+ if (!value) {
378
+ throw new Error(`[foir] ${hook} must be used inside <FoirLive>`);
379
+ }
380
+ return value;
381
+ }
382
+ function useFoirLive(modelKey) {
383
+ const { retain } = useFoirLiveContext("useFoirLive");
384
+ useEffect(() => retain(modelKey), [retain, modelKey]);
385
+ }
386
+ function useFoirLiveStatus() {
387
+ const { subscribeEffects, snapshot } = useFoirLiveContext("useFoirLiveStatus");
388
+ return useSyncExternalStore(subscribeEffects, snapshot, () => EMPTY_SNAPSHOT);
389
+ }
390
+ export {
391
+ FoirLive,
392
+ foirModelQueryKey,
393
+ useFoirLive,
394
+ useFoirLiveStatus
395
+ };