@absolutejs/sync 1.9.0 → 1.9.2

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.
@@ -0,0 +1,1938 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __require = import.meta.require;
17
+
18
+ // src/engine/equiJoin.ts
19
+ var shallowEqual = (a, b) => {
20
+ if (a === b) {
21
+ return true;
22
+ }
23
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
24
+ return false;
25
+ }
26
+ const aKeys = Object.keys(a);
27
+ const bKeys = Object.keys(b);
28
+ return aKeys.length === bKeys.length && aKeys.every((key) => a[key] === b[key]);
29
+ };
30
+ var addToIndex = (index, joinValue, key) => {
31
+ let bucket = index.get(joinValue);
32
+ if (bucket === undefined) {
33
+ bucket = new Set;
34
+ index.set(joinValue, bucket);
35
+ }
36
+ bucket.add(key);
37
+ };
38
+ var removeFromIndex = (index, joinValue, key) => {
39
+ const bucket = index.get(joinValue);
40
+ if (bucket === undefined) {
41
+ return;
42
+ }
43
+ bucket.delete(key);
44
+ if (bucket.size === 0) {
45
+ index.delete(joinValue);
46
+ }
47
+ };
48
+ var createEquiJoin = (options) => {
49
+ const { leftKey, rightKey, leftOn, rightOn, select, selectUnmatched } = options;
50
+ const equals = options.equals ?? shallowEqual;
51
+ const lefts = new Map;
52
+ const rights = new Map;
53
+ const leftByJoin = new Map;
54
+ const rightByJoin = new Map;
55
+ const output = new Map;
56
+ const outByLeft = new Map;
57
+ const outKey = (lk, rk) => `${lk} ${rk}`;
58
+ const unmatchedKey = (lk) => `${lk} ~`;
59
+ const leftOutputs = (lk, left) => {
60
+ const result = new Map;
61
+ const rks = rightByJoin.get(leftOn(left));
62
+ if (rks !== undefined && rks.size > 0) {
63
+ for (const rk of rks) {
64
+ const right = rights.get(rk);
65
+ if (right !== undefined) {
66
+ result.set(outKey(lk, rk), select(left, right));
67
+ }
68
+ }
69
+ } else if (selectUnmatched !== undefined) {
70
+ result.set(unmatchedKey(lk), selectUnmatched(left));
71
+ }
72
+ return result;
73
+ };
74
+ const reconcileLeft = (lk, after) => {
75
+ const before = outByLeft.get(lk) ?? new Set;
76
+ const added = [];
77
+ const removed = [];
78
+ const changed = [];
79
+ for (const [ok, value] of after) {
80
+ const previous = output.get(ok);
81
+ if (previous === undefined) {
82
+ added.push(value);
83
+ } else if (!equals(previous, value)) {
84
+ changed.push(value);
85
+ }
86
+ output.set(ok, value);
87
+ }
88
+ for (const ok of before) {
89
+ if (!after.has(ok)) {
90
+ const previous = output.get(ok);
91
+ if (previous !== undefined) {
92
+ removed.push(previous);
93
+ output.delete(ok);
94
+ }
95
+ }
96
+ }
97
+ if (after.size === 0) {
98
+ outByLeft.delete(lk);
99
+ } else {
100
+ outByLeft.set(lk, new Set(after.keys()));
101
+ }
102
+ return { added, removed, changed };
103
+ };
104
+ const mergeInto = (target, diff) => {
105
+ target.added.push(...diff.added);
106
+ target.removed.push(...diff.removed);
107
+ target.changed.push(...diff.changed);
108
+ };
109
+ return {
110
+ hydrate: (left, right) => {
111
+ lefts.clear();
112
+ rights.clear();
113
+ leftByJoin.clear();
114
+ rightByJoin.clear();
115
+ output.clear();
116
+ outByLeft.clear();
117
+ for (const right_ of right) {
118
+ const rk = rightKey(right_);
119
+ rights.set(rk, right_);
120
+ addToIndex(rightByJoin, rightOn(right_), rk);
121
+ }
122
+ for (const left_ of left) {
123
+ const lk = leftKey(left_);
124
+ lefts.set(lk, left_);
125
+ addToIndex(leftByJoin, leftOn(left_), lk);
126
+ const outs = leftOutputs(lk, left_);
127
+ for (const [ok, value] of outs) {
128
+ output.set(ok, value);
129
+ }
130
+ if (outs.size > 0) {
131
+ outByLeft.set(lk, new Set(outs.keys()));
132
+ }
133
+ }
134
+ },
135
+ applyLeft: ({ op, row }) => {
136
+ const lk = leftKey(row);
137
+ const existing = lefts.get(lk);
138
+ if (existing !== undefined) {
139
+ removeFromIndex(leftByJoin, leftOn(existing), lk);
140
+ }
141
+ if (op === "delete") {
142
+ lefts.delete(lk);
143
+ } else {
144
+ lefts.set(lk, row);
145
+ addToIndex(leftByJoin, leftOn(row), lk);
146
+ }
147
+ const after = op === "delete" ? new Map : leftOutputs(lk, row);
148
+ return reconcileLeft(lk, after);
149
+ },
150
+ applyRight: ({ op, row }) => {
151
+ const rk = rightKey(row);
152
+ const existing = rights.get(rk);
153
+ const affected = new Set;
154
+ const addAffected = (joinValue) => {
155
+ for (const lk of leftByJoin.get(joinValue) ?? []) {
156
+ affected.add(lk);
157
+ }
158
+ };
159
+ if (existing !== undefined) {
160
+ addAffected(rightOn(existing));
161
+ removeFromIndex(rightByJoin, rightOn(existing), rk);
162
+ }
163
+ if (op === "delete") {
164
+ rights.delete(rk);
165
+ } else {
166
+ rights.set(rk, row);
167
+ addToIndex(rightByJoin, rightOn(row), rk);
168
+ addAffected(rightOn(row));
169
+ }
170
+ const diff = { added: [], removed: [], changed: [] };
171
+ for (const lk of affected) {
172
+ const left = lefts.get(lk);
173
+ if (left !== undefined) {
174
+ mergeInto(diff, reconcileLeft(lk, leftOutputs(lk, left)));
175
+ }
176
+ }
177
+ return diff;
178
+ },
179
+ rows: () => [...output.values()],
180
+ size: () => output.size
181
+ };
182
+ };
183
+
184
+ // src/engine/materializedView.ts
185
+ var emptyDiff = () => ({
186
+ added: [],
187
+ removed: [],
188
+ changed: []
189
+ });
190
+ var shallowEqual2 = (a, b) => {
191
+ if (a === b) {
192
+ return true;
193
+ }
194
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
195
+ return false;
196
+ }
197
+ const aKeys = Object.keys(a);
198
+ const bKeys = Object.keys(b);
199
+ if (aKeys.length !== bKeys.length) {
200
+ return false;
201
+ }
202
+ return aKeys.every((key) => a[key] === b[key]);
203
+ };
204
+ var isEmptyViewDiff = (diff) => diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0;
205
+ var createMaterializedView = (options) => {
206
+ const { key, match } = options;
207
+ const equals = options.equals ?? shallowEqual2;
208
+ const set = new Map;
209
+ return {
210
+ hydrate: (rows) => {
211
+ set.clear();
212
+ for (const row of rows) {
213
+ set.set(key(row), row);
214
+ }
215
+ },
216
+ reset: (rows) => {
217
+ const next = new Map;
218
+ const added = [];
219
+ const changed = [];
220
+ for (const row of rows) {
221
+ const rowKey = key(row);
222
+ next.set(rowKey, row);
223
+ const previous = set.get(rowKey);
224
+ if (previous === undefined) {
225
+ added.push(row);
226
+ } else if (!equals(previous, row)) {
227
+ changed.push(row);
228
+ }
229
+ }
230
+ const removed = [];
231
+ for (const [rowKey, previous] of set) {
232
+ if (!next.has(rowKey)) {
233
+ removed.push(previous);
234
+ }
235
+ }
236
+ set.clear();
237
+ for (const [rowKey, row] of next) {
238
+ set.set(rowKey, row);
239
+ }
240
+ return { added, removed, changed };
241
+ },
242
+ apply: ({ op, row }) => {
243
+ const rowKey = key(row);
244
+ const existing = set.get(rowKey);
245
+ if (op === "delete") {
246
+ if (existing === undefined) {
247
+ return emptyDiff();
248
+ }
249
+ set.delete(rowKey);
250
+ return { added: [], removed: [existing], changed: [] };
251
+ }
252
+ if (match(row)) {
253
+ set.set(rowKey, row);
254
+ return existing === undefined ? { added: [row], removed: [], changed: [] } : { added: [], removed: [], changed: [row] };
255
+ }
256
+ if (existing !== undefined) {
257
+ set.delete(rowKey);
258
+ return { added: [], removed: [existing], changed: [] };
259
+ }
260
+ return emptyDiff();
261
+ },
262
+ rows: () => [...set.values()],
263
+ size: () => set.size
264
+ };
265
+ };
266
+
267
+ // src/engine/retry.ts
268
+ var PG_RETRY_CODES = new Set(["40001", "40P01"]);
269
+ var isSerializationFailure = (error) => {
270
+ if (error === null || typeof error !== "object")
271
+ return false;
272
+ const code = error.code;
273
+ if (typeof code === "string" && PG_RETRY_CODES.has(code))
274
+ return true;
275
+ const cause = error.cause;
276
+ if (cause !== undefined)
277
+ return isSerializationFailure(cause);
278
+ return false;
279
+ };
280
+ var exponentialBackoff = (options = {}) => (attempt) => {
281
+ const base = options.baseMs ?? 25;
282
+ const factor = options.factor ?? 2;
283
+ const max = options.maxMs ?? 1000;
284
+ const jitter = options.jitter ?? 0.2;
285
+ const raw = Math.min(max, base * factor ** Math.max(0, attempt - 1));
286
+ const spread = raw * jitter;
287
+ return raw + (Math.random() * 2 - 1) * spread;
288
+ };
289
+
290
+ class RetriesExhaustedError extends Error {
291
+ attempts;
292
+ elapsedMs;
293
+ cause;
294
+ constructor(attempts, elapsedMs, cause) {
295
+ const message = cause instanceof Error ? cause.message : String(cause);
296
+ super(`retries exhausted after ${attempts} attempts (${elapsedMs}ms): ${message}`);
297
+ this.name = "RetriesExhaustedError";
298
+ this.attempts = attempts;
299
+ this.elapsedMs = elapsedMs;
300
+ this.cause = cause;
301
+ }
302
+ }
303
+
304
+ // src/engine/sandbox.ts
305
+ var isolatedJscModule;
306
+ var loadIsolatedJsc = async () => {
307
+ if (isolatedJscModule !== undefined)
308
+ return isolatedJscModule;
309
+ try {
310
+ isolatedJscModule = await import("@absolutejs/isolated-jsc");
311
+ return isolatedJscModule;
312
+ } catch (error) {
313
+ throw new Error('sandboxedHandler requires the optional peer "@absolutejs/isolated-jsc". Install it with: bun add @absolutejs/isolated-jsc', { cause: error });
314
+ }
315
+ };
316
+ var wrap = (source) => `
317
+ function (__callId, args, ctx) {
318
+ const userFn = (${source});
319
+ if (typeof userFn !== 'function') {
320
+ throw new Error(
321
+ 'sandboxedHandler must evaluate to (args, ctx, actions) => result; got ' +
322
+ typeof userFn
323
+ );
324
+ }
325
+ const actions = {
326
+ insert: (table, data) => __dispatch(__callId, 'insert', table, data),
327
+ update: (table, data) => __dispatch(__callId, 'update', table, data),
328
+ delete: (table, row) => __dispatch(__callId, 'delete', table, row),
329
+ change: (collection, change) => __dispatch(__callId, 'change', collection, change),
330
+ now: () => __dispatch(__callId, 'now'),
331
+ fetch: (url, init) => __dispatch(__callId, 'fetch', url, init)
332
+ };
333
+ return userFn(args, ctx, actions);
334
+ }
335
+ `;
336
+ var compile = async (source, config, bridgeFetch) => {
337
+ const { Reference, createIsolatedRunner, resolveIsolatePolicy } = await loadIsolatedJsc();
338
+ const callMap = new Map;
339
+ const dispatch = new Reference((callId, op, ...rest) => {
340
+ const a = callMap.get(callId);
341
+ if (a === undefined) {
342
+ throw new Error(`__dispatch invoked for orphan callId ${String(callId)}`);
343
+ }
344
+ switch (op) {
345
+ case "insert":
346
+ return a.insert(rest[0], rest[1]);
347
+ case "update":
348
+ return a.update(rest[0], rest[1]);
349
+ case "delete":
350
+ return a.delete(rest[0], rest[1]);
351
+ case "change":
352
+ return a.change(rest[0], rest[1]);
353
+ case "now":
354
+ return a.now();
355
+ case "fetch":
356
+ return runBridgeFetch(bridgeFetch, rest[0], rest[1]);
357
+ default:
358
+ throw new Error(`unknown sandbox action op: ${String(op)}`);
359
+ }
360
+ });
361
+ const timeoutMs = config.timeout ?? 5000;
362
+ const sourceToCall = wrap(source);
363
+ const policy = resolveIsolatePolicy("tenant-script", {
364
+ allowWorkerFallback: true,
365
+ backend: config.backend ?? "auto",
366
+ memoryLimit: config.memoryLimit ?? 32,
367
+ timeout: timeoutMs
368
+ });
369
+ const runner = createIsolatedRunner({
370
+ globals: { __dispatch: dispatch },
371
+ policy
372
+ });
373
+ await runner.precompile("sandboxedHandler", sourceToCall);
374
+ return {
375
+ callMap,
376
+ nextCallId: 1,
377
+ runner,
378
+ source: sourceToCall,
379
+ timeoutMs
380
+ };
381
+ };
382
+ var runBridgeFetch = async (config, url, init) => {
383
+ if (config === undefined) {
384
+ throw new Error("actions.fetch called but the engine has no `bridgeFetch` config \u2014 " + "pass `bridgeFetch: { ... }` to createSyncEngine.");
385
+ }
386
+ let parsed;
387
+ try {
388
+ parsed = new URL(url);
389
+ } catch {
390
+ throw new Error(`actions.fetch: invalid URL "${String(url)}"`);
391
+ }
392
+ const endpoint = config[parsed.hostname] ?? (Object.prototype.hasOwnProperty.call(config, "*") ? config["*"] : undefined);
393
+ if (endpoint === undefined) {
394
+ throw new Error(`actions.fetch: hostname "${parsed.hostname}" is not allowlisted in bridgeFetch config`);
395
+ }
396
+ const headers = { ...endpoint.headers ?? {} };
397
+ if (init?.headers !== undefined) {
398
+ const incoming = init.headers;
399
+ for (const [name, value] of Object.entries(incoming)) {
400
+ if (name.toLowerCase() === "authorization")
401
+ continue;
402
+ headers[name] = value;
403
+ }
404
+ }
405
+ if (endpoint.authorization !== undefined) {
406
+ let auth;
407
+ try {
408
+ auth = await endpoint.authorization();
409
+ } catch {
410
+ throw new Error("actions.fetch: authorization callback failed");
411
+ }
412
+ headers.Authorization = auth;
413
+ }
414
+ const response = await fetch(url, { ...init, headers });
415
+ const responseHeaders = {};
416
+ response.headers.forEach((value, name) => {
417
+ responseHeaders[name] = value;
418
+ });
419
+ const body = await response.text();
420
+ return {
421
+ body,
422
+ headers: responseHeaders,
423
+ ok: response.ok,
424
+ status: response.status,
425
+ statusText: response.statusText,
426
+ url: response.url
427
+ };
428
+ };
429
+ var makeSandboxedHandler = (source, config = {}, engineExtras) => {
430
+ let pending;
431
+ const metricsHook = engineExtras?.metricsHook;
432
+ const bridgeFetch = engineExtras?.bridgeFetch;
433
+ const getCompiled = async () => {
434
+ if (pending !== undefined) {
435
+ return pending;
436
+ }
437
+ pending = compile(source, config, bridgeFetch);
438
+ return pending;
439
+ };
440
+ return async (args, ctx, actions) => {
441
+ const compiled = await getCompiled();
442
+ const callId = compiled.nextCallId++;
443
+ compiled.callMap.set(callId, actions);
444
+ if (metricsHook === undefined) {
445
+ try {
446
+ return await compiled.runner.call("sandboxedHandler", compiled.source, [callId, args, ctx], { run: { timeout: compiled.timeoutMs } });
447
+ } catch (error) {
448
+ if (isIsolateDisposalError(error)) {
449
+ pending = undefined;
450
+ await disposeCompiled(compiled);
451
+ }
452
+ throw error;
453
+ } finally {
454
+ compiled.callMap.delete(callId);
455
+ }
456
+ }
457
+ const startedAt = performance.now();
458
+ const id = makeRandomId();
459
+ try {
460
+ const { result, metrics } = await compiled.runner.call("sandboxedHandler", compiled.source, [callId, args, ctx], { run: { timeout: compiled.timeoutMs }, withMetrics: true });
461
+ fireMetrics(metricsHook.onMetrics, {
462
+ backend: metrics.backend,
463
+ cpuMs: metrics.cpuMs,
464
+ durationMs: performance.now() - startedAt,
465
+ heapBytes: metrics.heapBytes,
466
+ id,
467
+ mutationName: metricsHook.mutationName,
468
+ ok: true,
469
+ timestamp: Date.now()
470
+ });
471
+ return result;
472
+ } catch (error) {
473
+ if (isIsolateDisposalError(error)) {
474
+ pending = undefined;
475
+ await disposeCompiled(compiled);
476
+ }
477
+ fireMetrics(metricsHook.onMetrics, {
478
+ cpuMs: 0,
479
+ durationMs: performance.now() - startedAt,
480
+ errorMessage: error instanceof Error ? error.message : String(error),
481
+ errorName: error instanceof Error ? error.name : "Error",
482
+ heapBytes: 0,
483
+ id,
484
+ mutationName: metricsHook.mutationName,
485
+ ok: false,
486
+ timestamp: Date.now()
487
+ });
488
+ throw error;
489
+ } finally {
490
+ compiled.callMap.delete(callId);
491
+ }
492
+ };
493
+ };
494
+ var makeRandomId = () => `hm_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
495
+ var isIsolateDisposalError = (error) => error instanceof Error && (error.name === "TimeoutError" || error.name === "MemoryLimitError" || error.name === "IsolateDisposedError");
496
+ var disposeCompiled = async (compiled) => {
497
+ try {
498
+ await compiled.runner.dispose();
499
+ } catch {}
500
+ };
501
+ var fireMetrics = (hook, record) => {
502
+ let outcome;
503
+ try {
504
+ outcome = hook(record);
505
+ } catch {
506
+ return;
507
+ }
508
+ if (outcome instanceof Promise) {
509
+ outcome.catch(() => {});
510
+ }
511
+ };
512
+
513
+ // src/engine/pack.ts
514
+ class PackTableConflictError extends Error {
515
+ table;
516
+ existingPack;
517
+ newPack;
518
+ constructor(table, existingPack, newPack) {
519
+ super(`Pack "${newPack}" claims table "${table}", but "${existingPack}" already owns it. Use a tablePrefix on one of them.`);
520
+ this.name = "PackTableConflictError";
521
+ this.table = table;
522
+ this.existingPack = existingPack;
523
+ this.newPack = newPack;
524
+ }
525
+ }
526
+
527
+ class PackMissingDependencyError extends Error {
528
+ pack;
529
+ missingTable;
530
+ constructor(pack, missingTable) {
531
+ super(`Pack "${pack}" requires a reader for table "${missingTable}" but none is registered. Call engine.registerReader("${missingTable}", ...) before engine.registerPack.`);
532
+ this.name = "PackMissingDependencyError";
533
+ this.pack = pack;
534
+ this.missingTable = missingTable;
535
+ }
536
+ }
537
+ var defineSyncPack = (pack) => pack;
538
+
539
+ // src/engine/search.ts
540
+ var SEARCH_SCORE_FIELD = "_score";
541
+ var defineSearchCollection = (definition) => ({
542
+ ...definition,
543
+ kind: "search"
544
+ });
545
+
546
+ // src/engine/syncEngine.ts
547
+ class UnauthorizedError extends Error {
548
+ constructor(subject) {
549
+ super(`Not authorized: ${subject}`);
550
+ this.name = "UnauthorizedError";
551
+ }
552
+ }
553
+
554
+ class SchemaError extends Error {
555
+ constructor(table, fieldName) {
556
+ super(`Schema violation on "${table}": invalid field "${fieldName}"`);
557
+ this.name = "SchemaError";
558
+ }
559
+ }
560
+
561
+ class MissedChangesError extends Error {
562
+ requestedSince;
563
+ availableSince;
564
+ constructor(requestedSince, availableSince) {
565
+ super(`Change log no longer covers version ${requestedSince}; oldest available is ${availableSince}. ` + `Re-bootstrap and resume from ${availableSince}.`);
566
+ this.name = "MissedChangesError";
567
+ this.requestedSince = requestedSince;
568
+ this.availableSince = availableSince;
569
+ }
570
+ }
571
+
572
+ class CdcConsumerSlowError extends Error {
573
+ maxBuffer;
574
+ lastDeliveredVersion;
575
+ constructor(maxBuffer, lastDeliveredVersion) {
576
+ super(`CDC stream buffer overflowed (max ${maxBuffer}); consumer fell behind. ` + `Last delivered version: ${lastDeliveredVersion}. Resubscribe with since=${lastDeliveredVersion}.`);
577
+ this.name = "CdcConsumerSlowError";
578
+ this.maxBuffer = maxBuffer;
579
+ this.lastDeliveredVersion = lastDeliveredVersion;
580
+ }
581
+ }
582
+ var defaultKey = (row) => row.id;
583
+ var shallowEqual3 = (a, b) => {
584
+ if (a === b) {
585
+ return true;
586
+ }
587
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
588
+ return false;
589
+ }
590
+ const aKeys = Object.keys(a);
591
+ const bKeys = Object.keys(b);
592
+ return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
593
+ };
594
+ var subKeyIds = new WeakMap;
595
+ var subKeyCounter = 0;
596
+ var stableValueKey = (value) => {
597
+ if (value === undefined)
598
+ return "u";
599
+ if (value === null)
600
+ return "n";
601
+ const tag = typeof value;
602
+ if (tag === "string")
603
+ return `s:${value}`;
604
+ if (tag === "number" || tag === "boolean" || tag === "bigint") {
605
+ return `${tag[0]}:${String(value)}`;
606
+ }
607
+ if (tag !== "object")
608
+ return `${tag[0]}:fn`;
609
+ try {
610
+ return `o:${JSON.stringify(value, (_k, v) => {
611
+ if (v === null || typeof v !== "object" || Array.isArray(v))
612
+ return v;
613
+ const record = v;
614
+ const sorted = {};
615
+ for (const key of Object.keys(record).sort()) {
616
+ sorted[key] = record[key];
617
+ }
618
+ return sorted;
619
+ })}`;
620
+ } catch {
621
+ const obj = value;
622
+ let id = subKeyIds.get(obj);
623
+ if (id === undefined) {
624
+ subKeyCounter += 1;
625
+ id = `i${subKeyCounter}`;
626
+ subKeyIds.set(obj, id);
627
+ }
628
+ return `i:${id}`;
629
+ }
630
+ };
631
+ var stableSubKey = (collection, params, ctx) => `${collection}|${stableValueKey(params)}|${stableValueKey(ctx)}`;
632
+ var equalsIgnoringScore = (a, b) => {
633
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
634
+ return a === b;
635
+ }
636
+ const strip = (value) => Object.keys(value).filter((k) => k !== SEARCH_SCORE_FIELD);
637
+ const aKeys = strip(a);
638
+ const bKeys = strip(b);
639
+ return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
640
+ };
641
+ var createSyncEngine = (options = {}) => {
642
+ const registry = new Map;
643
+ const mutations = new Map;
644
+ const sandboxRunners = new Map;
645
+ const writers = new Map;
646
+ const readers = new Map;
647
+ const schedules = new Map;
648
+ const packTableOwners = new Map;
649
+ const registeredPacks = [];
650
+ const permissions = new Map;
651
+ for (const [table, rules] of Object.entries(options.permissions ?? {})) {
652
+ permissions.set(table, rules);
653
+ }
654
+ const readRuleFor = (table) => permissions.get(table)?.read;
655
+ const writeRuleFor = (table, op) => {
656
+ const rules = permissions.get(table);
657
+ return rules?.[op] ?? rules?.write;
658
+ };
659
+ const schemas = new Map;
660
+ for (const [table, schema] of Object.entries(options.schemas ?? {})) {
661
+ schemas.set(table, schema);
662
+ }
663
+ const crdtFields = new Map;
664
+ const validateWrite = (table, op, row) => {
665
+ const schema = schemas.get(table);
666
+ if (schema === undefined || typeof row !== "object" || row === null) {
667
+ return;
668
+ }
669
+ const record = row;
670
+ for (const [fieldName, validate] of Object.entries(schema.fields)) {
671
+ const present = fieldName in record;
672
+ if (op === "update" && !present) {
673
+ continue;
674
+ }
675
+ if (!validate(record[fieldName])) {
676
+ throw new SchemaError(table, fieldName);
677
+ }
678
+ }
679
+ };
680
+ const migrateRow = (table, row) => {
681
+ const migrate = schemas.get(table)?.migrate;
682
+ return migrate ? migrate(row) : row;
683
+ };
684
+ const reactiveSubs = new Set;
685
+ const searchSubs = new Set;
686
+ const searchIndexes = new Map;
687
+ const active = new Map;
688
+ const tableIndex = new Map;
689
+ const changeLogSize = options.changeLogSize ?? 1024;
690
+ const changeLog = [];
691
+ let version = 0;
692
+ const reactiveCacheMax = options.reactiveCache?.max ?? 256;
693
+ const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
694
+ const cachedReruns = new Map;
695
+ const touchCacheEntry = (key, entry) => {
696
+ cachedReruns.delete(key);
697
+ cachedReruns.set(key, entry);
698
+ };
699
+ const readCacheEntry = (key) => {
700
+ if (reactiveCacheMax <= 0)
701
+ return;
702
+ const entry = cachedReruns.get(key);
703
+ if (entry === undefined)
704
+ return;
705
+ if (reactiveCacheTtlMs > 0 && entry.expiresAt < Date.now()) {
706
+ cachedReruns.delete(key);
707
+ return;
708
+ }
709
+ touchCacheEntry(key, entry);
710
+ return entry;
711
+ };
712
+ const writeCacheEntry = (entry) => {
713
+ if (reactiveCacheMax <= 0)
714
+ return;
715
+ cachedReruns.set(entry.rerunKey, entry);
716
+ while (cachedReruns.size > reactiveCacheMax) {
717
+ const oldest = cachedReruns.keys().next().value;
718
+ if (oldest === undefined)
719
+ break;
720
+ cachedReruns.delete(oldest);
721
+ }
722
+ };
723
+ const activityListeners = new Set;
724
+ const emitActivity = (event) => {
725
+ for (const listener of activityListeners) {
726
+ listener(event);
727
+ }
728
+ };
729
+ const streamSubscribers = new Set;
730
+ const runInTransaction = options.transaction;
731
+ const instanceId = globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
732
+ let clusterBus;
733
+ const broadcast = (changes) => {
734
+ if (clusterBus !== undefined && changes.length > 0) {
735
+ clusterBus.publish({ changes, origin: instanceId });
736
+ }
737
+ };
738
+ const subsFor = (collection) => {
739
+ let set = active.get(collection);
740
+ if (set === undefined) {
741
+ set = new Set;
742
+ active.set(collection, set);
743
+ }
744
+ return set;
745
+ };
746
+ const addTableIndex = (table, name) => {
747
+ let set = tableIndex.get(table);
748
+ if (set === undefined) {
749
+ set = new Set;
750
+ tableIndex.set(table, set);
751
+ }
752
+ set.add(name);
753
+ };
754
+ const sideChange = (change, match) => change.op !== "delete" && match !== undefined && !match(change.row) ? { op: "delete", row: change.row } : change;
755
+ const EMPTY_DIFF = {
756
+ added: [],
757
+ removed: [],
758
+ changed: []
759
+ };
760
+ const subscriptionDiff = async (subscription, table, change) => {
761
+ if (subscription.kind === "graph") {
762
+ return subscription.instance.applyChange(table, change);
763
+ }
764
+ if (subscription.kind === "join") {
765
+ const js = subscription.join;
766
+ if (table === js.leftTable) {
767
+ return js.op.applyLeft(sideChange(change, js.leftMatch));
768
+ }
769
+ if (table === js.rightTable) {
770
+ return js.op.applyRight(sideChange(change, js.rightMatch));
771
+ }
772
+ return EMPTY_DIFF;
773
+ }
774
+ if (subscription.kind === "reactive") {
775
+ return EMPTY_DIFF;
776
+ }
777
+ if (subscription.kind === "search") {
778
+ return EMPTY_DIFF;
779
+ }
780
+ if (subscription.incremental) {
781
+ try {
782
+ return subscription.view.apply(change);
783
+ } catch {
784
+ return subscription.view.reset(await subscription.rehydrate());
785
+ }
786
+ }
787
+ return subscription.view.reset(await subscription.rehydrate());
788
+ };
789
+ const subscriptionsForTable = function* (table) {
790
+ const names = tableIndex.get(table);
791
+ if (names === undefined) {
792
+ return;
793
+ }
794
+ for (const name of names) {
795
+ const set = active.get(name);
796
+ if (set === undefined) {
797
+ continue;
798
+ }
799
+ yield* set;
800
+ }
801
+ };
802
+ const mergeViewDiffs = (diffs, key) => {
803
+ const net = new Map;
804
+ for (const diff of diffs) {
805
+ for (const row of diff.removed) {
806
+ const previous = net.get(key(row));
807
+ if (previous?.state === "added") {
808
+ net.delete(key(row));
809
+ } else {
810
+ net.set(key(row), { state: "removed", row });
811
+ }
812
+ }
813
+ for (const row of diff.added) {
814
+ const previous = net.get(key(row));
815
+ net.set(key(row), {
816
+ state: previous?.state === "removed" ? "changed" : "added",
817
+ row
818
+ });
819
+ }
820
+ for (const row of diff.changed) {
821
+ const previous = net.get(key(row));
822
+ net.set(key(row), {
823
+ state: previous?.state === "added" ? "added" : "changed",
824
+ row
825
+ });
826
+ }
827
+ }
828
+ const added = [];
829
+ const changed = [];
830
+ const removed = [];
831
+ for (const { state, row } of net.values()) {
832
+ if (state === "added") {
833
+ added.push(row);
834
+ } else if (state === "changed") {
835
+ changed.push(row);
836
+ } else {
837
+ removed.push(row);
838
+ }
839
+ }
840
+ return { added, changed, removed };
841
+ };
842
+ const depKey = (table, key) => `${table} ${key}`;
843
+ const changedKeyFor = (table, change) => readers.get(table)?.key?.(change.row);
844
+ const makeReadHandle = (ctx, readTables, readKeys, rangeDeps, applyRules = true) => {
845
+ const readerFor = (table) => {
846
+ const reader = readers.get(table);
847
+ if (reader === undefined) {
848
+ throw new Error(`No reader registered for table "${table}" \u2014 register one with engine.registerReader`);
849
+ }
850
+ return reader;
851
+ };
852
+ const ruleFor = (table) => applyRules ? readRuleFor(table) : undefined;
853
+ return {
854
+ all: async (table) => {
855
+ readTables.add(table);
856
+ const rows = [...await readerFor(table).all(ctx)].map((row) => migrateRow(table, row));
857
+ const rule = ruleFor(table);
858
+ return rule ? rows.filter((row) => rule(ctx, row)) : rows;
859
+ },
860
+ get: async (table, key) => {
861
+ const reader = readerFor(table);
862
+ if (reader.get === undefined) {
863
+ throw new Error(`Reader for table "${table}" has no get(); use db.all() or add get`);
864
+ }
865
+ if (reader.key !== undefined) {
866
+ readKeys.add(depKey(table, key));
867
+ } else {
868
+ readTables.add(table);
869
+ }
870
+ const raw = await reader.get(key, ctx);
871
+ const row = raw === undefined ? undefined : migrateRow(table, raw);
872
+ const rule = ruleFor(table);
873
+ return rule && row !== undefined && !rule(ctx, row) ? undefined : row;
874
+ },
875
+ where: async (table, predicate) => {
876
+ const reader = readerFor(table);
877
+ const rule = ruleFor(table);
878
+ const effective = rule ? (row) => predicate(row) && rule(ctx, row) : predicate;
879
+ const matched = [...await reader.all(ctx)].map((row) => migrateRow(table, row)).filter(effective);
880
+ if (reader.key !== undefined) {
881
+ const key = reader.key;
882
+ rangeDeps.push({
883
+ table,
884
+ predicate: effective,
885
+ keys: new Set(matched.map(key))
886
+ });
887
+ } else {
888
+ readTables.add(table);
889
+ }
890
+ return matched;
891
+ }
892
+ };
893
+ };
894
+ const writerFor = (table) => {
895
+ const writer = writers.get(table);
896
+ if (writer === undefined) {
897
+ throw new Error(`No writer registered for table "${table}" \u2014 register one with engine.registerWriter, or use actions.change`);
898
+ }
899
+ return writer;
900
+ };
901
+ const readExisting = async (table, value, ctx) => {
902
+ const reader = readers.get(table);
903
+ if (reader?.get === undefined) {
904
+ return;
905
+ }
906
+ const id = reader.key ? reader.key(value) : value.id;
907
+ return id === undefined ? undefined : reader.get(id, ctx);
908
+ };
909
+ const authorizeWrite = async (table, op, value, ctx) => {
910
+ const rule = writeRuleFor(table, op);
911
+ if (rule === undefined) {
912
+ return;
913
+ }
914
+ let subject = value;
915
+ if (op !== "insert") {
916
+ const existing = await readExisting(table, value, ctx);
917
+ if (existing !== undefined) {
918
+ subject = existing;
919
+ }
920
+ }
921
+ if (!rule(ctx, subject)) {
922
+ throw new UnauthorizedError(`${op} on table "${table}"`);
923
+ }
924
+ };
925
+ const mergeCrdtFields = async (table, op, data, ctx) => {
926
+ const fields = crdtFields.get(table);
927
+ if (fields === undefined || data === null || typeof data !== "object") {
928
+ return data;
929
+ }
930
+ const incoming = data;
931
+ const existing = op === "update" ? await readExisting(table, data, ctx) : undefined;
932
+ const base = existing !== null && typeof existing === "object" ? existing : undefined;
933
+ const merged = { ...incoming };
934
+ for (const [field, adapter] of Object.entries(fields)) {
935
+ if (incoming[field] === undefined) {
936
+ continue;
937
+ }
938
+ merged[field] = adapter.merge(base?.[field] ?? adapter.empty(), incoming[field]);
939
+ }
940
+ return merged;
941
+ };
942
+ const makeActions = (tx, ctx, enforce) => {
943
+ const buffered = [];
944
+ const actions = {
945
+ change: (collection, change) => {
946
+ buffered.push({
947
+ table: collection,
948
+ change
949
+ });
950
+ return Promise.resolve();
951
+ },
952
+ insert: async (table, data) => {
953
+ validateWrite(table, "insert", data);
954
+ if (enforce) {
955
+ await authorizeWrite(table, "insert", data, ctx);
956
+ }
957
+ const merged = await mergeCrdtFields(table, "insert", data, ctx);
958
+ const row = await writerFor(table).insert(merged, ctx, tx);
959
+ buffered.push({ table, change: { op: "insert", row } });
960
+ return row;
961
+ },
962
+ update: async (table, data) => {
963
+ validateWrite(table, "update", data);
964
+ if (enforce) {
965
+ await authorizeWrite(table, "update", data, ctx);
966
+ }
967
+ const merged = await mergeCrdtFields(table, "update", data, ctx);
968
+ const row = await writerFor(table).update(merged, ctx, tx);
969
+ buffered.push({ table, change: { op: "update", row } });
970
+ return row;
971
+ },
972
+ delete: async (table, row) => {
973
+ if (enforce) {
974
+ await authorizeWrite(table, "delete", row, ctx);
975
+ }
976
+ await writerFor(table).delete(row, ctx, tx);
977
+ buffered.push({ table, change: { op: "delete", row } });
978
+ },
979
+ now: () => Date.now()
980
+ };
981
+ return { actions, buffered };
982
+ };
983
+ const diffRerun = (sub, rows, equals = shallowEqual3) => {
984
+ const next = new Map;
985
+ for (const row of rows) {
986
+ next.set(sub.key(row), row);
987
+ }
988
+ const added = [];
989
+ const removed = [];
990
+ const changed = [];
991
+ for (const [rowKey, row] of next) {
992
+ const previous = sub.current.get(rowKey);
993
+ if (previous === undefined) {
994
+ added.push(row);
995
+ } else if (!equals(previous, row)) {
996
+ changed.push(row);
997
+ }
998
+ }
999
+ for (const [rowKey, row] of sub.current) {
1000
+ if (!next.has(rowKey)) {
1001
+ removed.push(row);
1002
+ }
1003
+ }
1004
+ sub.current = next;
1005
+ return { added, removed, changed };
1006
+ };
1007
+ const inRange = (dep, change) => dep.table === change.table && (change.key !== undefined && dep.keys.has(change.key) || dep.predicate(change.row));
1008
+ const readSetOverlaps = (readTables, readKeys, rangeDeps, changes) => changes.some((change) => readTables.has(change.table) || change.key !== undefined && readKeys.has(depKey(change.table, change.key)) || rangeDeps.some((dep) => inRange(dep, change)));
1009
+ const isReactiveAffected = (sub, changes) => readSetOverlaps(sub.readTables, sub.readKeys, sub.rangeDeps, changes);
1010
+ const invalidateCacheForChanges = (changes) => {
1011
+ if (cachedReruns.size === 0)
1012
+ return;
1013
+ for (const [key, entry] of cachedReruns) {
1014
+ if (readSetOverlaps(entry.readTables, entry.readKeys, entry.rangeDeps, changes)) {
1015
+ cachedReruns.delete(key);
1016
+ }
1017
+ }
1018
+ };
1019
+ const reactivePairs = async (changes) => {
1020
+ invalidateCacheForChanges(changes);
1021
+ const pairs = [];
1022
+ const sharedRuns = new Map;
1023
+ for (const sub of reactiveSubs) {
1024
+ if (!isReactiveAffected(sub, changes)) {
1025
+ continue;
1026
+ }
1027
+ let runPromise = sharedRuns.get(sub.rerunKey);
1028
+ if (runPromise === undefined) {
1029
+ runPromise = sub.rerun();
1030
+ sharedRuns.set(sub.rerunKey, runPromise);
1031
+ }
1032
+ const { rows, readTables, readKeys, rangeDeps } = await runPromise;
1033
+ sub.readTables = readTables;
1034
+ sub.readKeys = readKeys;
1035
+ sub.rangeDeps = rangeDeps;
1036
+ const diff = diffRerun(sub, rows);
1037
+ if (!isEmptyViewDiff(diff)) {
1038
+ pairs.push([sub, diff]);
1039
+ }
1040
+ }
1041
+ for (const [key, runPromise] of sharedRuns) {
1042
+ runPromise.then(({ rows, readTables, readKeys, rangeDeps }) => {
1043
+ writeCacheEntry({
1044
+ expiresAt: Date.now() + reactiveCacheTtlMs,
1045
+ rangeDeps,
1046
+ readKeys,
1047
+ readTables,
1048
+ rerunKey: key,
1049
+ rows,
1050
+ version
1051
+ });
1052
+ }).catch(() => {});
1053
+ }
1054
+ return pairs;
1055
+ };
1056
+ const ensureSearchIndex = async (definition) => {
1057
+ let entry = searchIndexes.get(definition.name);
1058
+ if (entry === undefined) {
1059
+ entry = { index: definition.index(), definition, hydrated: false };
1060
+ searchIndexes.set(definition.name, entry);
1061
+ }
1062
+ if (!entry.hydrated) {
1063
+ for (const row of await definition.source()) {
1064
+ entry.index.add(row);
1065
+ }
1066
+ entry.hydrated = true;
1067
+ }
1068
+ return entry;
1069
+ };
1070
+ const searchPairs = (changes) => {
1071
+ const touched = new Set;
1072
+ for (const { table, change } of changes) {
1073
+ for (const entry of searchIndexes.values()) {
1074
+ if (!entry.hydrated || entry.definition.table !== table) {
1075
+ continue;
1076
+ }
1077
+ if (change.op === "delete") {
1078
+ entry.index.remove(entry.definition.key(change.row));
1079
+ } else {
1080
+ entry.index.add(change.row);
1081
+ }
1082
+ touched.add(entry.definition.name);
1083
+ }
1084
+ }
1085
+ const pairs = [];
1086
+ for (const sub of searchSubs) {
1087
+ if (!touched.has(sub.collection)) {
1088
+ continue;
1089
+ }
1090
+ const diff = diffRerun(sub, sub.rerun(), equalsIgnoringScore);
1091
+ if (!isEmptyViewDiff(diff)) {
1092
+ pairs.push([sub, diff]);
1093
+ }
1094
+ }
1095
+ return pairs;
1096
+ };
1097
+ const logChange = (changeVersion, entry) => {
1098
+ changeLog.push(entry);
1099
+ if (changeLog.length > changeLogSize) {
1100
+ changeLog.shift();
1101
+ }
1102
+ for (const subscriber of streamSubscribers) {
1103
+ subscriber(entry);
1104
+ }
1105
+ };
1106
+ const applyChange = async (table, change, shouldBroadcast = true) => {
1107
+ version += 1;
1108
+ const changeVersion = version;
1109
+ logChange(changeVersion, { version: changeVersion, table, change });
1110
+ emitActivity({
1111
+ type: "change",
1112
+ at: Date.now(),
1113
+ table,
1114
+ op: change.op,
1115
+ version: changeVersion
1116
+ });
1117
+ const emissions = [];
1118
+ for (const subscription of subscriptionsForTable(table)) {
1119
+ const diff = await subscriptionDiff(subscription, table, change);
1120
+ if (!isEmptyViewDiff(diff)) {
1121
+ emissions.push([subscription, diff]);
1122
+ }
1123
+ }
1124
+ emissions.push(...await reactivePairs([
1125
+ { table, key: changedKeyFor(table, change), row: change.row }
1126
+ ]));
1127
+ emissions.push(...searchPairs([{ table, change }]));
1128
+ for (const [subscription, diff] of emissions) {
1129
+ subscription.onDiff(diff, changeVersion);
1130
+ }
1131
+ if (shouldBroadcast) {
1132
+ broadcast([{ table, change }]);
1133
+ }
1134
+ };
1135
+ const applyChangeBatch = async (changes, shouldBroadcast = true) => {
1136
+ if (changes.length === 0) {
1137
+ return;
1138
+ }
1139
+ version += 1;
1140
+ const batchVersion = version;
1141
+ const perSubscription = new Map;
1142
+ const reactiveChanges = [];
1143
+ for (const { table, change } of changes) {
1144
+ logChange(batchVersion, { version: batchVersion, table, change });
1145
+ emitActivity({
1146
+ type: "change",
1147
+ at: Date.now(),
1148
+ table,
1149
+ op: change.op,
1150
+ version: batchVersion
1151
+ });
1152
+ reactiveChanges.push({
1153
+ table,
1154
+ key: changedKeyFor(table, change),
1155
+ row: change.row
1156
+ });
1157
+ for (const subscription of subscriptionsForTable(table)) {
1158
+ const diff = await subscriptionDiff(subscription, table, change);
1159
+ const list = perSubscription.get(subscription);
1160
+ if (list === undefined) {
1161
+ perSubscription.set(subscription, [diff]);
1162
+ } else {
1163
+ list.push(diff);
1164
+ }
1165
+ }
1166
+ }
1167
+ const emissions = [];
1168
+ for (const [subscription, diffs] of perSubscription) {
1169
+ const merged = diffs.length === 1 ? diffs[0] : mergeViewDiffs(diffs, subscription.key);
1170
+ if (!isEmptyViewDiff(merged)) {
1171
+ emissions.push([subscription, merged]);
1172
+ }
1173
+ }
1174
+ emissions.push(...await reactivePairs(reactiveChanges));
1175
+ emissions.push(...searchPairs(changes));
1176
+ for (const [subscription, diff] of emissions) {
1177
+ subscription.onDiff(diff, batchVersion);
1178
+ }
1179
+ if (shouldBroadcast) {
1180
+ broadcast(changes);
1181
+ }
1182
+ };
1183
+ const canResume = (since, incremental) => {
1184
+ if (!incremental) {
1185
+ return false;
1186
+ }
1187
+ if (since >= version) {
1188
+ return true;
1189
+ }
1190
+ const oldest = changeLog[0];
1191
+ return oldest !== undefined && oldest.version <= since + 1;
1192
+ };
1193
+ const buildCatchup = (since, tables, key, match) => {
1194
+ const latest = new Map;
1195
+ for (const entry of changeLog) {
1196
+ if (entry.version <= since || !tables.includes(entry.table)) {
1197
+ continue;
1198
+ }
1199
+ const row = entry.change.row;
1200
+ const present = entry.change.op !== "delete" && match(row) ? "upsert" : "remove";
1201
+ latest.set(key(row), { op: present, row });
1202
+ }
1203
+ const changed = [];
1204
+ const removed = [];
1205
+ for (const { op, row } of latest.values()) {
1206
+ (op === "upsert" ? changed : removed).push(row);
1207
+ }
1208
+ return { added: [], removed, changed };
1209
+ };
1210
+ const subscribeJoin = async (collection, definition, params, ctx, onDiff, set) => {
1211
+ if (definition.authorize !== undefined) {
1212
+ const allowed = await definition.authorize(params, ctx);
1213
+ if (!allowed) {
1214
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1215
+ }
1216
+ }
1217
+ const { left, right } = definition;
1218
+ const op = createEquiJoin({
1219
+ leftKey: left.key,
1220
+ rightKey: right.key,
1221
+ leftOn: left.on,
1222
+ rightOn: right.on,
1223
+ select: definition.select
1224
+ });
1225
+ op.hydrate([...await left.hydrate(params, ctx)], [...await right.hydrate(params, ctx)]);
1226
+ const atVersion = version;
1227
+ const subscription = {
1228
+ kind: "join",
1229
+ collection,
1230
+ join: {
1231
+ op,
1232
+ leftTable: left.table,
1233
+ rightTable: right.table,
1234
+ leftMatch: left.match ? (row) => left.match(row, params, ctx) : undefined,
1235
+ rightMatch: right.match ? (row) => right.match(row, params, ctx) : undefined
1236
+ },
1237
+ key: definition.key,
1238
+ onDiff
1239
+ };
1240
+ set.add(subscription);
1241
+ return {
1242
+ initial: op.rows(),
1243
+ version: atVersion,
1244
+ unsubscribe: () => {
1245
+ set.delete(subscription);
1246
+ }
1247
+ };
1248
+ };
1249
+ const subscribeGraph = async (collection, definition, params, ctx, onDiff, set) => {
1250
+ if (definition.authorize !== undefined) {
1251
+ const allowed = await definition.authorize(params, ctx);
1252
+ if (!allowed) {
1253
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1254
+ }
1255
+ }
1256
+ const instance = definition.query.instantiate(params, ctx);
1257
+ const initial = await instance.hydrate();
1258
+ const atVersion = version;
1259
+ const subscription = {
1260
+ kind: "graph",
1261
+ collection,
1262
+ instance,
1263
+ key: definition.key,
1264
+ onDiff
1265
+ };
1266
+ set.add(subscription);
1267
+ return {
1268
+ initial,
1269
+ version: atVersion,
1270
+ unsubscribe: () => {
1271
+ set.delete(subscription);
1272
+ }
1273
+ };
1274
+ };
1275
+ const subscribeReactive = async (collection, definition, params, ctx, onDiff, set) => {
1276
+ if (definition.authorize !== undefined) {
1277
+ const allowed = await definition.authorize(params, ctx);
1278
+ if (!allowed) {
1279
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1280
+ }
1281
+ }
1282
+ const rerun = async () => {
1283
+ const readTables = new Set;
1284
+ const readKeys = new Set;
1285
+ const rangeDeps = [];
1286
+ const db = makeReadHandle(ctx, readTables, readKeys, rangeDeps);
1287
+ const rows = [...await definition.run({ ctx, db, params })];
1288
+ return { rangeDeps, readKeys, readTables, rows };
1289
+ };
1290
+ const rerunKey = stableSubKey(collection, params, ctx);
1291
+ const cached = readCacheEntry(rerunKey);
1292
+ const first = cached !== undefined ? {
1293
+ rangeDeps: cached.rangeDeps,
1294
+ readKeys: cached.readKeys,
1295
+ readTables: cached.readTables,
1296
+ rows: cached.rows
1297
+ } : await rerun();
1298
+ if (cached === undefined) {
1299
+ writeCacheEntry({
1300
+ expiresAt: Date.now() + reactiveCacheTtlMs,
1301
+ rangeDeps: first.rangeDeps,
1302
+ readKeys: first.readKeys,
1303
+ readTables: first.readTables,
1304
+ rerunKey,
1305
+ rows: first.rows,
1306
+ version
1307
+ });
1308
+ }
1309
+ const current = new Map;
1310
+ for (const row of first.rows) {
1311
+ current.set(definition.key(row), row);
1312
+ }
1313
+ const atVersion = version;
1314
+ const subscription = {
1315
+ kind: "reactive",
1316
+ collection,
1317
+ key: definition.key,
1318
+ rerun,
1319
+ rerunKey,
1320
+ current,
1321
+ readTables: first.readTables,
1322
+ readKeys: first.readKeys,
1323
+ rangeDeps: first.rangeDeps,
1324
+ onDiff
1325
+ };
1326
+ set.add(subscription);
1327
+ reactiveSubs.add(subscription);
1328
+ return {
1329
+ initial: first.rows,
1330
+ version: atVersion,
1331
+ unsubscribe: () => {
1332
+ set.delete(subscription);
1333
+ reactiveSubs.delete(subscription);
1334
+ }
1335
+ };
1336
+ };
1337
+ const subscribeSearch = async (collection, definition, params, ctx, onDiff, set) => {
1338
+ const query = params;
1339
+ if (definition.authorize !== undefined) {
1340
+ const allowed = await definition.authorize(query, ctx);
1341
+ if (!allowed) {
1342
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1343
+ }
1344
+ }
1345
+ const entry = await ensureSearchIndex(definition);
1346
+ const limit = definition.limit ?? 20;
1347
+ const readRule = readRuleFor(definition.table);
1348
+ const rerun = () => {
1349
+ const candidates = entry.index.search(query, readRule ? limit * 5 : limit);
1350
+ const visible = readRule ? candidates.filter((hit) => readRule(ctx, hit.row)) : candidates;
1351
+ return visible.slice(0, limit).map((hit) => ({
1352
+ ...hit.row,
1353
+ [SEARCH_SCORE_FIELD]: hit.score
1354
+ }));
1355
+ };
1356
+ const initial = rerun();
1357
+ const current = new Map;
1358
+ for (const row of initial) {
1359
+ current.set(definition.key(row), row);
1360
+ }
1361
+ const atVersion = version;
1362
+ const subscription = {
1363
+ kind: "search",
1364
+ collection,
1365
+ key: definition.key,
1366
+ rerun,
1367
+ current,
1368
+ onDiff
1369
+ };
1370
+ set.add(subscription);
1371
+ searchSubs.add(subscription);
1372
+ return {
1373
+ initial,
1374
+ version: atVersion,
1375
+ unsubscribe: () => {
1376
+ set.delete(subscription);
1377
+ searchSubs.delete(subscription);
1378
+ }
1379
+ };
1380
+ };
1381
+ const engine = {
1382
+ register: (collection) => {
1383
+ registry.set(collection.name, collection);
1384
+ for (const table of collection.tables ?? [collection.name]) {
1385
+ addTableIndex(table, collection.name);
1386
+ }
1387
+ },
1388
+ registerJoin: (collection) => {
1389
+ registry.set(collection.name, collection);
1390
+ addTableIndex(collection.left.table, collection.name);
1391
+ addTableIndex(collection.right.table, collection.name);
1392
+ },
1393
+ registerGraph: (collection) => {
1394
+ registry.set(collection.name, collection);
1395
+ for (const table of collection.query.tables()) {
1396
+ addTableIndex(table, collection.name);
1397
+ }
1398
+ },
1399
+ registerSearch: (collection) => {
1400
+ registry.set(collection.name, collection);
1401
+ },
1402
+ subscribe: async ({ collection, params, ctx, onDiff, since }) => {
1403
+ const registered = registry.get(collection);
1404
+ if (registered === undefined) {
1405
+ throw new Error(`Unknown collection "${collection}"`);
1406
+ }
1407
+ const typedOnDiff = onDiff;
1408
+ const subscribeSet = subsFor(collection);
1409
+ const registeredKind = registered.kind;
1410
+ if (registeredKind === "join") {
1411
+ const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1412
+ return joined;
1413
+ }
1414
+ if (registeredKind === "graph") {
1415
+ const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1416
+ return graphed;
1417
+ }
1418
+ if (registeredKind === "reactive") {
1419
+ const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1420
+ return reactived;
1421
+ }
1422
+ if (registeredKind === "search") {
1423
+ const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1424
+ return searched;
1425
+ }
1426
+ const definition = registered;
1427
+ if (definition.authorize !== undefined) {
1428
+ const allowed = await definition.authorize(params, ctx);
1429
+ if (!allowed) {
1430
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1431
+ }
1432
+ }
1433
+ const key = definition.key ?? defaultKey;
1434
+ const match = definition.match;
1435
+ const tables = definition.tables ?? [collection];
1436
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
1437
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1438
+ const rehydrate = async () => {
1439
+ const raw = [...await definition.hydrate(params, ctx)];
1440
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1441
+ return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1442
+ };
1443
+ const incremental = match !== undefined && tables.length === 1;
1444
+ const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
1445
+ const view = createMaterializedView({
1446
+ key,
1447
+ match: boundMatch
1448
+ });
1449
+ const resuming = since !== undefined && canResume(since, incremental);
1450
+ view.hydrate([...await rehydrate()]);
1451
+ const atVersion = version;
1452
+ const subscription = {
1453
+ kind: "view",
1454
+ collection,
1455
+ view,
1456
+ incremental,
1457
+ rehydrate,
1458
+ key,
1459
+ onDiff: typedOnDiff
1460
+ };
1461
+ subscribeSet.add(subscription);
1462
+ const unsubscribe = () => {
1463
+ subscribeSet.delete(subscription);
1464
+ };
1465
+ if (resuming) {
1466
+ return {
1467
+ initial: [],
1468
+ catchup: buildCatchup(since, tables, key, boundMatch),
1469
+ version: atVersion,
1470
+ unsubscribe
1471
+ };
1472
+ }
1473
+ return {
1474
+ initial: view.rows(),
1475
+ version: atVersion,
1476
+ unsubscribe
1477
+ };
1478
+ },
1479
+ hydrate: async (collection, params, ctx) => {
1480
+ const definition = registry.get(collection);
1481
+ if (definition === undefined) {
1482
+ throw new Error(`Unknown collection "${collection}"`);
1483
+ }
1484
+ if (definition.authorize !== undefined) {
1485
+ const allowed = await definition.authorize(params, ctx);
1486
+ if (!allowed) {
1487
+ throw new UnauthorizedError(`hydrate collection "${collection}"`);
1488
+ }
1489
+ }
1490
+ const raw = [...await definition.hydrate(params, ctx)];
1491
+ const tables = definition.tables ?? [collection];
1492
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
1493
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1494
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1495
+ return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1496
+ },
1497
+ applyChange: (table, change) => applyChange(table, change),
1498
+ connectSource: async (source) => {
1499
+ await source.start((table, change) => applyChange(table, change));
1500
+ return async () => {
1501
+ await source.stop();
1502
+ };
1503
+ },
1504
+ connectCluster: async (bus) => {
1505
+ const unsubscribe = await bus.subscribe((message) => {
1506
+ if (message.origin === instanceId) {
1507
+ return;
1508
+ }
1509
+ applyChangeBatch(message.changes, false);
1510
+ });
1511
+ clusterBus = bus;
1512
+ return async () => {
1513
+ clusterBus = undefined;
1514
+ await unsubscribe();
1515
+ };
1516
+ },
1517
+ subscriptionCount: (collection) => {
1518
+ if (collection !== undefined) {
1519
+ return active.get(collection)?.size ?? 0;
1520
+ }
1521
+ let total = 0;
1522
+ for (const set of active.values()) {
1523
+ total += set.size;
1524
+ }
1525
+ return total;
1526
+ },
1527
+ registerMutation: (mutation) => {
1528
+ if (mutation.handler === undefined && mutation.sandboxedHandler === undefined) {
1529
+ throw new Error(`Mutation "${mutation.name}" must define either \`handler\` or \`sandboxedHandler\``);
1530
+ }
1531
+ if (mutation.handler !== undefined && mutation.sandboxedHandler !== undefined) {
1532
+ throw new Error(`Mutation "${mutation.name}" defines both \`handler\` and \`sandboxedHandler\` \u2014 pick one`);
1533
+ }
1534
+ mutations.set(mutation.name, mutation);
1535
+ if (mutation.sandboxedHandler !== undefined) {
1536
+ sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox, {
1537
+ bridgeFetch: options.bridgeFetch,
1538
+ metricsHook: options.handlerMetrics === undefined ? undefined : {
1539
+ mutationName: mutation.name,
1540
+ onMetrics: options.handlerMetrics
1541
+ }
1542
+ }));
1543
+ }
1544
+ },
1545
+ registerWriter: (table, writer) => {
1546
+ writers.set(table, writer);
1547
+ },
1548
+ registerReactive: (query) => {
1549
+ registry.set(query.name, query);
1550
+ },
1551
+ registerReader: (table, reader) => {
1552
+ readers.set(table, reader);
1553
+ },
1554
+ registerPermissions: (table, rules) => {
1555
+ permissions.set(table, rules);
1556
+ },
1557
+ registerSchema: (table, schema) => {
1558
+ schemas.set(table, schema);
1559
+ },
1560
+ registerCrdt: (table, fields) => {
1561
+ crdtFields.set(table, fields);
1562
+ const name = `${table}:merge`;
1563
+ mutations.set(name, {
1564
+ handler: async (args, ctx, actions) => {
1565
+ const existing = await readExisting(table, args, ctx);
1566
+ return existing === undefined ? actions.insert(table, args) : actions.update(table, args);
1567
+ },
1568
+ name
1569
+ });
1570
+ },
1571
+ migrate: (table, row) => migrateRow(table, row),
1572
+ runMutation: async (name, args, ctx) => {
1573
+ const mutation = mutations.get(name);
1574
+ if (mutation === undefined) {
1575
+ throw new Error(`Unknown mutation "${name}"`);
1576
+ }
1577
+ if (mutation.authorize !== undefined) {
1578
+ const allowed = await mutation.authorize(args, ctx);
1579
+ if (!allowed) {
1580
+ throw new UnauthorizedError(`run mutation "${name}"`);
1581
+ }
1582
+ }
1583
+ const sandboxRunner = sandboxRunners.get(name);
1584
+ const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
1585
+ const runHandler = async (tx) => {
1586
+ const { actions, buffered } = makeActions(tx, ctx, true);
1587
+ const result = await invokeHandler(args, ctx, actions);
1588
+ return { buffered, result };
1589
+ };
1590
+ const retry = mutation.retry;
1591
+ const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
1592
+ const isRetryable = retry?.isRetryable ?? isSerializationFailure;
1593
+ const computeDelay = retry?.backoff ?? exponentialBackoff();
1594
+ const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
1595
+ const startedAt = Date.now();
1596
+ let lastError;
1597
+ let attemptsMade = 0;
1598
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
1599
+ attemptsMade = attempt;
1600
+ try {
1601
+ const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1602
+ await applyChangeBatch(buffered);
1603
+ emitActivity({
1604
+ type: "mutation",
1605
+ at: Date.now(),
1606
+ name,
1607
+ status: "ok"
1608
+ });
1609
+ return result;
1610
+ } catch (error) {
1611
+ lastError = error;
1612
+ const elapsedMs = Date.now() - startedAt;
1613
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
1614
+ if (!canRetry)
1615
+ break;
1616
+ const rawDelay = computeDelay(attempt);
1617
+ const remaining = maxElapsedMs - elapsedMs;
1618
+ if (remaining <= 0)
1619
+ break;
1620
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
1621
+ emitActivity({
1622
+ type: "mutationRetry",
1623
+ at: Date.now(),
1624
+ name,
1625
+ attempt,
1626
+ delayMs,
1627
+ errorName: error instanceof Error ? error.name : "Error",
1628
+ errorMessage: error instanceof Error ? error.message : String(error)
1629
+ });
1630
+ if (delayMs > 0) {
1631
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1632
+ }
1633
+ }
1634
+ }
1635
+ emitActivity({
1636
+ type: "mutation",
1637
+ at: Date.now(),
1638
+ name,
1639
+ status: "error"
1640
+ });
1641
+ if (attemptsMade > 1) {
1642
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
1643
+ }
1644
+ throw lastError;
1645
+ },
1646
+ registerSchedule: (schedule) => {
1647
+ schedules.set(schedule.name, schedule);
1648
+ },
1649
+ listSchedules: () => [...schedules.values()],
1650
+ runSchedule: async (name) => {
1651
+ const schedule = schedules.get(name);
1652
+ if (schedule === undefined) {
1653
+ throw new Error(`Unknown schedule "${name}"`);
1654
+ }
1655
+ const runHandler = async (tx) => {
1656
+ const { actions, buffered } = makeActions(tx, {}, false);
1657
+ const db = makeReadHandle({}, new Set, new Set, [], false);
1658
+ await schedule.run({ actions, db });
1659
+ return buffered;
1660
+ };
1661
+ const retry = schedule.retry;
1662
+ const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
1663
+ const isRetryable = retry?.isRetryable ?? isSerializationFailure;
1664
+ const computeDelay = retry?.backoff ?? exponentialBackoff();
1665
+ const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
1666
+ const startedAt = Date.now();
1667
+ let lastError;
1668
+ let attemptsMade = 0;
1669
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
1670
+ attemptsMade = attempt;
1671
+ try {
1672
+ const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1673
+ await applyChangeBatch(buffered);
1674
+ emitActivity({
1675
+ type: "schedule",
1676
+ at: Date.now(),
1677
+ name,
1678
+ status: "ok"
1679
+ });
1680
+ return;
1681
+ } catch (error) {
1682
+ lastError = error;
1683
+ const elapsedMs = Date.now() - startedAt;
1684
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
1685
+ if (!canRetry)
1686
+ break;
1687
+ const rawDelay = computeDelay(attempt);
1688
+ const remaining = maxElapsedMs - elapsedMs;
1689
+ if (remaining <= 0)
1690
+ break;
1691
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
1692
+ emitActivity({
1693
+ type: "scheduleRetry",
1694
+ at: Date.now(),
1695
+ name,
1696
+ attempt,
1697
+ delayMs,
1698
+ errorName: error instanceof Error ? error.name : "Error",
1699
+ errorMessage: error instanceof Error ? error.message : String(error)
1700
+ });
1701
+ if (delayMs > 0) {
1702
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1703
+ }
1704
+ }
1705
+ }
1706
+ emitActivity({
1707
+ type: "schedule",
1708
+ at: Date.now(),
1709
+ name,
1710
+ status: "error"
1711
+ });
1712
+ if (attemptsMade > 1) {
1713
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
1714
+ }
1715
+ throw lastError;
1716
+ },
1717
+ registerPack: (pack) => {
1718
+ for (const table of pack.ownsTables) {
1719
+ const existing = packTableOwners.get(table);
1720
+ if (existing !== undefined) {
1721
+ throw new PackTableConflictError(table, existing, pack.name);
1722
+ }
1723
+ }
1724
+ if (pack.requireDependencies === true) {
1725
+ for (const table of pack.readsTables ?? []) {
1726
+ if (!readers.has(table)) {
1727
+ throw new PackMissingDependencyError(pack.name, table);
1728
+ }
1729
+ }
1730
+ }
1731
+ if (pack.schemas !== undefined) {
1732
+ for (const [table, schema] of Object.entries(pack.schemas)) {
1733
+ engine.registerSchema(table, schema);
1734
+ }
1735
+ }
1736
+ if (pack.permissions !== undefined) {
1737
+ for (const [table, rules] of Object.entries(pack.permissions)) {
1738
+ engine.registerPermissions(table, rules);
1739
+ }
1740
+ }
1741
+ if (pack.readers !== undefined) {
1742
+ for (const [table, reader] of Object.entries(pack.readers)) {
1743
+ engine.registerReader(table, reader);
1744
+ }
1745
+ }
1746
+ if (pack.writers !== undefined) {
1747
+ for (const [table, writer] of Object.entries(pack.writers)) {
1748
+ engine.registerWriter(table, writer);
1749
+ }
1750
+ }
1751
+ if (pack.crdt !== undefined) {
1752
+ for (const [table, fields] of Object.entries(pack.crdt)) {
1753
+ engine.registerCrdt(table, fields);
1754
+ }
1755
+ }
1756
+ for (const collection of pack.collections ?? []) {
1757
+ engine.register(collection);
1758
+ }
1759
+ for (const collection of pack.joinCollections ?? []) {
1760
+ engine.registerJoin(collection);
1761
+ }
1762
+ for (const collection of pack.graphCollections ?? []) {
1763
+ engine.registerGraph(collection);
1764
+ }
1765
+ for (const collection of pack.searchCollections ?? []) {
1766
+ engine.registerSearch(collection);
1767
+ }
1768
+ for (const query of pack.reactiveQueries ?? []) {
1769
+ engine.registerReactive(query);
1770
+ }
1771
+ for (const mutation of pack.mutations ?? []) {
1772
+ engine.registerMutation(mutation);
1773
+ }
1774
+ for (const schedule of pack.schedules ?? []) {
1775
+ engine.registerSchedule(schedule);
1776
+ }
1777
+ for (const table of pack.ownsTables) {
1778
+ packTableOwners.set(table, pack.name);
1779
+ }
1780
+ registeredPacks.push({
1781
+ name: pack.name,
1782
+ version: pack.version,
1783
+ ownsTables: [...pack.ownsTables],
1784
+ readsTables: [...pack.readsTables ?? []]
1785
+ });
1786
+ },
1787
+ inspect: () => {
1788
+ const collections = [...registry.entries()].map(([name, def]) => {
1789
+ const kind = def.kind ?? "view";
1790
+ let tables = [];
1791
+ if (kind === "join") {
1792
+ const join = def;
1793
+ tables = [join.left.table, join.right.table];
1794
+ } else if (kind === "graph") {
1795
+ tables = def.query.tables();
1796
+ } else if (kind === "search") {
1797
+ tables = [
1798
+ def.table
1799
+ ];
1800
+ } else if (kind === "view") {
1801
+ tables = def.tables ?? [name];
1802
+ }
1803
+ return {
1804
+ name,
1805
+ kind,
1806
+ tables,
1807
+ subscriptions: active.get(name)?.size ?? 0
1808
+ };
1809
+ });
1810
+ const DEVTOOLS_RECENT = 50;
1811
+ return {
1812
+ version,
1813
+ collections,
1814
+ mutations: [...mutations.keys()],
1815
+ schedules: [...schedules.values()].map((schedule) => ({
1816
+ name: schedule.name,
1817
+ pattern: schedule.pattern
1818
+ })),
1819
+ readers: [...readers.keys()],
1820
+ writers: [...writers.keys()],
1821
+ recentChanges: changeLog.slice(-DEVTOOLS_RECENT).map((entry) => ({
1822
+ version: entry.version,
1823
+ table: entry.table,
1824
+ op: entry.change.op
1825
+ })),
1826
+ packs: registeredPacks.map((pack) => ({
1827
+ name: pack.name,
1828
+ version: pack.version,
1829
+ ownsTables: [...pack.ownsTables],
1830
+ readsTables: [...pack.readsTables]
1831
+ }))
1832
+ };
1833
+ },
1834
+ onActivity: (listener) => {
1835
+ activityListeners.add(listener);
1836
+ return () => {
1837
+ activityListeners.delete(listener);
1838
+ };
1839
+ },
1840
+ streamChanges: ({
1841
+ since = 0,
1842
+ signal,
1843
+ maxBuffer = 1e4
1844
+ } = {}) => {
1845
+ const oldest = changeLog[0];
1846
+ if (since > 0 && oldest !== undefined && oldest.version > since + 1) {
1847
+ const err = new MissedChangesError(since, oldest.version);
1848
+ return {
1849
+ [Symbol.asyncIterator]() {
1850
+ return {
1851
+ next: () => Promise.reject(err)
1852
+ };
1853
+ }
1854
+ };
1855
+ }
1856
+ const buffer = [];
1857
+ let waiter = null;
1858
+ let overflow = false;
1859
+ const wake = () => {
1860
+ if (waiter !== null) {
1861
+ const resume = waiter;
1862
+ waiter = null;
1863
+ resume();
1864
+ }
1865
+ };
1866
+ const subscriber = (entry) => {
1867
+ if (buffer.length >= maxBuffer) {
1868
+ overflow = true;
1869
+ wake();
1870
+ return;
1871
+ }
1872
+ buffer.push(entry);
1873
+ wake();
1874
+ };
1875
+ streamSubscribers.add(subscriber);
1876
+ const onAbort = () => wake();
1877
+ signal?.addEventListener("abort", onAbort, { once: true });
1878
+ let lastDelivered = since;
1879
+ return {
1880
+ async* [Symbol.asyncIterator]() {
1881
+ try {
1882
+ const history = [...changeLog];
1883
+ const headVersion = history.length > 0 ? history[history.length - 1].version : since;
1884
+ for (const entry of history) {
1885
+ if (signal?.aborted)
1886
+ return;
1887
+ if (entry.version > since) {
1888
+ lastDelivered = entry.version;
1889
+ yield entry;
1890
+ }
1891
+ }
1892
+ while (!signal?.aborted) {
1893
+ while (buffer.length > 0) {
1894
+ const entry = buffer.shift();
1895
+ if (entry.version > headVersion) {
1896
+ lastDelivered = entry.version;
1897
+ yield entry;
1898
+ }
1899
+ }
1900
+ if (overflow) {
1901
+ throw new CdcConsumerSlowError(maxBuffer, lastDelivered);
1902
+ }
1903
+ if (signal?.aborted)
1904
+ return;
1905
+ await new Promise((resolve) => {
1906
+ waiter = resolve;
1907
+ });
1908
+ }
1909
+ } finally {
1910
+ streamSubscribers.delete(subscriber);
1911
+ signal?.removeEventListener("abort", onAbort);
1912
+ }
1913
+ }
1914
+ };
1915
+ }
1916
+ };
1917
+ return engine;
1918
+ };
1919
+
1920
+ // src/testing.ts
1921
+ var createTestEngine = (options) => createSyncEngine(options);
1922
+ var expectRejection = async (work) => {
1923
+ try {
1924
+ await work();
1925
+ } catch (error) {
1926
+ return error;
1927
+ }
1928
+ throw new Error("expected `work` to reject but it resolved");
1929
+ };
1930
+ var runAsActor = (engine, actorId, mutation, args, extraCtx) => engine.runMutation(mutation, args, { ...extraCtx, userId: actorId });
1931
+ export {
1932
+ runAsActor,
1933
+ expectRejection,
1934
+ createTestEngine
1935
+ };
1936
+
1937
+ //# debugId=CDFC0D23D33F4AF664756E2164756E21
1938
+ //# sourceMappingURL=testing.js.map