@absolutejs/sync 2.3.0 → 2.5.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.
- package/README.md +26 -0
- package/dist/adapters/drizzle/index.js +2 -2
- package/dist/adapters/drizzle/index.js.map +3 -3
- package/dist/index.js +3 -3
- package/dist/index.js.map +3 -3
- package/dist/manifest.js +2 -2552
- package/dist/manifest.js.map +3 -115
- package/dist/platform.d.ts +139 -0
- package/dist/platform.js +3162 -0
- package/dist/platform.js.map +24 -0
- package/dist/plugin.d.ts +1 -1
- package/package.json +7 -2
package/dist/platform.js
ADDED
|
@@ -0,0 +1,3162 @@
|
|
|
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/platform.ts
|
|
19
|
+
import { Type } from "@sinclair/typebox";
|
|
20
|
+
import { Value } from "@sinclair/typebox/value";
|
|
21
|
+
import { Elysia as Elysia3 } from "elysia";
|
|
22
|
+
|
|
23
|
+
// node_modules/@absolutejs/telemetry/dist/index.js
|
|
24
|
+
var NOOP_SPAN_CONTEXT = {
|
|
25
|
+
spanId: "0000000000000000",
|
|
26
|
+
traceFlags: 0,
|
|
27
|
+
traceId: "00000000000000000000000000000000"
|
|
28
|
+
};
|
|
29
|
+
var noopSpan = {
|
|
30
|
+
addEvent: () => noopSpan,
|
|
31
|
+
end: () => {},
|
|
32
|
+
isRecording: () => false,
|
|
33
|
+
recordException: () => {},
|
|
34
|
+
setAttribute: () => noopSpan,
|
|
35
|
+
setAttributes: () => noopSpan,
|
|
36
|
+
setStatus: () => noopSpan,
|
|
37
|
+
spanContext: () => NOOP_SPAN_CONTEXT,
|
|
38
|
+
updateName: () => noopSpan
|
|
39
|
+
};
|
|
40
|
+
var startActiveSpanNoop = (_name, optionsOrFn, maybeFn) => {
|
|
41
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
42
|
+
return fn(noopSpan);
|
|
43
|
+
};
|
|
44
|
+
var noopTracer = {
|
|
45
|
+
startActiveSpan: startActiveSpanNoop,
|
|
46
|
+
startSpan: () => noopSpan
|
|
47
|
+
};
|
|
48
|
+
var tracerOrNoop = (provider, name, version) => provider !== undefined ? provider.getTracer(name, version) : noopTracer;
|
|
49
|
+
var ABS_ATTRS = {
|
|
50
|
+
tenant: "abs.tenant",
|
|
51
|
+
shardId: "abs.shard.id",
|
|
52
|
+
engineId: "abs.engine.id",
|
|
53
|
+
collection: "abs.collection",
|
|
54
|
+
mutation: "abs.mutation",
|
|
55
|
+
mutationAttempt: "abs.mutation.attempt",
|
|
56
|
+
subscriptionId: "abs.subscription.id",
|
|
57
|
+
batchSize: "abs.batch.size",
|
|
58
|
+
clusterMessageOrigin: "abs.cluster.origin",
|
|
59
|
+
jobId: "abs.job.id",
|
|
60
|
+
jobKind: "abs.job.kind",
|
|
61
|
+
jobAttempt: "abs.job.attempt",
|
|
62
|
+
jobMaxAttempts: "abs.job.max_attempts",
|
|
63
|
+
workerId: "abs.worker.id",
|
|
64
|
+
runtimeKey: "abs.runtime.key",
|
|
65
|
+
runtimePid: "abs.runtime.pid",
|
|
66
|
+
runtimePort: "abs.runtime.port",
|
|
67
|
+
runtimeExitReason: "abs.runtime.exit_reason",
|
|
68
|
+
runtimeReadinessMs: "abs.runtime.readiness_ms",
|
|
69
|
+
routeShard: "abs.route.shard",
|
|
70
|
+
routeDecision: "abs.route.decision",
|
|
71
|
+
secretName: "abs.secret.name",
|
|
72
|
+
secretFingerprint: "abs.secret.fingerprint",
|
|
73
|
+
auditKind: "abs.audit.kind"
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/engine/equiJoin.ts
|
|
77
|
+
var shallowEqual = (a, b) => {
|
|
78
|
+
if (a === b) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const aKeys = Object.keys(a);
|
|
85
|
+
const bKeys = Object.keys(b);
|
|
86
|
+
return aKeys.length === bKeys.length && aKeys.every((key) => a[key] === b[key]);
|
|
87
|
+
};
|
|
88
|
+
var addToIndex = (index, joinValue, key) => {
|
|
89
|
+
let bucket = index.get(joinValue);
|
|
90
|
+
if (bucket === undefined) {
|
|
91
|
+
bucket = new Set;
|
|
92
|
+
index.set(joinValue, bucket);
|
|
93
|
+
}
|
|
94
|
+
bucket.add(key);
|
|
95
|
+
};
|
|
96
|
+
var removeFromIndex = (index, joinValue, key) => {
|
|
97
|
+
const bucket = index.get(joinValue);
|
|
98
|
+
if (bucket === undefined) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
bucket.delete(key);
|
|
102
|
+
if (bucket.size === 0) {
|
|
103
|
+
index.delete(joinValue);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var createEquiJoin = (options) => {
|
|
107
|
+
const { leftKey, rightKey, leftOn, rightOn, select, selectUnmatched } = options;
|
|
108
|
+
const equals = options.equals ?? shallowEqual;
|
|
109
|
+
const lefts = new Map;
|
|
110
|
+
const rights = new Map;
|
|
111
|
+
const leftByJoin = new Map;
|
|
112
|
+
const rightByJoin = new Map;
|
|
113
|
+
const output = new Map;
|
|
114
|
+
const outByLeft = new Map;
|
|
115
|
+
const outKey = (lk, rk) => `${lk} ${rk}`;
|
|
116
|
+
const unmatchedKey = (lk) => `${lk} ~`;
|
|
117
|
+
const leftOutputs = (lk, left) => {
|
|
118
|
+
const result = new Map;
|
|
119
|
+
const rks = rightByJoin.get(leftOn(left));
|
|
120
|
+
if (rks !== undefined && rks.size > 0) {
|
|
121
|
+
for (const rk of rks) {
|
|
122
|
+
const right = rights.get(rk);
|
|
123
|
+
if (right !== undefined) {
|
|
124
|
+
result.set(outKey(lk, rk), select(left, right));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} else if (selectUnmatched !== undefined) {
|
|
128
|
+
result.set(unmatchedKey(lk), selectUnmatched(left));
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
const reconcileLeft = (lk, after) => {
|
|
133
|
+
const before = outByLeft.get(lk) ?? new Set;
|
|
134
|
+
const added = [];
|
|
135
|
+
const removed = [];
|
|
136
|
+
const changed = [];
|
|
137
|
+
for (const [ok, value] of after) {
|
|
138
|
+
const previous = output.get(ok);
|
|
139
|
+
if (previous === undefined) {
|
|
140
|
+
added.push(value);
|
|
141
|
+
} else if (!equals(previous, value)) {
|
|
142
|
+
changed.push(value);
|
|
143
|
+
}
|
|
144
|
+
output.set(ok, value);
|
|
145
|
+
}
|
|
146
|
+
for (const ok of before) {
|
|
147
|
+
if (!after.has(ok)) {
|
|
148
|
+
const previous = output.get(ok);
|
|
149
|
+
if (previous !== undefined) {
|
|
150
|
+
removed.push(previous);
|
|
151
|
+
output.delete(ok);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (after.size === 0) {
|
|
156
|
+
outByLeft.delete(lk);
|
|
157
|
+
} else {
|
|
158
|
+
outByLeft.set(lk, new Set(after.keys()));
|
|
159
|
+
}
|
|
160
|
+
return { added, removed, changed };
|
|
161
|
+
};
|
|
162
|
+
const mergeInto = (target, diff) => {
|
|
163
|
+
target.added.push(...diff.added);
|
|
164
|
+
target.removed.push(...diff.removed);
|
|
165
|
+
target.changed.push(...diff.changed);
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
hydrate: (left, right) => {
|
|
169
|
+
lefts.clear();
|
|
170
|
+
rights.clear();
|
|
171
|
+
leftByJoin.clear();
|
|
172
|
+
rightByJoin.clear();
|
|
173
|
+
output.clear();
|
|
174
|
+
outByLeft.clear();
|
|
175
|
+
for (const right_ of right) {
|
|
176
|
+
const rk = rightKey(right_);
|
|
177
|
+
rights.set(rk, right_);
|
|
178
|
+
addToIndex(rightByJoin, rightOn(right_), rk);
|
|
179
|
+
}
|
|
180
|
+
for (const left_ of left) {
|
|
181
|
+
const lk = leftKey(left_);
|
|
182
|
+
lefts.set(lk, left_);
|
|
183
|
+
addToIndex(leftByJoin, leftOn(left_), lk);
|
|
184
|
+
const outs = leftOutputs(lk, left_);
|
|
185
|
+
for (const [ok, value] of outs) {
|
|
186
|
+
output.set(ok, value);
|
|
187
|
+
}
|
|
188
|
+
if (outs.size > 0) {
|
|
189
|
+
outByLeft.set(lk, new Set(outs.keys()));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
applyLeft: ({ op, row }) => {
|
|
194
|
+
const lk = leftKey(row);
|
|
195
|
+
const existing = lefts.get(lk);
|
|
196
|
+
if (existing !== undefined) {
|
|
197
|
+
removeFromIndex(leftByJoin, leftOn(existing), lk);
|
|
198
|
+
}
|
|
199
|
+
if (op === "delete") {
|
|
200
|
+
lefts.delete(lk);
|
|
201
|
+
} else {
|
|
202
|
+
lefts.set(lk, row);
|
|
203
|
+
addToIndex(leftByJoin, leftOn(row), lk);
|
|
204
|
+
}
|
|
205
|
+
const after = op === "delete" ? new Map : leftOutputs(lk, row);
|
|
206
|
+
return reconcileLeft(lk, after);
|
|
207
|
+
},
|
|
208
|
+
applyRight: ({ op, row }) => {
|
|
209
|
+
const rk = rightKey(row);
|
|
210
|
+
const existing = rights.get(rk);
|
|
211
|
+
const affected = new Set;
|
|
212
|
+
const addAffected = (joinValue) => {
|
|
213
|
+
for (const lk of leftByJoin.get(joinValue) ?? []) {
|
|
214
|
+
affected.add(lk);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
if (existing !== undefined) {
|
|
218
|
+
addAffected(rightOn(existing));
|
|
219
|
+
removeFromIndex(rightByJoin, rightOn(existing), rk);
|
|
220
|
+
}
|
|
221
|
+
if (op === "delete") {
|
|
222
|
+
rights.delete(rk);
|
|
223
|
+
} else {
|
|
224
|
+
rights.set(rk, row);
|
|
225
|
+
addToIndex(rightByJoin, rightOn(row), rk);
|
|
226
|
+
addAffected(rightOn(row));
|
|
227
|
+
}
|
|
228
|
+
const diff = { added: [], removed: [], changed: [] };
|
|
229
|
+
for (const lk of affected) {
|
|
230
|
+
const left = lefts.get(lk);
|
|
231
|
+
if (left !== undefined) {
|
|
232
|
+
mergeInto(diff, reconcileLeft(lk, leftOutputs(lk, left)));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return diff;
|
|
236
|
+
},
|
|
237
|
+
rows: () => [...output.values()],
|
|
238
|
+
size: () => output.size
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/engine/materializedView.ts
|
|
243
|
+
var emptyDiff = () => ({
|
|
244
|
+
added: [],
|
|
245
|
+
removed: [],
|
|
246
|
+
changed: []
|
|
247
|
+
});
|
|
248
|
+
var shallowEqual2 = (a, b) => {
|
|
249
|
+
if (a === b) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const aKeys = Object.keys(a);
|
|
256
|
+
const bKeys = Object.keys(b);
|
|
257
|
+
if (aKeys.length !== bKeys.length) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return aKeys.every((key) => a[key] === b[key]);
|
|
261
|
+
};
|
|
262
|
+
var isEmptyViewDiff = (diff) => diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0;
|
|
263
|
+
var createMaterializedView = (options) => {
|
|
264
|
+
const { key, match } = options;
|
|
265
|
+
const equals = options.equals ?? shallowEqual2;
|
|
266
|
+
const set = new Map;
|
|
267
|
+
return {
|
|
268
|
+
hydrate: (rows) => {
|
|
269
|
+
set.clear();
|
|
270
|
+
for (const row of rows) {
|
|
271
|
+
set.set(key(row), row);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
reset: (rows) => {
|
|
275
|
+
const next = new Map;
|
|
276
|
+
const added = [];
|
|
277
|
+
const changed = [];
|
|
278
|
+
for (const row of rows) {
|
|
279
|
+
const rowKey = key(row);
|
|
280
|
+
next.set(rowKey, row);
|
|
281
|
+
const previous = set.get(rowKey);
|
|
282
|
+
if (previous === undefined) {
|
|
283
|
+
added.push(row);
|
|
284
|
+
} else if (!equals(previous, row)) {
|
|
285
|
+
changed.push(row);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const removed = [];
|
|
289
|
+
for (const [rowKey, previous] of set) {
|
|
290
|
+
if (!next.has(rowKey)) {
|
|
291
|
+
removed.push(previous);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
set.clear();
|
|
295
|
+
for (const [rowKey, row] of next) {
|
|
296
|
+
set.set(rowKey, row);
|
|
297
|
+
}
|
|
298
|
+
return { added, removed, changed };
|
|
299
|
+
},
|
|
300
|
+
apply: ({ op, row }) => {
|
|
301
|
+
const rowKey = key(row);
|
|
302
|
+
const existing = set.get(rowKey);
|
|
303
|
+
if (op === "delete") {
|
|
304
|
+
if (existing === undefined) {
|
|
305
|
+
return emptyDiff();
|
|
306
|
+
}
|
|
307
|
+
set.delete(rowKey);
|
|
308
|
+
return { added: [], removed: [existing], changed: [] };
|
|
309
|
+
}
|
|
310
|
+
if (match(row)) {
|
|
311
|
+
set.set(rowKey, row);
|
|
312
|
+
return existing === undefined ? { added: [row], removed: [], changed: [] } : { added: [], removed: [], changed: [row] };
|
|
313
|
+
}
|
|
314
|
+
if (existing !== undefined) {
|
|
315
|
+
set.delete(rowKey);
|
|
316
|
+
return { added: [], removed: [existing], changed: [] };
|
|
317
|
+
}
|
|
318
|
+
return emptyDiff();
|
|
319
|
+
},
|
|
320
|
+
rows: () => [...set.values()],
|
|
321
|
+
size: () => set.size
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/engine/retry.ts
|
|
326
|
+
var PG_RETRY_CODES = new Set(["40001", "40P01"]);
|
|
327
|
+
var isSerializationFailure = (error) => {
|
|
328
|
+
if (error === null || typeof error !== "object")
|
|
329
|
+
return false;
|
|
330
|
+
const code = error.code;
|
|
331
|
+
if (typeof code === "string" && PG_RETRY_CODES.has(code))
|
|
332
|
+
return true;
|
|
333
|
+
const cause = error.cause;
|
|
334
|
+
if (cause !== undefined)
|
|
335
|
+
return isSerializationFailure(cause);
|
|
336
|
+
return false;
|
|
337
|
+
};
|
|
338
|
+
var exponentialBackoff = (options = {}) => (attempt) => {
|
|
339
|
+
const base = options.baseMs ?? 25;
|
|
340
|
+
const factor = options.factor ?? 2;
|
|
341
|
+
const max = options.maxMs ?? 1000;
|
|
342
|
+
const jitter = options.jitter ?? 0.2;
|
|
343
|
+
const raw = Math.min(max, base * factor ** Math.max(0, attempt - 1));
|
|
344
|
+
const spread = raw * jitter;
|
|
345
|
+
return raw + (Math.random() * 2 - 1) * spread;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
class RetriesExhaustedError extends Error {
|
|
349
|
+
attempts;
|
|
350
|
+
elapsedMs;
|
|
351
|
+
cause;
|
|
352
|
+
constructor(attempts, elapsedMs, cause) {
|
|
353
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
354
|
+
super(`retries exhausted after ${attempts} attempts (${elapsedMs}ms): ${message}`);
|
|
355
|
+
this.name = "RetriesExhaustedError";
|
|
356
|
+
this.attempts = attempts;
|
|
357
|
+
this.elapsedMs = elapsedMs;
|
|
358
|
+
this.cause = cause;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/engine/sandbox.ts
|
|
363
|
+
var isolatedJscModule;
|
|
364
|
+
var loadIsolatedJsc = async () => {
|
|
365
|
+
if (isolatedJscModule !== undefined)
|
|
366
|
+
return isolatedJscModule;
|
|
367
|
+
try {
|
|
368
|
+
isolatedJscModule = await import("@absolutejs/isolated-jsc");
|
|
369
|
+
return isolatedJscModule;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
throw new Error('sandboxedHandler requires the optional peer "@absolutejs/isolated-jsc". Install it with: bun add @absolutejs/isolated-jsc', { cause: error });
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
var wrap = (source) => `
|
|
375
|
+
function (__callId, args, ctx) {
|
|
376
|
+
const userFn = (${source});
|
|
377
|
+
if (typeof userFn !== 'function') {
|
|
378
|
+
throw new Error(
|
|
379
|
+
'sandboxedHandler must evaluate to (args, ctx, actions, unsafeHost) => result; got ' +
|
|
380
|
+
typeof userFn
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const actions = {
|
|
384
|
+
insert: (table, data) => __dispatch(__callId, 'insert', table, data),
|
|
385
|
+
update: (table, data) => __dispatch(__callId, 'update', table, data),
|
|
386
|
+
delete: (table, row) => __dispatch(__callId, 'delete', table, row),
|
|
387
|
+
change: (collection, change) => __dispatch(__callId, 'change', collection, change),
|
|
388
|
+
now: () => __dispatch(__callId, 'now'),
|
|
389
|
+
fetch: (url, init) => __dispatch(__callId, 'fetch', url, init)
|
|
390
|
+
};
|
|
391
|
+
// Escape hatch \u2014 host fns the mutation explicitly opted in to.
|
|
392
|
+
// The Proxy means every property access is a host call; the
|
|
393
|
+
// engine throws if the property name isn't declared in the
|
|
394
|
+
// mutation's sandbox.unsafeHost map.
|
|
395
|
+
const unsafeHost = new Proxy({}, {
|
|
396
|
+
get: (_target, fnName) => {
|
|
397
|
+
if (typeof fnName !== 'string') return undefined;
|
|
398
|
+
return (...callArgs) =>
|
|
399
|
+
__dispatch(__callId, 'unsafeHost', fnName, callArgs);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
return userFn(args, ctx, actions, unsafeHost);
|
|
403
|
+
}
|
|
404
|
+
`;
|
|
405
|
+
var compile = async (source, config, bridgeFetch) => {
|
|
406
|
+
const { Reference, createIsolatedRunner, resolveIsolatePolicy } = await loadIsolatedJsc();
|
|
407
|
+
const callMap = new Map;
|
|
408
|
+
const unsafeHost = config.unsafeHost;
|
|
409
|
+
const dispatch = new Reference((callId, op, ...rest) => {
|
|
410
|
+
const a = callMap.get(callId);
|
|
411
|
+
if (a === undefined) {
|
|
412
|
+
throw new Error(`__dispatch invoked for orphan callId ${String(callId)}`);
|
|
413
|
+
}
|
|
414
|
+
switch (op) {
|
|
415
|
+
case "insert":
|
|
416
|
+
return a.insert(rest[0], rest[1]);
|
|
417
|
+
case "update":
|
|
418
|
+
return a.update(rest[0], rest[1]);
|
|
419
|
+
case "delete":
|
|
420
|
+
return a.delete(rest[0], rest[1]);
|
|
421
|
+
case "change":
|
|
422
|
+
return a.change(rest[0], rest[1]);
|
|
423
|
+
case "now":
|
|
424
|
+
return a.now();
|
|
425
|
+
case "fetch":
|
|
426
|
+
return runBridgeFetch(bridgeFetch, rest[0], rest[1]);
|
|
427
|
+
case "unsafeHost": {
|
|
428
|
+
const fnName = rest[0];
|
|
429
|
+
const callArgs = rest[1] ?? [];
|
|
430
|
+
if (unsafeHost === undefined || typeof unsafeHost[fnName] !== "function") {
|
|
431
|
+
throw new Error(`sandboxedHandler called unsafeHost.${fnName}() but it was not declared in the mutation's sandbox.unsafeHost config. Declare it (and only the host fns you intend to expose) to opt in to the escape hatch.`);
|
|
432
|
+
}
|
|
433
|
+
return unsafeHost[fnName](...callArgs);
|
|
434
|
+
}
|
|
435
|
+
default:
|
|
436
|
+
throw new Error(`unknown sandbox action op: ${String(op)}`);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
const timeoutMs = config.timeout ?? 5000;
|
|
440
|
+
const sourceToCall = wrap(source);
|
|
441
|
+
const policy = resolveIsolatePolicy("tenant-script", {
|
|
442
|
+
allowWorkerFallback: true,
|
|
443
|
+
backend: config.backend ?? "auto",
|
|
444
|
+
memoryLimit: config.memoryLimit ?? 32,
|
|
445
|
+
timeout: timeoutMs
|
|
446
|
+
});
|
|
447
|
+
const runner = createIsolatedRunner({
|
|
448
|
+
globals: { __dispatch: dispatch },
|
|
449
|
+
policy
|
|
450
|
+
});
|
|
451
|
+
await runner.precompile("sandboxedHandler", sourceToCall);
|
|
452
|
+
return {
|
|
453
|
+
callMap,
|
|
454
|
+
nextCallId: 1,
|
|
455
|
+
runner,
|
|
456
|
+
source: sourceToCall,
|
|
457
|
+
timeoutMs
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
var runBridgeFetch = async (config, url, init) => {
|
|
461
|
+
if (config === undefined) {
|
|
462
|
+
throw new Error("actions.fetch called but the engine has no `bridgeFetch` config \u2014 " + "pass `bridgeFetch: { ... }` to createSyncEngine.");
|
|
463
|
+
}
|
|
464
|
+
let parsed;
|
|
465
|
+
try {
|
|
466
|
+
parsed = new URL(url);
|
|
467
|
+
} catch {
|
|
468
|
+
throw new Error(`actions.fetch: invalid URL "${String(url)}"`);
|
|
469
|
+
}
|
|
470
|
+
const endpoint = config[parsed.hostname] ?? (Object.prototype.hasOwnProperty.call(config, "*") ? config["*"] : undefined);
|
|
471
|
+
if (endpoint === undefined) {
|
|
472
|
+
throw new Error(`actions.fetch: hostname "${parsed.hostname}" is not allowlisted in bridgeFetch config`);
|
|
473
|
+
}
|
|
474
|
+
const headers = { ...endpoint.headers ?? {} };
|
|
475
|
+
if (init?.headers !== undefined) {
|
|
476
|
+
const incoming = init.headers;
|
|
477
|
+
for (const [name, value] of Object.entries(incoming)) {
|
|
478
|
+
if (name.toLowerCase() === "authorization")
|
|
479
|
+
continue;
|
|
480
|
+
headers[name] = value;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (endpoint.authorization !== undefined) {
|
|
484
|
+
let auth;
|
|
485
|
+
try {
|
|
486
|
+
auth = await endpoint.authorization();
|
|
487
|
+
} catch {
|
|
488
|
+
throw new Error("actions.fetch: authorization callback failed");
|
|
489
|
+
}
|
|
490
|
+
headers.Authorization = auth;
|
|
491
|
+
}
|
|
492
|
+
const response = await fetch(url, { ...init, headers });
|
|
493
|
+
const responseHeaders = {};
|
|
494
|
+
response.headers.forEach((value, name) => {
|
|
495
|
+
responseHeaders[name] = value;
|
|
496
|
+
});
|
|
497
|
+
const body = await response.text();
|
|
498
|
+
return {
|
|
499
|
+
body,
|
|
500
|
+
headers: responseHeaders,
|
|
501
|
+
ok: response.ok,
|
|
502
|
+
status: response.status,
|
|
503
|
+
statusText: response.statusText,
|
|
504
|
+
url: response.url
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
var makeSandboxedHandler = (source, config = {}, engineExtras) => {
|
|
508
|
+
let pending;
|
|
509
|
+
const metricsHook = engineExtras?.metricsHook;
|
|
510
|
+
const bridgeFetch = engineExtras?.bridgeFetch;
|
|
511
|
+
const getCompiled = async () => {
|
|
512
|
+
if (pending !== undefined) {
|
|
513
|
+
return pending;
|
|
514
|
+
}
|
|
515
|
+
pending = compile(source, config, bridgeFetch);
|
|
516
|
+
return pending;
|
|
517
|
+
};
|
|
518
|
+
return async (args, ctx, actions) => {
|
|
519
|
+
const compiled = await getCompiled();
|
|
520
|
+
const callId = compiled.nextCallId++;
|
|
521
|
+
compiled.callMap.set(callId, actions);
|
|
522
|
+
if (metricsHook === undefined) {
|
|
523
|
+
try {
|
|
524
|
+
return await compiled.runner.call("sandboxedHandler", compiled.source, [callId, args, ctx], { run: { timeout: compiled.timeoutMs } });
|
|
525
|
+
} catch (error) {
|
|
526
|
+
if (isIsolateDisposalError(error)) {
|
|
527
|
+
pending = undefined;
|
|
528
|
+
await disposeCompiled(compiled);
|
|
529
|
+
}
|
|
530
|
+
throw error;
|
|
531
|
+
} finally {
|
|
532
|
+
compiled.callMap.delete(callId);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const startedAt = performance.now();
|
|
536
|
+
const id = makeRandomId();
|
|
537
|
+
try {
|
|
538
|
+
const { result, metrics } = await compiled.runner.call("sandboxedHandler", compiled.source, [callId, args, ctx], { run: { timeout: compiled.timeoutMs }, withMetrics: true });
|
|
539
|
+
fireMetrics(metricsHook.onMetrics, {
|
|
540
|
+
backend: metrics.backend,
|
|
541
|
+
cpuMs: metrics.cpuMs,
|
|
542
|
+
durationMs: performance.now() - startedAt,
|
|
543
|
+
heapBytes: metrics.heapBytes,
|
|
544
|
+
id,
|
|
545
|
+
mutationName: metricsHook.mutationName,
|
|
546
|
+
ok: true,
|
|
547
|
+
timestamp: Date.now()
|
|
548
|
+
});
|
|
549
|
+
return result;
|
|
550
|
+
} catch (error) {
|
|
551
|
+
if (isIsolateDisposalError(error)) {
|
|
552
|
+
pending = undefined;
|
|
553
|
+
await disposeCompiled(compiled);
|
|
554
|
+
}
|
|
555
|
+
fireMetrics(metricsHook.onMetrics, {
|
|
556
|
+
cpuMs: 0,
|
|
557
|
+
durationMs: performance.now() - startedAt,
|
|
558
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
559
|
+
errorName: error instanceof Error ? error.name : "Error",
|
|
560
|
+
heapBytes: 0,
|
|
561
|
+
id,
|
|
562
|
+
mutationName: metricsHook.mutationName,
|
|
563
|
+
ok: false,
|
|
564
|
+
timestamp: Date.now()
|
|
565
|
+
});
|
|
566
|
+
throw error;
|
|
567
|
+
} finally {
|
|
568
|
+
compiled.callMap.delete(callId);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
};
|
|
572
|
+
var makeRandomId = () => `hm_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
573
|
+
var isIsolateDisposalError = (error) => error instanceof Error && (error.name === "TimeoutError" || error.name === "MemoryLimitError" || error.name === "IsolateDisposedError");
|
|
574
|
+
var disposeCompiled = async (compiled) => {
|
|
575
|
+
try {
|
|
576
|
+
await compiled.runner.dispose();
|
|
577
|
+
} catch {}
|
|
578
|
+
};
|
|
579
|
+
var fireMetrics = (hook, record) => {
|
|
580
|
+
let outcome;
|
|
581
|
+
try {
|
|
582
|
+
outcome = hook(record);
|
|
583
|
+
} catch {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (outcome instanceof Promise) {
|
|
587
|
+
outcome.catch(() => {});
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// src/engine/pack.ts
|
|
592
|
+
class PackTableConflictError extends Error {
|
|
593
|
+
table;
|
|
594
|
+
existingPack;
|
|
595
|
+
newPack;
|
|
596
|
+
constructor(table, existingPack, newPack) {
|
|
597
|
+
super(`Pack "${newPack}" claims table "${table}", but "${existingPack}" already owns it. Use a tablePrefix on one of them.`);
|
|
598
|
+
this.name = "PackTableConflictError";
|
|
599
|
+
this.table = table;
|
|
600
|
+
this.existingPack = existingPack;
|
|
601
|
+
this.newPack = newPack;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
class PackMissingDependencyError extends Error {
|
|
606
|
+
pack;
|
|
607
|
+
missingTable;
|
|
608
|
+
constructor(pack, missingTable) {
|
|
609
|
+
super(`Pack "${pack}" requires a reader for table "${missingTable}" but none is registered. Call engine.registerReader("${missingTable}", ...) before engine.registerPack.`);
|
|
610
|
+
this.name = "PackMissingDependencyError";
|
|
611
|
+
this.pack = pack;
|
|
612
|
+
this.missingTable = missingTable;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
var defineSyncPack = (pack) => pack;
|
|
616
|
+
|
|
617
|
+
// src/engine/search.ts
|
|
618
|
+
var SEARCH_SCORE_FIELD = "_score";
|
|
619
|
+
var defineSearchCollection = (definition) => ({
|
|
620
|
+
...definition,
|
|
621
|
+
kind: "search"
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// src/engine/migrate.ts
|
|
625
|
+
class EngineFencedError extends Error {
|
|
626
|
+
reason;
|
|
627
|
+
constructor(reason) {
|
|
628
|
+
super(`[sync] Engine is fenced for migration: ${reason}`);
|
|
629
|
+
this.name = "EngineFencedError";
|
|
630
|
+
this.reason = reason;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/engine/syncEngine.ts
|
|
635
|
+
class UnauthorizedError extends Error {
|
|
636
|
+
constructor(subject) {
|
|
637
|
+
super(`Not authorized: ${subject}`);
|
|
638
|
+
this.name = "UnauthorizedError";
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
class AbortError extends Error {
|
|
643
|
+
constructor(reason) {
|
|
644
|
+
super(reason ?? "Aborted");
|
|
645
|
+
this.name = "AbortError";
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
var checkAborted = (signal) => {
|
|
649
|
+
if (signal?.aborted) {
|
|
650
|
+
throw new AbortError(signal.reason instanceof Error ? signal.reason.message : typeof signal.reason === "string" ? signal.reason : "Aborted");
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
var linkAbortToUnsubscribe = (signal, unsubscribe) => {
|
|
654
|
+
if (signal === undefined)
|
|
655
|
+
return;
|
|
656
|
+
if (signal.aborted) {
|
|
657
|
+
unsubscribe();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const handler = () => {
|
|
661
|
+
try {
|
|
662
|
+
unsubscribe();
|
|
663
|
+
} catch {}
|
|
664
|
+
};
|
|
665
|
+
signal.addEventListener("abort", handler, { once: true });
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
class SchemaError extends Error {
|
|
669
|
+
constructor(table, fieldName) {
|
|
670
|
+
super(`Schema violation on "${table}": invalid field "${fieldName}"`);
|
|
671
|
+
this.name = "SchemaError";
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
class MissedChangesError extends Error {
|
|
676
|
+
requestedSince;
|
|
677
|
+
availableSince;
|
|
678
|
+
constructor(requestedSince, availableSince) {
|
|
679
|
+
super(`Change log no longer covers version ${requestedSince}; oldest available is ${availableSince}. ` + `Re-bootstrap and resume from ${availableSince}.`);
|
|
680
|
+
this.name = "MissedChangesError";
|
|
681
|
+
this.requestedSince = requestedSince;
|
|
682
|
+
this.availableSince = availableSince;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
class CdcConsumerSlowError extends Error {
|
|
687
|
+
maxBuffer;
|
|
688
|
+
lastDeliveredVersion;
|
|
689
|
+
constructor(maxBuffer, lastDeliveredVersion) {
|
|
690
|
+
super(`CDC stream buffer overflowed (max ${maxBuffer}); consumer fell behind. ` + `Last delivered version: ${lastDeliveredVersion}. Resubscribe with since=${lastDeliveredVersion}.`);
|
|
691
|
+
this.name = "CdcConsumerSlowError";
|
|
692
|
+
this.maxBuffer = maxBuffer;
|
|
693
|
+
this.lastDeliveredVersion = lastDeliveredVersion;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
class MutationQueueOverflowError extends Error {
|
|
698
|
+
queueLimit;
|
|
699
|
+
constructor(queueLimit) {
|
|
700
|
+
super(`Mutation queue overflowed (limit ${queueLimit}); the engine is at ` + `its mutationConcurrency cap and the waiting queue is full. ` + `Retry later or shed load at the gateway.`);
|
|
701
|
+
this.name = "MutationQueueOverflowError";
|
|
702
|
+
this.queueLimit = queueLimit;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
class SubscriptionLimitError extends Error {
|
|
707
|
+
tenantKey;
|
|
708
|
+
limit;
|
|
709
|
+
active;
|
|
710
|
+
constructor(tenantKey, limit, active) {
|
|
711
|
+
super(`Tenant "${tenantKey}" is at the subscription cap ` + `(${active}/${limit}). Close an existing subscription before opening another.`);
|
|
712
|
+
this.name = "SubscriptionLimitError";
|
|
713
|
+
this.tenantKey = tenantKey;
|
|
714
|
+
this.limit = limit;
|
|
715
|
+
this.active = active;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
var defaultKey = (row) => row.id;
|
|
719
|
+
var shallowEqual3 = (a, b) => {
|
|
720
|
+
if (a === b) {
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
const aKeys = Object.keys(a);
|
|
727
|
+
const bKeys = Object.keys(b);
|
|
728
|
+
return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
|
|
729
|
+
};
|
|
730
|
+
var subKeyIds = new WeakMap;
|
|
731
|
+
var subKeyCounter = 0;
|
|
732
|
+
var stableValueKey = (value) => {
|
|
733
|
+
if (value === undefined)
|
|
734
|
+
return "u";
|
|
735
|
+
if (value === null)
|
|
736
|
+
return "n";
|
|
737
|
+
const tag = typeof value;
|
|
738
|
+
if (tag === "string")
|
|
739
|
+
return `s:${value}`;
|
|
740
|
+
if (tag === "number" || tag === "boolean" || tag === "bigint") {
|
|
741
|
+
return `${tag[0]}:${String(value)}`;
|
|
742
|
+
}
|
|
743
|
+
if (tag !== "object")
|
|
744
|
+
return `${tag[0]}:fn`;
|
|
745
|
+
try {
|
|
746
|
+
return `o:${JSON.stringify(value, (_k, v) => {
|
|
747
|
+
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
748
|
+
return v;
|
|
749
|
+
const record = v;
|
|
750
|
+
const sorted = {};
|
|
751
|
+
for (const key of Object.keys(record).sort()) {
|
|
752
|
+
sorted[key] = record[key];
|
|
753
|
+
}
|
|
754
|
+
return sorted;
|
|
755
|
+
})}`;
|
|
756
|
+
} catch {
|
|
757
|
+
const obj = value;
|
|
758
|
+
let id = subKeyIds.get(obj);
|
|
759
|
+
if (id === undefined) {
|
|
760
|
+
subKeyCounter += 1;
|
|
761
|
+
id = `i${subKeyCounter}`;
|
|
762
|
+
subKeyIds.set(obj, id);
|
|
763
|
+
}
|
|
764
|
+
return `i:${id}`;
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
var stableSubKey = (collection, params, ctx) => `${collection}|${stableValueKey(params)}|${stableValueKey(ctx)}`;
|
|
768
|
+
var equalsIgnoringScore = (a, b) => {
|
|
769
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
|
|
770
|
+
return a === b;
|
|
771
|
+
}
|
|
772
|
+
const strip = (value) => Object.keys(value).filter((k) => k !== SEARCH_SCORE_FIELD);
|
|
773
|
+
const aKeys = strip(a);
|
|
774
|
+
const bKeys = strip(b);
|
|
775
|
+
return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
|
|
776
|
+
};
|
|
777
|
+
var createSyncEngine = (options = {}) => {
|
|
778
|
+
const registry = new Map;
|
|
779
|
+
const mutations = new Map;
|
|
780
|
+
const sandboxRunners = new Map;
|
|
781
|
+
const writers = new Map;
|
|
782
|
+
const readers = new Map;
|
|
783
|
+
const schedules = new Map;
|
|
784
|
+
const packTableOwners = new Map;
|
|
785
|
+
const registeredPacks = [];
|
|
786
|
+
const permissions = new Map;
|
|
787
|
+
for (const [table, rules] of Object.entries(options.permissions ?? {})) {
|
|
788
|
+
permissions.set(table, rules);
|
|
789
|
+
}
|
|
790
|
+
const readRuleFor = (table) => permissions.get(table)?.read;
|
|
791
|
+
const writeRuleFor = (table, op) => {
|
|
792
|
+
const rules = permissions.get(table);
|
|
793
|
+
return rules?.[op] ?? rules?.write;
|
|
794
|
+
};
|
|
795
|
+
const schemas = new Map;
|
|
796
|
+
for (const [table, schema] of Object.entries(options.schemas ?? {})) {
|
|
797
|
+
schemas.set(table, schema);
|
|
798
|
+
}
|
|
799
|
+
const crdtFields = new Map;
|
|
800
|
+
const validateWrite = (table, op, row) => {
|
|
801
|
+
const schema = schemas.get(table);
|
|
802
|
+
if (schema === undefined || typeof row !== "object" || row === null) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const record = row;
|
|
806
|
+
for (const [fieldName, validate] of Object.entries(schema.fields)) {
|
|
807
|
+
const present = fieldName in record;
|
|
808
|
+
if (op === "update" && !present) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (!validate(record[fieldName])) {
|
|
812
|
+
throw new SchemaError(table, fieldName);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
const migrateRow = (table, row) => {
|
|
817
|
+
const migrate = schemas.get(table)?.migrate;
|
|
818
|
+
return migrate ? migrate(row) : row;
|
|
819
|
+
};
|
|
820
|
+
const reactiveSubs = new Set;
|
|
821
|
+
const searchSubs = new Set;
|
|
822
|
+
const searchIndexes = new Map;
|
|
823
|
+
const active = new Map;
|
|
824
|
+
const tableIndex = new Map;
|
|
825
|
+
const changeLogSize = options.changeLogSize ?? 1024;
|
|
826
|
+
const changeLogRetainMs = options.changeLogRetainMs ?? null;
|
|
827
|
+
const changeLog = [];
|
|
828
|
+
let version = 0;
|
|
829
|
+
const engineStartedAt = Date.now();
|
|
830
|
+
let mutationsCompleted = 0;
|
|
831
|
+
let mutationsFailed = 0;
|
|
832
|
+
let mutationsRetried = 0;
|
|
833
|
+
let mutationsInFlight = 0;
|
|
834
|
+
let connectedSources = 0;
|
|
835
|
+
let sourceChangesReceived = 0;
|
|
836
|
+
let sourceLastChangeAt = null;
|
|
837
|
+
const mutationWaiters = [];
|
|
838
|
+
let mutationsQueued = 0;
|
|
839
|
+
const activeFences = new Set;
|
|
840
|
+
const acquireMutationSlot = async () => {
|
|
841
|
+
const limit = options.mutationConcurrency;
|
|
842
|
+
if (limit === undefined) {
|
|
843
|
+
mutationsInFlight += 1;
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (mutationsInFlight < limit && mutationWaiters.length === 0) {
|
|
847
|
+
mutationsInFlight += 1;
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const queueLimit = options.mutationQueueLimit;
|
|
851
|
+
if (queueLimit !== undefined && mutationsQueued >= queueLimit) {
|
|
852
|
+
throw new MutationQueueOverflowError(queueLimit);
|
|
853
|
+
}
|
|
854
|
+
mutationsQueued += 1;
|
|
855
|
+
try {
|
|
856
|
+
await new Promise((resolve) => {
|
|
857
|
+
mutationWaiters.push(resolve);
|
|
858
|
+
});
|
|
859
|
+
} finally {
|
|
860
|
+
mutationsQueued -= 1;
|
|
861
|
+
}
|
|
862
|
+
mutationsInFlight += 1;
|
|
863
|
+
};
|
|
864
|
+
const releaseMutationSlot = () => {
|
|
865
|
+
mutationsInFlight -= 1;
|
|
866
|
+
if (options.mutationConcurrency === undefined)
|
|
867
|
+
return;
|
|
868
|
+
const next = mutationWaiters.shift();
|
|
869
|
+
if (next !== undefined)
|
|
870
|
+
next();
|
|
871
|
+
};
|
|
872
|
+
const subscriptionsByTenant = new Map;
|
|
873
|
+
const acquireSubscriptionSlot = (ctx, args) => {
|
|
874
|
+
const cap = options.subscriptionLimit;
|
|
875
|
+
if (cap === undefined)
|
|
876
|
+
return;
|
|
877
|
+
const tenantKey = cap.key(ctx, args);
|
|
878
|
+
if (tenantKey === undefined)
|
|
879
|
+
return;
|
|
880
|
+
const active2 = subscriptionsByTenant.get(tenantKey) ?? 0;
|
|
881
|
+
if (active2 >= cap.max) {
|
|
882
|
+
throw new SubscriptionLimitError(tenantKey, cap.max, active2);
|
|
883
|
+
}
|
|
884
|
+
subscriptionsByTenant.set(tenantKey, active2 + 1);
|
|
885
|
+
return tenantKey;
|
|
886
|
+
};
|
|
887
|
+
const releaseSubscriptionSlot = (tenantKey) => {
|
|
888
|
+
if (tenantKey === undefined)
|
|
889
|
+
return;
|
|
890
|
+
const active2 = subscriptionsByTenant.get(tenantKey);
|
|
891
|
+
if (active2 === undefined || active2 <= 1) {
|
|
892
|
+
subscriptionsByTenant.delete(tenantKey);
|
|
893
|
+
} else {
|
|
894
|
+
subscriptionsByTenant.set(tenantKey, active2 - 1);
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
const reactiveCacheMax = options.reactiveCache?.max ?? 256;
|
|
898
|
+
const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
|
|
899
|
+
const cachedReruns = new Map;
|
|
900
|
+
const touchCacheEntry = (key, entry) => {
|
|
901
|
+
cachedReruns.delete(key);
|
|
902
|
+
cachedReruns.set(key, entry);
|
|
903
|
+
};
|
|
904
|
+
const readCacheEntry = (key) => {
|
|
905
|
+
if (reactiveCacheMax <= 0)
|
|
906
|
+
return;
|
|
907
|
+
const entry = cachedReruns.get(key);
|
|
908
|
+
if (entry === undefined)
|
|
909
|
+
return;
|
|
910
|
+
if (reactiveCacheTtlMs > 0 && entry.expiresAt < Date.now()) {
|
|
911
|
+
cachedReruns.delete(key);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
touchCacheEntry(key, entry);
|
|
915
|
+
return entry;
|
|
916
|
+
};
|
|
917
|
+
const writeCacheEntry = (entry) => {
|
|
918
|
+
if (reactiveCacheMax <= 0)
|
|
919
|
+
return;
|
|
920
|
+
cachedReruns.set(entry.rerunKey, entry);
|
|
921
|
+
while (cachedReruns.size > reactiveCacheMax) {
|
|
922
|
+
const oldest = cachedReruns.keys().next().value;
|
|
923
|
+
if (oldest === undefined)
|
|
924
|
+
break;
|
|
925
|
+
cachedReruns.delete(oldest);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
const activityListeners = new Set;
|
|
929
|
+
const emitActivity = (event) => {
|
|
930
|
+
for (const listener of activityListeners) {
|
|
931
|
+
listener(event);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
const streamSubscribers = new Set;
|
|
935
|
+
const runInTransaction = options.transaction;
|
|
936
|
+
const instanceId = options.instanceId ?? globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
|
|
937
|
+
let clusterBus;
|
|
938
|
+
const tracer = tracerOrNoop(options.tracerProvider, "@absolutejs/sync");
|
|
939
|
+
const importChangeLog = (snapshot) => {
|
|
940
|
+
if (version !== 0) {
|
|
941
|
+
throw new Error(`[sync] importChangeLog: engine already has version ${version}; ` + `restore must happen before any local writes commit.`);
|
|
942
|
+
}
|
|
943
|
+
if (snapshot.instanceId !== instanceId) {
|
|
944
|
+
throw new Error(`[sync] importChangeLog: snapshot instanceId "${snapshot.instanceId}" ` + `does not match this engine's instanceId "${instanceId}". ` + `Pass options.instanceId = "${snapshot.instanceId}" to createSyncEngine.`);
|
|
945
|
+
}
|
|
946
|
+
version = snapshot.version;
|
|
947
|
+
for (const entry of snapshot.entries) {
|
|
948
|
+
changeLog.push(entry);
|
|
949
|
+
}
|
|
950
|
+
while (changeLog.length > changeLogSize) {
|
|
951
|
+
changeLog.shift();
|
|
952
|
+
}
|
|
953
|
+
if (changeLogRetainMs !== null && changeLogRetainMs > 0) {
|
|
954
|
+
const cutoff = Date.now() - changeLogRetainMs;
|
|
955
|
+
while (changeLog.length > 0 && changeLog[0].at < cutoff) {
|
|
956
|
+
changeLog.shift();
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return snapshot.entries.length;
|
|
960
|
+
};
|
|
961
|
+
if (options.initialChangeLog !== undefined) {
|
|
962
|
+
importChangeLog(options.initialChangeLog);
|
|
963
|
+
}
|
|
964
|
+
const broadcast = (changes, originVersion) => {
|
|
965
|
+
if (clusterBus !== undefined && changes.length > 0) {
|
|
966
|
+
clusterBus.publish({
|
|
967
|
+
changes,
|
|
968
|
+
origin: instanceId,
|
|
969
|
+
originVersion
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
const subsFor = (collection) => {
|
|
974
|
+
let set = active.get(collection);
|
|
975
|
+
if (set === undefined) {
|
|
976
|
+
set = new Set;
|
|
977
|
+
active.set(collection, set);
|
|
978
|
+
}
|
|
979
|
+
return set;
|
|
980
|
+
};
|
|
981
|
+
const addTableIndex = (table, name) => {
|
|
982
|
+
let set = tableIndex.get(table);
|
|
983
|
+
if (set === undefined) {
|
|
984
|
+
set = new Set;
|
|
985
|
+
tableIndex.set(table, set);
|
|
986
|
+
}
|
|
987
|
+
set.add(name);
|
|
988
|
+
};
|
|
989
|
+
const sideChange = (change, match) => change.op !== "delete" && match !== undefined && !match(change.row) ? { op: "delete", row: change.row } : change;
|
|
990
|
+
const EMPTY_DIFF = {
|
|
991
|
+
added: [],
|
|
992
|
+
removed: [],
|
|
993
|
+
changed: []
|
|
994
|
+
};
|
|
995
|
+
const subscriptionDiff = async (subscription, table, change) => {
|
|
996
|
+
if (subscription.kind === "graph") {
|
|
997
|
+
return subscription.instance.applyChange(table, change);
|
|
998
|
+
}
|
|
999
|
+
if (subscription.kind === "join") {
|
|
1000
|
+
const js = subscription.join;
|
|
1001
|
+
if (table === js.leftTable) {
|
|
1002
|
+
return js.op.applyLeft(sideChange(change, js.leftMatch));
|
|
1003
|
+
}
|
|
1004
|
+
if (table === js.rightTable) {
|
|
1005
|
+
return js.op.applyRight(sideChange(change, js.rightMatch));
|
|
1006
|
+
}
|
|
1007
|
+
return EMPTY_DIFF;
|
|
1008
|
+
}
|
|
1009
|
+
if (subscription.kind === "reactive") {
|
|
1010
|
+
return EMPTY_DIFF;
|
|
1011
|
+
}
|
|
1012
|
+
if (subscription.kind === "search") {
|
|
1013
|
+
return EMPTY_DIFF;
|
|
1014
|
+
}
|
|
1015
|
+
if (subscription.incremental) {
|
|
1016
|
+
try {
|
|
1017
|
+
return subscription.view.apply(change);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return subscription.view.reset(await subscription.rehydrate());
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (subscription.affects && !subscription.affects(change)) {
|
|
1023
|
+
return EMPTY_DIFF;
|
|
1024
|
+
}
|
|
1025
|
+
return subscription.view.reset(await subscription.rehydrate());
|
|
1026
|
+
};
|
|
1027
|
+
const subscriptionsForTable = function* (table) {
|
|
1028
|
+
const names = tableIndex.get(table);
|
|
1029
|
+
if (names === undefined) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
for (const name of names) {
|
|
1033
|
+
const set = active.get(name);
|
|
1034
|
+
if (set === undefined) {
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
yield* set;
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
const mergeViewDiffs = (diffs, key) => {
|
|
1041
|
+
const net = new Map;
|
|
1042
|
+
for (const diff of diffs) {
|
|
1043
|
+
for (const row of diff.removed) {
|
|
1044
|
+
const previous = net.get(key(row));
|
|
1045
|
+
if (previous?.state === "added") {
|
|
1046
|
+
net.delete(key(row));
|
|
1047
|
+
} else {
|
|
1048
|
+
net.set(key(row), { state: "removed", row });
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
for (const row of diff.added) {
|
|
1052
|
+
const previous = net.get(key(row));
|
|
1053
|
+
net.set(key(row), {
|
|
1054
|
+
state: previous?.state === "removed" ? "changed" : "added",
|
|
1055
|
+
row
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
for (const row of diff.changed) {
|
|
1059
|
+
const previous = net.get(key(row));
|
|
1060
|
+
net.set(key(row), {
|
|
1061
|
+
state: previous?.state === "added" ? "added" : "changed",
|
|
1062
|
+
row
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const added = [];
|
|
1067
|
+
const changed = [];
|
|
1068
|
+
const removed = [];
|
|
1069
|
+
for (const { state, row } of net.values()) {
|
|
1070
|
+
if (state === "added") {
|
|
1071
|
+
added.push(row);
|
|
1072
|
+
} else if (state === "changed") {
|
|
1073
|
+
changed.push(row);
|
|
1074
|
+
} else {
|
|
1075
|
+
removed.push(row);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return { added, changed, removed };
|
|
1079
|
+
};
|
|
1080
|
+
const depKey = (table, key) => `${table} ${key}`;
|
|
1081
|
+
const changedKeyFor = (table, change) => readers.get(table)?.key?.(change.row);
|
|
1082
|
+
const makeReadHandle = (ctx, readTables, readKeys, rangeDeps, applyRules = true) => {
|
|
1083
|
+
const readerFor = (table) => {
|
|
1084
|
+
const reader = readers.get(table);
|
|
1085
|
+
if (reader === undefined) {
|
|
1086
|
+
throw new Error(`No reader registered for table "${table}" \u2014 register one with engine.registerReader`);
|
|
1087
|
+
}
|
|
1088
|
+
return reader;
|
|
1089
|
+
};
|
|
1090
|
+
const ruleFor = (table) => applyRules ? readRuleFor(table) : undefined;
|
|
1091
|
+
return {
|
|
1092
|
+
all: async (table) => {
|
|
1093
|
+
readTables.add(table);
|
|
1094
|
+
const rows = [...await readerFor(table).all(ctx)].map((row) => migrateRow(table, row));
|
|
1095
|
+
const rule = ruleFor(table);
|
|
1096
|
+
return rule ? rows.filter((row) => rule(ctx, row)) : rows;
|
|
1097
|
+
},
|
|
1098
|
+
get: async (table, key) => {
|
|
1099
|
+
const reader = readerFor(table);
|
|
1100
|
+
if (reader.get === undefined) {
|
|
1101
|
+
throw new Error(`Reader for table "${table}" has no get(); use db.all() or add get`);
|
|
1102
|
+
}
|
|
1103
|
+
if (reader.key !== undefined) {
|
|
1104
|
+
readKeys.add(depKey(table, key));
|
|
1105
|
+
} else {
|
|
1106
|
+
readTables.add(table);
|
|
1107
|
+
}
|
|
1108
|
+
const raw = await reader.get(key, ctx);
|
|
1109
|
+
const row = raw === undefined ? undefined : migrateRow(table, raw);
|
|
1110
|
+
const rule = ruleFor(table);
|
|
1111
|
+
return rule && row !== undefined && !rule(ctx, row) ? undefined : row;
|
|
1112
|
+
},
|
|
1113
|
+
where: async (table, predicate) => {
|
|
1114
|
+
const reader = readerFor(table);
|
|
1115
|
+
const rule = ruleFor(table);
|
|
1116
|
+
const effective = rule ? (row) => predicate(row) && rule(ctx, row) : predicate;
|
|
1117
|
+
const matched = [...await reader.all(ctx)].map((row) => migrateRow(table, row)).filter(effective);
|
|
1118
|
+
if (reader.key !== undefined) {
|
|
1119
|
+
const key = reader.key;
|
|
1120
|
+
rangeDeps.push({
|
|
1121
|
+
table,
|
|
1122
|
+
predicate: effective,
|
|
1123
|
+
keys: new Set(matched.map(key))
|
|
1124
|
+
});
|
|
1125
|
+
} else {
|
|
1126
|
+
readTables.add(table);
|
|
1127
|
+
}
|
|
1128
|
+
return matched;
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
};
|
|
1132
|
+
const writerFor = (table) => {
|
|
1133
|
+
const writer = writers.get(table);
|
|
1134
|
+
if (writer === undefined) {
|
|
1135
|
+
throw new Error(`No writer registered for table "${table}" \u2014 register one with engine.registerWriter, or use actions.change`);
|
|
1136
|
+
}
|
|
1137
|
+
return writer;
|
|
1138
|
+
};
|
|
1139
|
+
const readExisting = async (table, value, ctx) => {
|
|
1140
|
+
const reader = readers.get(table);
|
|
1141
|
+
if (reader?.get === undefined) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const id = reader.key ? reader.key(value) : value.id;
|
|
1145
|
+
return id === undefined ? undefined : reader.get(id, ctx);
|
|
1146
|
+
};
|
|
1147
|
+
const authorizeWrite = async (table, op, value, ctx) => {
|
|
1148
|
+
const rule = writeRuleFor(table, op);
|
|
1149
|
+
if (rule === undefined) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
let subject = value;
|
|
1153
|
+
if (op !== "insert") {
|
|
1154
|
+
const existing = await readExisting(table, value, ctx);
|
|
1155
|
+
if (existing !== undefined) {
|
|
1156
|
+
subject = existing;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (!rule(ctx, subject)) {
|
|
1160
|
+
throw new UnauthorizedError(`${op} on table "${table}"`);
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
const mergeCrdtFields = async (table, op, data, ctx) => {
|
|
1164
|
+
const fields = crdtFields.get(table);
|
|
1165
|
+
if (fields === undefined || data === null || typeof data !== "object") {
|
|
1166
|
+
return data;
|
|
1167
|
+
}
|
|
1168
|
+
const incoming = data;
|
|
1169
|
+
const existing = op === "update" ? await readExisting(table, data, ctx) : undefined;
|
|
1170
|
+
const base = existing !== null && typeof existing === "object" ? existing : undefined;
|
|
1171
|
+
const merged = { ...incoming };
|
|
1172
|
+
for (const [field, adapter] of Object.entries(fields)) {
|
|
1173
|
+
if (incoming[field] === undefined) {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
merged[field] = adapter.merge(base?.[field] ?? adapter.empty(), incoming[field]);
|
|
1177
|
+
}
|
|
1178
|
+
return merged;
|
|
1179
|
+
};
|
|
1180
|
+
const makeActions = (tx, ctx, enforce) => {
|
|
1181
|
+
const buffered = [];
|
|
1182
|
+
const actions = {
|
|
1183
|
+
change: (collection, change) => {
|
|
1184
|
+
buffered.push({
|
|
1185
|
+
table: collection,
|
|
1186
|
+
change
|
|
1187
|
+
});
|
|
1188
|
+
return Promise.resolve();
|
|
1189
|
+
},
|
|
1190
|
+
insert: async (table, data) => {
|
|
1191
|
+
validateWrite(table, "insert", data);
|
|
1192
|
+
if (enforce) {
|
|
1193
|
+
await authorizeWrite(table, "insert", data, ctx);
|
|
1194
|
+
}
|
|
1195
|
+
const merged = await mergeCrdtFields(table, "insert", data, ctx);
|
|
1196
|
+
const row = await writerFor(table).insert(merged, ctx, tx);
|
|
1197
|
+
buffered.push({ table, change: { op: "insert", row } });
|
|
1198
|
+
return row;
|
|
1199
|
+
},
|
|
1200
|
+
update: async (table, data) => {
|
|
1201
|
+
validateWrite(table, "update", data);
|
|
1202
|
+
if (enforce) {
|
|
1203
|
+
await authorizeWrite(table, "update", data, ctx);
|
|
1204
|
+
}
|
|
1205
|
+
const merged = await mergeCrdtFields(table, "update", data, ctx);
|
|
1206
|
+
const row = await writerFor(table).update(merged, ctx, tx);
|
|
1207
|
+
buffered.push({ table, change: { op: "update", row } });
|
|
1208
|
+
return row;
|
|
1209
|
+
},
|
|
1210
|
+
delete: async (table, row) => {
|
|
1211
|
+
if (enforce) {
|
|
1212
|
+
await authorizeWrite(table, "delete", row, ctx);
|
|
1213
|
+
}
|
|
1214
|
+
await writerFor(table).delete(row, ctx, tx);
|
|
1215
|
+
buffered.push({ table, change: { op: "delete", row } });
|
|
1216
|
+
},
|
|
1217
|
+
now: () => Date.now()
|
|
1218
|
+
};
|
|
1219
|
+
return { actions, buffered };
|
|
1220
|
+
};
|
|
1221
|
+
const diffRerun = (sub, rows, equals = shallowEqual3) => {
|
|
1222
|
+
const next = new Map;
|
|
1223
|
+
for (const row of rows) {
|
|
1224
|
+
next.set(sub.key(row), row);
|
|
1225
|
+
}
|
|
1226
|
+
const added = [];
|
|
1227
|
+
const removed = [];
|
|
1228
|
+
const changed = [];
|
|
1229
|
+
for (const [rowKey, row] of next) {
|
|
1230
|
+
const previous = sub.current.get(rowKey);
|
|
1231
|
+
if (previous === undefined) {
|
|
1232
|
+
added.push(row);
|
|
1233
|
+
} else if (!equals(previous, row)) {
|
|
1234
|
+
changed.push(row);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
for (const [rowKey, row] of sub.current) {
|
|
1238
|
+
if (!next.has(rowKey)) {
|
|
1239
|
+
removed.push(row);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
sub.current = next;
|
|
1243
|
+
return { added, removed, changed };
|
|
1244
|
+
};
|
|
1245
|
+
const inRange = (dep, change) => dep.table === change.table && (change.key !== undefined && dep.keys.has(change.key) || dep.predicate(change.row));
|
|
1246
|
+
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)));
|
|
1247
|
+
const isReactiveAffected = (sub, changes) => readSetOverlaps(sub.readTables, sub.readKeys, sub.rangeDeps, changes);
|
|
1248
|
+
const invalidateCacheForChanges = (changes) => {
|
|
1249
|
+
if (cachedReruns.size === 0)
|
|
1250
|
+
return;
|
|
1251
|
+
for (const [key, entry] of cachedReruns) {
|
|
1252
|
+
if (readSetOverlaps(entry.readTables, entry.readKeys, entry.rangeDeps, changes)) {
|
|
1253
|
+
cachedReruns.delete(key);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
const reactivePairs = async (changes) => {
|
|
1258
|
+
invalidateCacheForChanges(changes);
|
|
1259
|
+
const pairs = [];
|
|
1260
|
+
const sharedRuns = new Map;
|
|
1261
|
+
for (const sub of reactiveSubs) {
|
|
1262
|
+
if (!isReactiveAffected(sub, changes)) {
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
let runPromise = sharedRuns.get(sub.rerunKey);
|
|
1266
|
+
if (runPromise === undefined) {
|
|
1267
|
+
runPromise = sub.rerun();
|
|
1268
|
+
sharedRuns.set(sub.rerunKey, runPromise);
|
|
1269
|
+
}
|
|
1270
|
+
const { rows, readTables, readKeys, rangeDeps } = await runPromise;
|
|
1271
|
+
sub.readTables = readTables;
|
|
1272
|
+
sub.readKeys = readKeys;
|
|
1273
|
+
sub.rangeDeps = rangeDeps;
|
|
1274
|
+
const diff = diffRerun(sub, rows);
|
|
1275
|
+
if (!isEmptyViewDiff(diff)) {
|
|
1276
|
+
pairs.push([sub, diff]);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
for (const [key, runPromise] of sharedRuns) {
|
|
1280
|
+
runPromise.then(({ rows, readTables, readKeys, rangeDeps }) => {
|
|
1281
|
+
writeCacheEntry({
|
|
1282
|
+
expiresAt: Date.now() + reactiveCacheTtlMs,
|
|
1283
|
+
rangeDeps,
|
|
1284
|
+
readKeys,
|
|
1285
|
+
readTables,
|
|
1286
|
+
rerunKey: key,
|
|
1287
|
+
rows,
|
|
1288
|
+
version
|
|
1289
|
+
});
|
|
1290
|
+
}).catch(() => {});
|
|
1291
|
+
}
|
|
1292
|
+
return pairs;
|
|
1293
|
+
};
|
|
1294
|
+
const ensureSearchIndex = async (definition) => {
|
|
1295
|
+
let entry = searchIndexes.get(definition.name);
|
|
1296
|
+
if (entry === undefined) {
|
|
1297
|
+
entry = { index: definition.index(), definition, hydrated: false };
|
|
1298
|
+
searchIndexes.set(definition.name, entry);
|
|
1299
|
+
}
|
|
1300
|
+
if (!entry.hydrated) {
|
|
1301
|
+
for (const row of await definition.source()) {
|
|
1302
|
+
entry.index.add(row);
|
|
1303
|
+
}
|
|
1304
|
+
entry.hydrated = true;
|
|
1305
|
+
}
|
|
1306
|
+
return entry;
|
|
1307
|
+
};
|
|
1308
|
+
const searchPairs = (changes) => {
|
|
1309
|
+
const touched = new Set;
|
|
1310
|
+
for (const { table, change } of changes) {
|
|
1311
|
+
for (const entry of searchIndexes.values()) {
|
|
1312
|
+
if (!entry.hydrated || entry.definition.table !== table) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (change.op === "delete") {
|
|
1316
|
+
entry.index.remove(entry.definition.key(change.row));
|
|
1317
|
+
} else {
|
|
1318
|
+
entry.index.add(change.row);
|
|
1319
|
+
}
|
|
1320
|
+
touched.add(entry.definition.name);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
const pairs = [];
|
|
1324
|
+
for (const sub of searchSubs) {
|
|
1325
|
+
if (!touched.has(sub.collection)) {
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const diff = diffRerun(sub, sub.rerun(), equalsIgnoringScore);
|
|
1329
|
+
if (!isEmptyViewDiff(diff)) {
|
|
1330
|
+
pairs.push([sub, diff]);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return pairs;
|
|
1334
|
+
};
|
|
1335
|
+
const logChange = (changeVersion, entry) => {
|
|
1336
|
+
changeLog.push(entry);
|
|
1337
|
+
if (changeLog.length > changeLogSize) {
|
|
1338
|
+
changeLog.shift();
|
|
1339
|
+
}
|
|
1340
|
+
if (changeLogRetainMs !== null && changeLogRetainMs > 0) {
|
|
1341
|
+
const cutoff = entry.at - changeLogRetainMs;
|
|
1342
|
+
while (changeLog.length > 0 && changeLog[0].at < cutoff) {
|
|
1343
|
+
changeLog.shift();
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
for (const subscriber of streamSubscribers) {
|
|
1347
|
+
subscriber(entry);
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
const encodeCursor = (versions) => JSON.stringify(versions);
|
|
1351
|
+
const decodeCursor = (cursor) => {
|
|
1352
|
+
try {
|
|
1353
|
+
const parsed = JSON.parse(cursor);
|
|
1354
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1355
|
+
return null;
|
|
1356
|
+
const out = {};
|
|
1357
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1358
|
+
if (typeof v === "number")
|
|
1359
|
+
out[k] = v;
|
|
1360
|
+
}
|
|
1361
|
+
return out;
|
|
1362
|
+
} catch {
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
const currentCursor = () => {
|
|
1367
|
+
const versions = { [instanceId]: version };
|
|
1368
|
+
for (let i = changeLog.length - 1;i >= 0; i--) {
|
|
1369
|
+
const entry = changeLog[i];
|
|
1370
|
+
if (versions[entry.origin] === undefined) {
|
|
1371
|
+
versions[entry.origin] = entry.originVersion;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return encodeCursor(versions);
|
|
1375
|
+
};
|
|
1376
|
+
const applyChange = async (table, change, shouldBroadcast = true) => {
|
|
1377
|
+
version += 1;
|
|
1378
|
+
const changeVersion = version;
|
|
1379
|
+
const at = Date.now();
|
|
1380
|
+
logChange(changeVersion, {
|
|
1381
|
+
version: changeVersion,
|
|
1382
|
+
table,
|
|
1383
|
+
change,
|
|
1384
|
+
at,
|
|
1385
|
+
origin: instanceId,
|
|
1386
|
+
originVersion: changeVersion
|
|
1387
|
+
});
|
|
1388
|
+
emitActivity({
|
|
1389
|
+
type: "change",
|
|
1390
|
+
at,
|
|
1391
|
+
table,
|
|
1392
|
+
op: change.op,
|
|
1393
|
+
version: changeVersion
|
|
1394
|
+
});
|
|
1395
|
+
const emissions = [];
|
|
1396
|
+
for (const subscription of subscriptionsForTable(table)) {
|
|
1397
|
+
const diff = await subscriptionDiff(subscription, table, change);
|
|
1398
|
+
if (!isEmptyViewDiff(diff)) {
|
|
1399
|
+
emissions.push([subscription, diff]);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
emissions.push(...await reactivePairs([
|
|
1403
|
+
{ table, key: changedKeyFor(table, change), row: change.row }
|
|
1404
|
+
]));
|
|
1405
|
+
emissions.push(...searchPairs([{ table, change }]));
|
|
1406
|
+
const cursorForBatch = currentCursor();
|
|
1407
|
+
for (const [subscription, diff] of emissions) {
|
|
1408
|
+
subscription.onDiff(diff, changeVersion, cursorForBatch);
|
|
1409
|
+
}
|
|
1410
|
+
if (shouldBroadcast) {
|
|
1411
|
+
broadcast([{ table, change }], changeVersion);
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
const applyChangeBatch = async (changes, shouldBroadcast = true, peerOrigin) => {
|
|
1415
|
+
if (changes.length === 0) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
version += 1;
|
|
1419
|
+
const batchVersion = version;
|
|
1420
|
+
const perSubscription = new Map;
|
|
1421
|
+
const reactiveChanges = [];
|
|
1422
|
+
const batchAt = Date.now();
|
|
1423
|
+
const batchOrigin = peerOrigin?.origin ?? instanceId;
|
|
1424
|
+
const batchOriginVersion = peerOrigin?.originVersion ?? batchVersion;
|
|
1425
|
+
for (const { table, change } of changes) {
|
|
1426
|
+
logChange(batchVersion, {
|
|
1427
|
+
version: batchVersion,
|
|
1428
|
+
table,
|
|
1429
|
+
change,
|
|
1430
|
+
at: batchAt,
|
|
1431
|
+
origin: batchOrigin,
|
|
1432
|
+
originVersion: batchOriginVersion
|
|
1433
|
+
});
|
|
1434
|
+
emitActivity({
|
|
1435
|
+
type: "change",
|
|
1436
|
+
at: batchAt,
|
|
1437
|
+
table,
|
|
1438
|
+
op: change.op,
|
|
1439
|
+
version: batchVersion
|
|
1440
|
+
});
|
|
1441
|
+
reactiveChanges.push({
|
|
1442
|
+
table,
|
|
1443
|
+
key: changedKeyFor(table, change),
|
|
1444
|
+
row: change.row
|
|
1445
|
+
});
|
|
1446
|
+
for (const subscription of subscriptionsForTable(table)) {
|
|
1447
|
+
const diff = await subscriptionDiff(subscription, table, change);
|
|
1448
|
+
const list = perSubscription.get(subscription);
|
|
1449
|
+
if (list === undefined) {
|
|
1450
|
+
perSubscription.set(subscription, [diff]);
|
|
1451
|
+
} else {
|
|
1452
|
+
list.push(diff);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const emissions = [];
|
|
1457
|
+
for (const [subscription, diffs] of perSubscription) {
|
|
1458
|
+
const merged = diffs.length === 1 ? diffs[0] : mergeViewDiffs(diffs, subscription.key);
|
|
1459
|
+
if (!isEmptyViewDiff(merged)) {
|
|
1460
|
+
emissions.push([subscription, merged]);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
emissions.push(...await reactivePairs(reactiveChanges));
|
|
1464
|
+
emissions.push(...searchPairs(changes));
|
|
1465
|
+
const cursorForBatch = currentCursor();
|
|
1466
|
+
for (const [subscription, diff] of emissions) {
|
|
1467
|
+
subscription.onDiff(diff, batchVersion, cursorForBatch);
|
|
1468
|
+
}
|
|
1469
|
+
if (shouldBroadcast) {
|
|
1470
|
+
broadcast(changes, batchVersion);
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
const normalizeSince = (since) => {
|
|
1474
|
+
if (typeof since === "number") {
|
|
1475
|
+
return { [instanceId]: since };
|
|
1476
|
+
}
|
|
1477
|
+
return decodeCursor(since);
|
|
1478
|
+
};
|
|
1479
|
+
const canResume = (since, incremental) => {
|
|
1480
|
+
if (!incremental) {
|
|
1481
|
+
return false;
|
|
1482
|
+
}
|
|
1483
|
+
const sinceVec = normalizeSince(since);
|
|
1484
|
+
if (sinceVec === null) {
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
const oldestPerOrigin = new Map;
|
|
1488
|
+
for (const entry of changeLog) {
|
|
1489
|
+
const current = oldestPerOrigin.get(entry.origin);
|
|
1490
|
+
if (current === undefined || entry.originVersion < current) {
|
|
1491
|
+
oldestPerOrigin.set(entry.origin, entry.originVersion);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
const oldestLogVersion = changeLog[0]?.version;
|
|
1495
|
+
for (const [origin, lastSeen] of Object.entries(sinceVec)) {
|
|
1496
|
+
if (origin === instanceId) {
|
|
1497
|
+
if (lastSeen >= version)
|
|
1498
|
+
continue;
|
|
1499
|
+
const oldestLocal = oldestPerOrigin.get(instanceId);
|
|
1500
|
+
if (oldestLocal !== undefined) {
|
|
1501
|
+
if (oldestLocal > lastSeen + 1)
|
|
1502
|
+
return false;
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
if (oldestLogVersion !== undefined && oldestLogVersion > lastSeen + 1) {
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
} else {
|
|
1509
|
+
const oldestPeer = oldestPerOrigin.get(origin);
|
|
1510
|
+
if (oldestPeer === undefined) {
|
|
1511
|
+
if (lastSeen > 0)
|
|
1512
|
+
return false;
|
|
1513
|
+
} else if (oldestPeer > lastSeen + 1) {
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return true;
|
|
1519
|
+
};
|
|
1520
|
+
const buildCatchup = (since, tables, key, match) => {
|
|
1521
|
+
const sinceVec = normalizeSince(since) ?? {};
|
|
1522
|
+
const latest = new Map;
|
|
1523
|
+
for (const entry of changeLog) {
|
|
1524
|
+
if (!tables.includes(entry.table))
|
|
1525
|
+
continue;
|
|
1526
|
+
const lastSeen = sinceVec[entry.origin];
|
|
1527
|
+
if (lastSeen !== undefined && entry.originVersion <= lastSeen)
|
|
1528
|
+
continue;
|
|
1529
|
+
const row = entry.change.row;
|
|
1530
|
+
const present = entry.change.op !== "delete" && match(row) ? "upsert" : "remove";
|
|
1531
|
+
latest.set(key(row), { op: present, row });
|
|
1532
|
+
}
|
|
1533
|
+
const changed = [];
|
|
1534
|
+
const removed = [];
|
|
1535
|
+
for (const { op, row } of latest.values()) {
|
|
1536
|
+
(op === "upsert" ? changed : removed).push(row);
|
|
1537
|
+
}
|
|
1538
|
+
return { added: [], removed, changed };
|
|
1539
|
+
};
|
|
1540
|
+
const subscribeJoin = async (collection, definition, params, ctx, onDiff, set) => {
|
|
1541
|
+
if (definition.authorize !== undefined) {
|
|
1542
|
+
const allowed = await definition.authorize(params, ctx);
|
|
1543
|
+
if (!allowed) {
|
|
1544
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
const { left, right } = definition;
|
|
1548
|
+
const op = createEquiJoin({
|
|
1549
|
+
leftKey: left.key,
|
|
1550
|
+
rightKey: right.key,
|
|
1551
|
+
leftOn: left.on,
|
|
1552
|
+
rightOn: right.on,
|
|
1553
|
+
select: definition.select
|
|
1554
|
+
});
|
|
1555
|
+
op.hydrate([...await left.hydrate(params, ctx)], [...await right.hydrate(params, ctx)]);
|
|
1556
|
+
const atVersion = version;
|
|
1557
|
+
const subscription = {
|
|
1558
|
+
kind: "join",
|
|
1559
|
+
collection,
|
|
1560
|
+
join: {
|
|
1561
|
+
op,
|
|
1562
|
+
leftTable: left.table,
|
|
1563
|
+
rightTable: right.table,
|
|
1564
|
+
leftMatch: left.match ? (row) => left.match(row, params, ctx) : undefined,
|
|
1565
|
+
rightMatch: right.match ? (row) => right.match(row, params, ctx) : undefined
|
|
1566
|
+
},
|
|
1567
|
+
key: definition.key,
|
|
1568
|
+
onDiff
|
|
1569
|
+
};
|
|
1570
|
+
set.add(subscription);
|
|
1571
|
+
return {
|
|
1572
|
+
initial: op.rows(),
|
|
1573
|
+
cursor: currentCursor(),
|
|
1574
|
+
version: atVersion,
|
|
1575
|
+
unsubscribe: () => {
|
|
1576
|
+
set.delete(subscription);
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
};
|
|
1580
|
+
const subscribeGraph = async (collection, definition, params, ctx, onDiff, set) => {
|
|
1581
|
+
if (definition.authorize !== undefined) {
|
|
1582
|
+
const allowed = await definition.authorize(params, ctx);
|
|
1583
|
+
if (!allowed) {
|
|
1584
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const instance = definition.query.instantiate(params, ctx);
|
|
1588
|
+
const initial = await instance.hydrate();
|
|
1589
|
+
const atVersion = version;
|
|
1590
|
+
const subscription = {
|
|
1591
|
+
kind: "graph",
|
|
1592
|
+
collection,
|
|
1593
|
+
instance,
|
|
1594
|
+
key: definition.key,
|
|
1595
|
+
onDiff
|
|
1596
|
+
};
|
|
1597
|
+
set.add(subscription);
|
|
1598
|
+
return {
|
|
1599
|
+
initial,
|
|
1600
|
+
cursor: currentCursor(),
|
|
1601
|
+
version: atVersion,
|
|
1602
|
+
unsubscribe: () => {
|
|
1603
|
+
set.delete(subscription);
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
};
|
|
1607
|
+
const subscribeReactive = async (collection, definition, params, ctx, onDiff, set) => {
|
|
1608
|
+
if (definition.authorize !== undefined) {
|
|
1609
|
+
const allowed = await definition.authorize(params, ctx);
|
|
1610
|
+
if (!allowed) {
|
|
1611
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
const rerun = async () => {
|
|
1615
|
+
const readTables = new Set;
|
|
1616
|
+
const readKeys = new Set;
|
|
1617
|
+
const rangeDeps = [];
|
|
1618
|
+
const db = makeReadHandle(ctx, readTables, readKeys, rangeDeps);
|
|
1619
|
+
const rows = [...await definition.run({ ctx, db, params })];
|
|
1620
|
+
return { rangeDeps, readKeys, readTables, rows };
|
|
1621
|
+
};
|
|
1622
|
+
const rerunKey = stableSubKey(collection, params, ctx);
|
|
1623
|
+
const cached = readCacheEntry(rerunKey);
|
|
1624
|
+
const first = cached !== undefined ? {
|
|
1625
|
+
rangeDeps: cached.rangeDeps,
|
|
1626
|
+
readKeys: cached.readKeys,
|
|
1627
|
+
readTables: cached.readTables,
|
|
1628
|
+
rows: cached.rows
|
|
1629
|
+
} : await rerun();
|
|
1630
|
+
if (cached === undefined) {
|
|
1631
|
+
writeCacheEntry({
|
|
1632
|
+
expiresAt: Date.now() + reactiveCacheTtlMs,
|
|
1633
|
+
rangeDeps: first.rangeDeps,
|
|
1634
|
+
readKeys: first.readKeys,
|
|
1635
|
+
readTables: first.readTables,
|
|
1636
|
+
rerunKey,
|
|
1637
|
+
rows: first.rows,
|
|
1638
|
+
version
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
const current = new Map;
|
|
1642
|
+
for (const row of first.rows) {
|
|
1643
|
+
current.set(definition.key(row), row);
|
|
1644
|
+
}
|
|
1645
|
+
const atVersion = version;
|
|
1646
|
+
const subscription = {
|
|
1647
|
+
kind: "reactive",
|
|
1648
|
+
collection,
|
|
1649
|
+
key: definition.key,
|
|
1650
|
+
rerun,
|
|
1651
|
+
rerunKey,
|
|
1652
|
+
current,
|
|
1653
|
+
readTables: first.readTables,
|
|
1654
|
+
readKeys: first.readKeys,
|
|
1655
|
+
rangeDeps: first.rangeDeps,
|
|
1656
|
+
onDiff
|
|
1657
|
+
};
|
|
1658
|
+
set.add(subscription);
|
|
1659
|
+
reactiveSubs.add(subscription);
|
|
1660
|
+
return {
|
|
1661
|
+
initial: first.rows,
|
|
1662
|
+
cursor: currentCursor(),
|
|
1663
|
+
version: atVersion,
|
|
1664
|
+
unsubscribe: () => {
|
|
1665
|
+
set.delete(subscription);
|
|
1666
|
+
reactiveSubs.delete(subscription);
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
};
|
|
1670
|
+
const subscribeSearch = async (collection, definition, params, ctx, onDiff, set) => {
|
|
1671
|
+
const query = params;
|
|
1672
|
+
if (definition.authorize !== undefined) {
|
|
1673
|
+
const allowed = await definition.authorize(query, ctx);
|
|
1674
|
+
if (!allowed) {
|
|
1675
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
const entry = await ensureSearchIndex(definition);
|
|
1679
|
+
const limit = definition.limit ?? 20;
|
|
1680
|
+
const readRule = readRuleFor(definition.table);
|
|
1681
|
+
const rerun = () => {
|
|
1682
|
+
const candidates = entry.index.search(query, readRule ? limit * 5 : limit);
|
|
1683
|
+
const visible = readRule ? candidates.filter((hit) => readRule(ctx, hit.row)) : candidates;
|
|
1684
|
+
return visible.slice(0, limit).map((hit) => ({
|
|
1685
|
+
...hit.row,
|
|
1686
|
+
[SEARCH_SCORE_FIELD]: hit.score
|
|
1687
|
+
}));
|
|
1688
|
+
};
|
|
1689
|
+
const initial = rerun();
|
|
1690
|
+
const current = new Map;
|
|
1691
|
+
for (const row of initial) {
|
|
1692
|
+
current.set(definition.key(row), row);
|
|
1693
|
+
}
|
|
1694
|
+
const atVersion = version;
|
|
1695
|
+
const subscription = {
|
|
1696
|
+
kind: "search",
|
|
1697
|
+
collection,
|
|
1698
|
+
key: definition.key,
|
|
1699
|
+
rerun,
|
|
1700
|
+
current,
|
|
1701
|
+
onDiff
|
|
1702
|
+
};
|
|
1703
|
+
set.add(subscription);
|
|
1704
|
+
searchSubs.add(subscription);
|
|
1705
|
+
return {
|
|
1706
|
+
initial,
|
|
1707
|
+
cursor: currentCursor(),
|
|
1708
|
+
version: atVersion,
|
|
1709
|
+
unsubscribe: () => {
|
|
1710
|
+
set.delete(subscription);
|
|
1711
|
+
searchSubs.delete(subscription);
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
};
|
|
1715
|
+
const engine = {
|
|
1716
|
+
register: (collection) => {
|
|
1717
|
+
registry.set(collection.name, collection);
|
|
1718
|
+
for (const table of collection.tables ?? [collection.name]) {
|
|
1719
|
+
addTableIndex(table, collection.name);
|
|
1720
|
+
}
|
|
1721
|
+
},
|
|
1722
|
+
registerJoin: (collection) => {
|
|
1723
|
+
registry.set(collection.name, collection);
|
|
1724
|
+
addTableIndex(collection.left.table, collection.name);
|
|
1725
|
+
addTableIndex(collection.right.table, collection.name);
|
|
1726
|
+
},
|
|
1727
|
+
registerGraph: (collection) => {
|
|
1728
|
+
registry.set(collection.name, collection);
|
|
1729
|
+
for (const table of collection.query.tables()) {
|
|
1730
|
+
addTableIndex(table, collection.name);
|
|
1731
|
+
}
|
|
1732
|
+
},
|
|
1733
|
+
registerSearch: (collection) => {
|
|
1734
|
+
registry.set(collection.name, collection);
|
|
1735
|
+
},
|
|
1736
|
+
subscribe: async ({
|
|
1737
|
+
collection,
|
|
1738
|
+
params,
|
|
1739
|
+
ctx,
|
|
1740
|
+
onDiff,
|
|
1741
|
+
since,
|
|
1742
|
+
signal
|
|
1743
|
+
}) => {
|
|
1744
|
+
const subscribeSpan = tracer.startSpan("sync.subscribe", {
|
|
1745
|
+
attributes: {
|
|
1746
|
+
[ABS_ATTRS.engineId]: instanceId,
|
|
1747
|
+
[ABS_ATTRS.collection]: collection
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
try {
|
|
1751
|
+
checkAborted(signal);
|
|
1752
|
+
const registered = registry.get(collection);
|
|
1753
|
+
if (registered === undefined) {
|
|
1754
|
+
throw new Error(`Unknown collection "${collection}"`);
|
|
1755
|
+
}
|
|
1756
|
+
const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
|
|
1757
|
+
let slotHandedOff = false;
|
|
1758
|
+
try {
|
|
1759
|
+
const typedOnDiff = onDiff;
|
|
1760
|
+
const subscribeSet = subsFor(collection);
|
|
1761
|
+
const wrapReturn = (sub) => {
|
|
1762
|
+
checkAborted(signal);
|
|
1763
|
+
const innerUnsubscribe = sub.unsubscribe;
|
|
1764
|
+
let released = false;
|
|
1765
|
+
const wrappedUnsubscribe = () => {
|
|
1766
|
+
if (released)
|
|
1767
|
+
return;
|
|
1768
|
+
released = true;
|
|
1769
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
1770
|
+
innerUnsubscribe();
|
|
1771
|
+
};
|
|
1772
|
+
const wrapped = {
|
|
1773
|
+
...sub,
|
|
1774
|
+
unsubscribe: wrappedUnsubscribe
|
|
1775
|
+
};
|
|
1776
|
+
linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
|
|
1777
|
+
slotHandedOff = true;
|
|
1778
|
+
return wrapped;
|
|
1779
|
+
};
|
|
1780
|
+
const registeredKind = registered.kind;
|
|
1781
|
+
if (registeredKind === "join") {
|
|
1782
|
+
const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
1783
|
+
return wrapReturn(joined);
|
|
1784
|
+
}
|
|
1785
|
+
if (registeredKind === "graph") {
|
|
1786
|
+
const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
1787
|
+
return wrapReturn(graphed);
|
|
1788
|
+
}
|
|
1789
|
+
if (registeredKind === "reactive") {
|
|
1790
|
+
const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
1791
|
+
return wrapReturn(reactived);
|
|
1792
|
+
}
|
|
1793
|
+
if (registeredKind === "search") {
|
|
1794
|
+
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
1795
|
+
return wrapReturn(searched);
|
|
1796
|
+
}
|
|
1797
|
+
const definition = registered;
|
|
1798
|
+
if (definition.authorize !== undefined) {
|
|
1799
|
+
const allowed = await definition.authorize(params, ctx);
|
|
1800
|
+
if (!allowed) {
|
|
1801
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
const key = definition.key ?? defaultKey;
|
|
1805
|
+
const match = definition.match;
|
|
1806
|
+
const tables = definition.tables ?? [collection];
|
|
1807
|
+
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
1808
|
+
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
1809
|
+
const rehydrate = async () => {
|
|
1810
|
+
const raw = [
|
|
1811
|
+
...await definition.hydrate(params, ctx)
|
|
1812
|
+
];
|
|
1813
|
+
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
1814
|
+
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
1815
|
+
};
|
|
1816
|
+
const incremental = match !== undefined && tables.length === 1;
|
|
1817
|
+
const boundAffects = !incremental && definition.affects ? (change) => definition.affects(change, params, ctx) : undefined;
|
|
1818
|
+
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
1819
|
+
const view = createMaterializedView({
|
|
1820
|
+
key,
|
|
1821
|
+
match: boundMatch
|
|
1822
|
+
});
|
|
1823
|
+
const resuming = since !== undefined && canResume(since, incremental);
|
|
1824
|
+
view.hydrate([...await rehydrate()]);
|
|
1825
|
+
const atVersion = version;
|
|
1826
|
+
const subscription = {
|
|
1827
|
+
kind: "view",
|
|
1828
|
+
collection,
|
|
1829
|
+
view,
|
|
1830
|
+
incremental,
|
|
1831
|
+
rehydrate,
|
|
1832
|
+
...boundAffects ? { affects: boundAffects } : {},
|
|
1833
|
+
key,
|
|
1834
|
+
onDiff: typedOnDiff
|
|
1835
|
+
};
|
|
1836
|
+
subscribeSet.add(subscription);
|
|
1837
|
+
const unsubscribe = () => {
|
|
1838
|
+
subscribeSet.delete(subscription);
|
|
1839
|
+
};
|
|
1840
|
+
if (resuming) {
|
|
1841
|
+
return wrapReturn({
|
|
1842
|
+
initial: [],
|
|
1843
|
+
catchup: buildCatchup(since, tables, key, boundMatch),
|
|
1844
|
+
cursor: currentCursor(),
|
|
1845
|
+
version: atVersion,
|
|
1846
|
+
unsubscribe
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
return wrapReturn({
|
|
1850
|
+
initial: view.rows(),
|
|
1851
|
+
cursor: currentCursor(),
|
|
1852
|
+
version: atVersion,
|
|
1853
|
+
unsubscribe
|
|
1854
|
+
});
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
if (!slotHandedOff)
|
|
1857
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
1858
|
+
throw error;
|
|
1859
|
+
}
|
|
1860
|
+
} catch (spanError) {
|
|
1861
|
+
subscribeSpan.recordException(spanError);
|
|
1862
|
+
subscribeSpan.setStatus({
|
|
1863
|
+
code: 2,
|
|
1864
|
+
message: spanError instanceof Error ? spanError.message : String(spanError)
|
|
1865
|
+
});
|
|
1866
|
+
throw spanError;
|
|
1867
|
+
} finally {
|
|
1868
|
+
subscribeSpan.end();
|
|
1869
|
+
}
|
|
1870
|
+
},
|
|
1871
|
+
hydrate: async (collection, params, ctx, options2) => {
|
|
1872
|
+
const signal = options2?.signal;
|
|
1873
|
+
checkAborted(signal);
|
|
1874
|
+
const definition = registry.get(collection);
|
|
1875
|
+
if (definition === undefined) {
|
|
1876
|
+
throw new Error(`Unknown collection "${collection}"`);
|
|
1877
|
+
}
|
|
1878
|
+
if (definition.authorize !== undefined) {
|
|
1879
|
+
const allowed = await definition.authorize(params, ctx);
|
|
1880
|
+
checkAborted(signal);
|
|
1881
|
+
if (!allowed) {
|
|
1882
|
+
throw new UnauthorizedError(`hydrate collection "${collection}"`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
const raw = [...await definition.hydrate(params, ctx)];
|
|
1886
|
+
checkAborted(signal);
|
|
1887
|
+
const tables = definition.tables ?? [collection];
|
|
1888
|
+
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
1889
|
+
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
1890
|
+
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
1891
|
+
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
1892
|
+
},
|
|
1893
|
+
applyChange: (table, change) => applyChange(table, change),
|
|
1894
|
+
connectSource: async (source) => {
|
|
1895
|
+
await source.start((table, change) => {
|
|
1896
|
+
sourceChangesReceived += 1;
|
|
1897
|
+
sourceLastChangeAt = Date.now();
|
|
1898
|
+
return applyChange(table, change);
|
|
1899
|
+
});
|
|
1900
|
+
connectedSources += 1;
|
|
1901
|
+
let stopped = false;
|
|
1902
|
+
return async () => {
|
|
1903
|
+
if (!stopped) {
|
|
1904
|
+
stopped = true;
|
|
1905
|
+
connectedSources -= 1;
|
|
1906
|
+
}
|
|
1907
|
+
await source.stop();
|
|
1908
|
+
};
|
|
1909
|
+
},
|
|
1910
|
+
connectCluster: async (bus) => {
|
|
1911
|
+
const unsubscribe = await bus.subscribe((message) => {
|
|
1912
|
+
if (message.origin === instanceId) {
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
applyChangeBatch(message.changes, false, {
|
|
1916
|
+
origin: message.origin,
|
|
1917
|
+
originVersion: message.originVersion ?? 0
|
|
1918
|
+
});
|
|
1919
|
+
});
|
|
1920
|
+
clusterBus = bus;
|
|
1921
|
+
return async () => {
|
|
1922
|
+
clusterBus = undefined;
|
|
1923
|
+
await unsubscribe();
|
|
1924
|
+
};
|
|
1925
|
+
},
|
|
1926
|
+
subscriptionCount: (collection) => {
|
|
1927
|
+
if (collection !== undefined) {
|
|
1928
|
+
return active.get(collection)?.size ?? 0;
|
|
1929
|
+
}
|
|
1930
|
+
let total = 0;
|
|
1931
|
+
for (const set of active.values()) {
|
|
1932
|
+
total += set.size;
|
|
1933
|
+
}
|
|
1934
|
+
return total;
|
|
1935
|
+
},
|
|
1936
|
+
registerMutation: (mutation) => {
|
|
1937
|
+
if (mutation.handler === undefined && mutation.sandboxedHandler === undefined) {
|
|
1938
|
+
throw new Error(`Mutation "${mutation.name}" must define either \`handler\` or \`sandboxedHandler\``);
|
|
1939
|
+
}
|
|
1940
|
+
if (mutation.handler !== undefined && mutation.sandboxedHandler !== undefined) {
|
|
1941
|
+
throw new Error(`Mutation "${mutation.name}" defines both \`handler\` and \`sandboxedHandler\` \u2014 pick one`);
|
|
1942
|
+
}
|
|
1943
|
+
mutations.set(mutation.name, mutation);
|
|
1944
|
+
if (mutation.sandboxedHandler !== undefined) {
|
|
1945
|
+
sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox, {
|
|
1946
|
+
bridgeFetch: options.bridgeFetch,
|
|
1947
|
+
metricsHook: options.handlerMetrics === undefined ? undefined : {
|
|
1948
|
+
mutationName: mutation.name,
|
|
1949
|
+
onMetrics: options.handlerMetrics
|
|
1950
|
+
}
|
|
1951
|
+
}));
|
|
1952
|
+
}
|
|
1953
|
+
},
|
|
1954
|
+
registerWriter: (table, writer) => {
|
|
1955
|
+
writers.set(table, writer);
|
|
1956
|
+
},
|
|
1957
|
+
registerReactive: (query) => {
|
|
1958
|
+
registry.set(query.name, query);
|
|
1959
|
+
},
|
|
1960
|
+
registerReader: (table, reader) => {
|
|
1961
|
+
readers.set(table, reader);
|
|
1962
|
+
},
|
|
1963
|
+
registerPermissions: (table, rules) => {
|
|
1964
|
+
permissions.set(table, rules);
|
|
1965
|
+
},
|
|
1966
|
+
registerSchema: (table, schema) => {
|
|
1967
|
+
schemas.set(table, schema);
|
|
1968
|
+
},
|
|
1969
|
+
registerCrdt: (table, fields) => {
|
|
1970
|
+
crdtFields.set(table, fields);
|
|
1971
|
+
const name = `${table}:merge`;
|
|
1972
|
+
mutations.set(name, {
|
|
1973
|
+
handler: async (args, ctx, actions) => {
|
|
1974
|
+
const existing = await readExisting(table, args, ctx);
|
|
1975
|
+
return existing === undefined ? actions.insert(table, args) : actions.update(table, args);
|
|
1976
|
+
},
|
|
1977
|
+
name
|
|
1978
|
+
});
|
|
1979
|
+
},
|
|
1980
|
+
migrate: (table, row) => migrateRow(table, row),
|
|
1981
|
+
runMutation: async (name, args, ctx) => {
|
|
1982
|
+
const span = tracer.startSpan("sync.runMutation", {
|
|
1983
|
+
attributes: {
|
|
1984
|
+
[ABS_ATTRS.engineId]: instanceId,
|
|
1985
|
+
[ABS_ATTRS.mutation]: name
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
try {
|
|
1989
|
+
if (activeFences.size > 0) {
|
|
1990
|
+
const oldest = activeFences.values().next().value;
|
|
1991
|
+
throw new EngineFencedError(oldest.reason);
|
|
1992
|
+
}
|
|
1993
|
+
const mutation = mutations.get(name);
|
|
1994
|
+
if (mutation === undefined) {
|
|
1995
|
+
throw new Error(`Unknown mutation "${name}"`);
|
|
1996
|
+
}
|
|
1997
|
+
if (mutation.authorize !== undefined) {
|
|
1998
|
+
const allowed = await mutation.authorize(args, ctx);
|
|
1999
|
+
if (!allowed) {
|
|
2000
|
+
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
await acquireMutationSlot();
|
|
2004
|
+
const sandboxRunner = sandboxRunners.get(name);
|
|
2005
|
+
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
|
|
2006
|
+
const runHandler = async (tx) => {
|
|
2007
|
+
const { actions, buffered } = makeActions(tx, ctx, true);
|
|
2008
|
+
const result = await invokeHandler(args, ctx, actions);
|
|
2009
|
+
return { buffered, result };
|
|
2010
|
+
};
|
|
2011
|
+
const retry = mutation.retry;
|
|
2012
|
+
const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
|
|
2013
|
+
const isRetryable = retry?.isRetryable ?? isSerializationFailure;
|
|
2014
|
+
const computeDelay = retry?.backoff ?? exponentialBackoff();
|
|
2015
|
+
const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
|
|
2016
|
+
const startedAt = Date.now();
|
|
2017
|
+
let lastError;
|
|
2018
|
+
let attemptsMade = 0;
|
|
2019
|
+
try {
|
|
2020
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2021
|
+
attemptsMade = attempt;
|
|
2022
|
+
try {
|
|
2023
|
+
const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
|
|
2024
|
+
await applyChangeBatch(buffered);
|
|
2025
|
+
mutationsCompleted += 1;
|
|
2026
|
+
emitActivity({
|
|
2027
|
+
type: "mutation",
|
|
2028
|
+
at: Date.now(),
|
|
2029
|
+
name,
|
|
2030
|
+
status: "ok"
|
|
2031
|
+
});
|
|
2032
|
+
return result;
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
lastError = error;
|
|
2035
|
+
const elapsedMs = Date.now() - startedAt;
|
|
2036
|
+
const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
|
|
2037
|
+
if (!canRetry)
|
|
2038
|
+
break;
|
|
2039
|
+
mutationsRetried += 1;
|
|
2040
|
+
const rawDelay = computeDelay(attempt);
|
|
2041
|
+
const remaining = maxElapsedMs - elapsedMs;
|
|
2042
|
+
if (remaining <= 0)
|
|
2043
|
+
break;
|
|
2044
|
+
const delayMs = Math.max(0, Math.min(rawDelay, remaining));
|
|
2045
|
+
emitActivity({
|
|
2046
|
+
type: "mutationRetry",
|
|
2047
|
+
at: Date.now(),
|
|
2048
|
+
name,
|
|
2049
|
+
attempt,
|
|
2050
|
+
delayMs,
|
|
2051
|
+
errorName: error instanceof Error ? error.name : "Error",
|
|
2052
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2053
|
+
});
|
|
2054
|
+
if (delayMs > 0) {
|
|
2055
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
mutationsFailed += 1;
|
|
2060
|
+
emitActivity({
|
|
2061
|
+
type: "mutation",
|
|
2062
|
+
at: Date.now(),
|
|
2063
|
+
name,
|
|
2064
|
+
status: "error"
|
|
2065
|
+
});
|
|
2066
|
+
if (attemptsMade > 1) {
|
|
2067
|
+
throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
|
|
2068
|
+
}
|
|
2069
|
+
throw lastError;
|
|
2070
|
+
} finally {
|
|
2071
|
+
releaseMutationSlot();
|
|
2072
|
+
}
|
|
2073
|
+
} catch (spanError) {
|
|
2074
|
+
span.recordException(spanError);
|
|
2075
|
+
span.setStatus({
|
|
2076
|
+
code: 2,
|
|
2077
|
+
message: spanError instanceof Error ? spanError.message : String(spanError)
|
|
2078
|
+
});
|
|
2079
|
+
throw spanError;
|
|
2080
|
+
} finally {
|
|
2081
|
+
span.end();
|
|
2082
|
+
}
|
|
2083
|
+
},
|
|
2084
|
+
runMutations: async (specs, ctx) => {
|
|
2085
|
+
if (specs.length === 0)
|
|
2086
|
+
return [];
|
|
2087
|
+
const resolved = specs.map((spec) => {
|
|
2088
|
+
const mutation = mutations.get(spec.name);
|
|
2089
|
+
if (mutation === undefined) {
|
|
2090
|
+
throw new Error(`Unknown mutation "${spec.name}"`);
|
|
2091
|
+
}
|
|
2092
|
+
return { args: spec.args, mutation, name: spec.name };
|
|
2093
|
+
});
|
|
2094
|
+
await acquireMutationSlot();
|
|
2095
|
+
const runBatch = async (tx) => {
|
|
2096
|
+
const results = [];
|
|
2097
|
+
const accumulated = [];
|
|
2098
|
+
for (const { args, mutation, name } of resolved) {
|
|
2099
|
+
if (mutation.authorize !== undefined) {
|
|
2100
|
+
const allowed = await mutation.authorize(args, ctx);
|
|
2101
|
+
if (!allowed) {
|
|
2102
|
+
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
const sandboxRunner = sandboxRunners.get(name);
|
|
2106
|
+
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions2) => Promise.resolve(mutation.handler(a, c, actions2));
|
|
2107
|
+
const { actions, buffered } = makeActions(tx, ctx, true);
|
|
2108
|
+
const result = await invokeHandler(args, ctx, actions);
|
|
2109
|
+
results.push(result);
|
|
2110
|
+
accumulated.push(...buffered);
|
|
2111
|
+
}
|
|
2112
|
+
return { accumulated, results };
|
|
2113
|
+
};
|
|
2114
|
+
try {
|
|
2115
|
+
const { accumulated, results } = runInTransaction !== undefined ? await runInTransaction((tx) => runBatch(tx)) : await runBatch(undefined);
|
|
2116
|
+
await applyChangeBatch(accumulated);
|
|
2117
|
+
emitActivity({
|
|
2118
|
+
type: "mutationBatch",
|
|
2119
|
+
at: Date.now(),
|
|
2120
|
+
names: resolved.map((entry) => entry.name),
|
|
2121
|
+
status: "ok"
|
|
2122
|
+
});
|
|
2123
|
+
return results;
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
emitActivity({
|
|
2126
|
+
type: "mutationBatch",
|
|
2127
|
+
at: Date.now(),
|
|
2128
|
+
names: resolved.map((entry) => entry.name),
|
|
2129
|
+
status: "error"
|
|
2130
|
+
});
|
|
2131
|
+
throw error;
|
|
2132
|
+
} finally {
|
|
2133
|
+
releaseMutationSlot();
|
|
2134
|
+
}
|
|
2135
|
+
},
|
|
2136
|
+
registerSchedule: (schedule) => {
|
|
2137
|
+
schedules.set(schedule.name, schedule);
|
|
2138
|
+
},
|
|
2139
|
+
listSchedules: () => [...schedules.values()],
|
|
2140
|
+
runSchedule: async (name) => {
|
|
2141
|
+
const schedule = schedules.get(name);
|
|
2142
|
+
if (schedule === undefined) {
|
|
2143
|
+
throw new Error(`Unknown schedule "${name}"`);
|
|
2144
|
+
}
|
|
2145
|
+
const runHandler = async (tx) => {
|
|
2146
|
+
const { actions, buffered } = makeActions(tx, {}, false);
|
|
2147
|
+
const db = makeReadHandle({}, new Set, new Set, [], false);
|
|
2148
|
+
await schedule.run({ actions, db });
|
|
2149
|
+
return buffered;
|
|
2150
|
+
};
|
|
2151
|
+
const retry = schedule.retry;
|
|
2152
|
+
const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
|
|
2153
|
+
const isRetryable = retry?.isRetryable ?? isSerializationFailure;
|
|
2154
|
+
const computeDelay = retry?.backoff ?? exponentialBackoff();
|
|
2155
|
+
const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
|
|
2156
|
+
const startedAt = Date.now();
|
|
2157
|
+
let lastError;
|
|
2158
|
+
let attemptsMade = 0;
|
|
2159
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2160
|
+
attemptsMade = attempt;
|
|
2161
|
+
try {
|
|
2162
|
+
const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
|
|
2163
|
+
await applyChangeBatch(buffered);
|
|
2164
|
+
emitActivity({
|
|
2165
|
+
type: "schedule",
|
|
2166
|
+
at: Date.now(),
|
|
2167
|
+
name,
|
|
2168
|
+
status: "ok"
|
|
2169
|
+
});
|
|
2170
|
+
return;
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
lastError = error;
|
|
2173
|
+
const elapsedMs = Date.now() - startedAt;
|
|
2174
|
+
const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
|
|
2175
|
+
if (!canRetry)
|
|
2176
|
+
break;
|
|
2177
|
+
const rawDelay = computeDelay(attempt);
|
|
2178
|
+
const remaining = maxElapsedMs - elapsedMs;
|
|
2179
|
+
if (remaining <= 0)
|
|
2180
|
+
break;
|
|
2181
|
+
const delayMs = Math.max(0, Math.min(rawDelay, remaining));
|
|
2182
|
+
emitActivity({
|
|
2183
|
+
type: "scheduleRetry",
|
|
2184
|
+
at: Date.now(),
|
|
2185
|
+
name,
|
|
2186
|
+
attempt,
|
|
2187
|
+
delayMs,
|
|
2188
|
+
errorName: error instanceof Error ? error.name : "Error",
|
|
2189
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2190
|
+
});
|
|
2191
|
+
if (delayMs > 0) {
|
|
2192
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
emitActivity({
|
|
2197
|
+
type: "schedule",
|
|
2198
|
+
at: Date.now(),
|
|
2199
|
+
name,
|
|
2200
|
+
status: "error"
|
|
2201
|
+
});
|
|
2202
|
+
if (attemptsMade > 1) {
|
|
2203
|
+
throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
|
|
2204
|
+
}
|
|
2205
|
+
throw lastError;
|
|
2206
|
+
},
|
|
2207
|
+
registerPack: (pack) => {
|
|
2208
|
+
for (const table of pack.ownsTables) {
|
|
2209
|
+
const existing = packTableOwners.get(table);
|
|
2210
|
+
if (existing !== undefined) {
|
|
2211
|
+
throw new PackTableConflictError(table, existing, pack.name);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
if (pack.requireDependencies === true) {
|
|
2215
|
+
for (const table of pack.readsTables ?? []) {
|
|
2216
|
+
if (!readers.has(table)) {
|
|
2217
|
+
throw new PackMissingDependencyError(pack.name, table);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
if (pack.schemas !== undefined) {
|
|
2222
|
+
for (const [table, schema] of Object.entries(pack.schemas)) {
|
|
2223
|
+
engine.registerSchema(table, schema);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (pack.permissions !== undefined) {
|
|
2227
|
+
for (const [table, rules] of Object.entries(pack.permissions)) {
|
|
2228
|
+
engine.registerPermissions(table, rules);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
if (pack.readers !== undefined) {
|
|
2232
|
+
for (const [table, reader] of Object.entries(pack.readers)) {
|
|
2233
|
+
engine.registerReader(table, reader);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
if (pack.writers !== undefined) {
|
|
2237
|
+
for (const [table, writer] of Object.entries(pack.writers)) {
|
|
2238
|
+
engine.registerWriter(table, writer);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
if (pack.crdt !== undefined) {
|
|
2242
|
+
for (const [table, fields] of Object.entries(pack.crdt)) {
|
|
2243
|
+
engine.registerCrdt(table, fields);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
for (const collection of pack.collections ?? []) {
|
|
2247
|
+
engine.register(collection);
|
|
2248
|
+
}
|
|
2249
|
+
for (const collection of pack.joinCollections ?? []) {
|
|
2250
|
+
engine.registerJoin(collection);
|
|
2251
|
+
}
|
|
2252
|
+
for (const collection of pack.graphCollections ?? []) {
|
|
2253
|
+
engine.registerGraph(collection);
|
|
2254
|
+
}
|
|
2255
|
+
for (const collection of pack.searchCollections ?? []) {
|
|
2256
|
+
engine.registerSearch(collection);
|
|
2257
|
+
}
|
|
2258
|
+
for (const query of pack.reactiveQueries ?? []) {
|
|
2259
|
+
engine.registerReactive(query);
|
|
2260
|
+
}
|
|
2261
|
+
for (const mutation of pack.mutations ?? []) {
|
|
2262
|
+
engine.registerMutation(mutation);
|
|
2263
|
+
}
|
|
2264
|
+
for (const schedule of pack.schedules ?? []) {
|
|
2265
|
+
engine.registerSchedule(schedule);
|
|
2266
|
+
}
|
|
2267
|
+
for (const table of pack.ownsTables) {
|
|
2268
|
+
packTableOwners.set(table, pack.name);
|
|
2269
|
+
}
|
|
2270
|
+
registeredPacks.push({
|
|
2271
|
+
name: pack.name,
|
|
2272
|
+
version: pack.version,
|
|
2273
|
+
ownsTables: [...pack.ownsTables],
|
|
2274
|
+
readsTables: [...pack.readsTables ?? []]
|
|
2275
|
+
});
|
|
2276
|
+
},
|
|
2277
|
+
inspect: () => {
|
|
2278
|
+
const collections = [...registry.entries()].map(([name, def]) => {
|
|
2279
|
+
const kind = def.kind ?? "view";
|
|
2280
|
+
let tables = [];
|
|
2281
|
+
if (kind === "join") {
|
|
2282
|
+
const join = def;
|
|
2283
|
+
tables = [join.left.table, join.right.table];
|
|
2284
|
+
} else if (kind === "graph") {
|
|
2285
|
+
tables = def.query.tables();
|
|
2286
|
+
} else if (kind === "search") {
|
|
2287
|
+
tables = [
|
|
2288
|
+
def.table
|
|
2289
|
+
];
|
|
2290
|
+
} else if (kind === "view") {
|
|
2291
|
+
tables = def.tables ?? [name];
|
|
2292
|
+
}
|
|
2293
|
+
return {
|
|
2294
|
+
name,
|
|
2295
|
+
kind,
|
|
2296
|
+
tables,
|
|
2297
|
+
subscriptions: active.get(name)?.size ?? 0
|
|
2298
|
+
};
|
|
2299
|
+
});
|
|
2300
|
+
const DEVTOOLS_RECENT = 50;
|
|
2301
|
+
return {
|
|
2302
|
+
version,
|
|
2303
|
+
collections,
|
|
2304
|
+
mutations: [...mutations.keys()],
|
|
2305
|
+
schedules: [...schedules.values()].map((schedule) => ({
|
|
2306
|
+
name: schedule.name,
|
|
2307
|
+
pattern: schedule.pattern
|
|
2308
|
+
})),
|
|
2309
|
+
readers: [...readers.keys()],
|
|
2310
|
+
writers: [...writers.keys()],
|
|
2311
|
+
recentChanges: changeLog.slice(-DEVTOOLS_RECENT).map((entry) => ({
|
|
2312
|
+
version: entry.version,
|
|
2313
|
+
table: entry.table,
|
|
2314
|
+
op: entry.change.op
|
|
2315
|
+
})),
|
|
2316
|
+
packs: registeredPacks.map((pack) => ({
|
|
2317
|
+
name: pack.name,
|
|
2318
|
+
version: pack.version,
|
|
2319
|
+
ownsTables: [...pack.ownsTables],
|
|
2320
|
+
readsTables: [...pack.readsTables]
|
|
2321
|
+
}))
|
|
2322
|
+
};
|
|
2323
|
+
},
|
|
2324
|
+
exportChangeLog: () => ({
|
|
2325
|
+
entries: changeLog.slice(),
|
|
2326
|
+
exportedAt: Date.now(),
|
|
2327
|
+
instanceId,
|
|
2328
|
+
version
|
|
2329
|
+
}),
|
|
2330
|
+
importChangeLog,
|
|
2331
|
+
replayTo: async ({ at, tables }) => {
|
|
2332
|
+
const filterTables = tables !== undefined ? new Set(tables) : undefined;
|
|
2333
|
+
const state = new Map;
|
|
2334
|
+
let asOfVersion = 0;
|
|
2335
|
+
let asOfAt = 0;
|
|
2336
|
+
const oldest = changeLog[0];
|
|
2337
|
+
const truncated = oldest !== undefined && oldest.version > 1 && oldest.at > at;
|
|
2338
|
+
for (const entry of changeLog) {
|
|
2339
|
+
if (entry.at > at)
|
|
2340
|
+
break;
|
|
2341
|
+
if (filterTables !== undefined && !filterTables.has(entry.table)) {
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
let tableState = state.get(entry.table);
|
|
2345
|
+
if (tableState === undefined) {
|
|
2346
|
+
tableState = new Map;
|
|
2347
|
+
state.set(entry.table, tableState);
|
|
2348
|
+
}
|
|
2349
|
+
const reader = readers.get(entry.table);
|
|
2350
|
+
const key = reader?.key?.(entry.change.row) ?? entry.change.row?.id;
|
|
2351
|
+
if (key === undefined) {
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2354
|
+
if (entry.change.op === "delete") {
|
|
2355
|
+
tableState.delete(key);
|
|
2356
|
+
} else {
|
|
2357
|
+
tableState.set(key, entry.change.row);
|
|
2358
|
+
}
|
|
2359
|
+
asOfVersion = entry.version;
|
|
2360
|
+
asOfAt = entry.at;
|
|
2361
|
+
}
|
|
2362
|
+
const rows = {};
|
|
2363
|
+
for (const [table, map] of state) {
|
|
2364
|
+
rows[table] = [...map.values()];
|
|
2365
|
+
}
|
|
2366
|
+
return { asOfAt, asOfVersion, rows, truncated };
|
|
2367
|
+
},
|
|
2368
|
+
fence: ({ reason }) => {
|
|
2369
|
+
const handle = {
|
|
2370
|
+
fencedAt: Date.now(),
|
|
2371
|
+
reason,
|
|
2372
|
+
lift: () => {
|
|
2373
|
+
activeFences.delete(handle);
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
activeFences.add(handle);
|
|
2377
|
+
return handle;
|
|
2378
|
+
},
|
|
2379
|
+
exportSnapshot: async ({
|
|
2380
|
+
tables,
|
|
2381
|
+
ctx = {}
|
|
2382
|
+
} = {}) => {
|
|
2383
|
+
const tableFilter = tables !== undefined ? new Set(tables) : undefined;
|
|
2384
|
+
const rows = {};
|
|
2385
|
+
for (const [table, reader] of readers) {
|
|
2386
|
+
if (tableFilter !== undefined && !tableFilter.has(table)) {
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
const iterable = await reader.all(ctx);
|
|
2390
|
+
rows[table] = [...iterable];
|
|
2391
|
+
}
|
|
2392
|
+
return {
|
|
2393
|
+
exportedAt: Date.now(),
|
|
2394
|
+
sourceInstanceId: instanceId,
|
|
2395
|
+
tables: rows,
|
|
2396
|
+
version
|
|
2397
|
+
};
|
|
2398
|
+
},
|
|
2399
|
+
importSnapshot: async (snapshot, { tables, onProgress, ctx = {} } = {}) => {
|
|
2400
|
+
const tableFilter = tables !== undefined ? new Set(tables) : undefined;
|
|
2401
|
+
const perTable = {};
|
|
2402
|
+
const skipped = [];
|
|
2403
|
+
let tablesImported = 0;
|
|
2404
|
+
let rowsImported = 0;
|
|
2405
|
+
for (const [table, snapshotRows] of Object.entries(snapshot.tables)) {
|
|
2406
|
+
if (tableFilter !== undefined && !tableFilter.has(table)) {
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
const writer = writers.get(table);
|
|
2410
|
+
if (writer === undefined) {
|
|
2411
|
+
skipped.push(table);
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
const total = snapshotRows.length;
|
|
2415
|
+
let done = 0;
|
|
2416
|
+
for (const row of snapshotRows) {
|
|
2417
|
+
await writer.insert(row, ctx, undefined);
|
|
2418
|
+
done += 1;
|
|
2419
|
+
rowsImported += 1;
|
|
2420
|
+
if (onProgress !== undefined) {
|
|
2421
|
+
onProgress(table, done, total);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
perTable[table] = done;
|
|
2425
|
+
if (done > 0)
|
|
2426
|
+
tablesImported += 1;
|
|
2427
|
+
}
|
|
2428
|
+
return {
|
|
2429
|
+
perTable,
|
|
2430
|
+
rowsImported,
|
|
2431
|
+
skipped,
|
|
2432
|
+
tablesImported
|
|
2433
|
+
};
|
|
2434
|
+
},
|
|
2435
|
+
metrics: () => {
|
|
2436
|
+
const now = Date.now();
|
|
2437
|
+
const byCollection = {};
|
|
2438
|
+
let totalSubscriptions = 0;
|
|
2439
|
+
for (const [name, subs] of active) {
|
|
2440
|
+
byCollection[name] = subs.size;
|
|
2441
|
+
totalSubscriptions += subs.size;
|
|
2442
|
+
}
|
|
2443
|
+
const oldest = changeLog[0];
|
|
2444
|
+
return {
|
|
2445
|
+
at: now,
|
|
2446
|
+
changeLog: {
|
|
2447
|
+
capacity: changeLogSize,
|
|
2448
|
+
entries: changeLog.length,
|
|
2449
|
+
oldestAgeMs: oldest ? now - oldest.at : null,
|
|
2450
|
+
oldestVersion: oldest ? oldest.version : null,
|
|
2451
|
+
retainMs: changeLogRetainMs
|
|
2452
|
+
},
|
|
2453
|
+
mutations: {
|
|
2454
|
+
completed: mutationsCompleted,
|
|
2455
|
+
failed: mutationsFailed,
|
|
2456
|
+
inFlight: mutationsInFlight,
|
|
2457
|
+
queued: mutationsQueued,
|
|
2458
|
+
retried: mutationsRetried
|
|
2459
|
+
},
|
|
2460
|
+
reactiveCache: {
|
|
2461
|
+
capacity: reactiveCacheMax,
|
|
2462
|
+
entries: cachedReruns.size
|
|
2463
|
+
},
|
|
2464
|
+
schedules: {
|
|
2465
|
+
registered: schedules.size
|
|
2466
|
+
},
|
|
2467
|
+
source: {
|
|
2468
|
+
changesReceived: sourceChangesReceived,
|
|
2469
|
+
connected: connectedSources,
|
|
2470
|
+
lastChangeAgeMs: sourceLastChangeAt === null ? null : now - sourceLastChangeAt,
|
|
2471
|
+
lastChangeAt: sourceLastChangeAt
|
|
2472
|
+
},
|
|
2473
|
+
subscriptions: {
|
|
2474
|
+
byCollection,
|
|
2475
|
+
byTenant: Object.fromEntries(subscriptionsByTenant),
|
|
2476
|
+
total: totalSubscriptions
|
|
2477
|
+
},
|
|
2478
|
+
uptimeMs: now - engineStartedAt,
|
|
2479
|
+
version
|
|
2480
|
+
};
|
|
2481
|
+
},
|
|
2482
|
+
onActivity: (listener) => {
|
|
2483
|
+
activityListeners.add(listener);
|
|
2484
|
+
return () => {
|
|
2485
|
+
activityListeners.delete(listener);
|
|
2486
|
+
};
|
|
2487
|
+
},
|
|
2488
|
+
streamChanges: ({
|
|
2489
|
+
since = 0,
|
|
2490
|
+
signal,
|
|
2491
|
+
maxBuffer = 1e4
|
|
2492
|
+
} = {}) => {
|
|
2493
|
+
const oldest = changeLog[0];
|
|
2494
|
+
if (since > 0 && oldest !== undefined && oldest.version > since + 1) {
|
|
2495
|
+
const err = new MissedChangesError(since, oldest.version);
|
|
2496
|
+
return {
|
|
2497
|
+
[Symbol.asyncIterator]() {
|
|
2498
|
+
return {
|
|
2499
|
+
next: () => Promise.reject(err)
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
const buffer = [];
|
|
2505
|
+
let waiter = null;
|
|
2506
|
+
let overflow = false;
|
|
2507
|
+
const wake = () => {
|
|
2508
|
+
if (waiter !== null) {
|
|
2509
|
+
const resume = waiter;
|
|
2510
|
+
waiter = null;
|
|
2511
|
+
resume();
|
|
2512
|
+
}
|
|
2513
|
+
};
|
|
2514
|
+
const subscriber = (entry) => {
|
|
2515
|
+
if (buffer.length >= maxBuffer) {
|
|
2516
|
+
overflow = true;
|
|
2517
|
+
wake();
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
buffer.push(entry);
|
|
2521
|
+
wake();
|
|
2522
|
+
};
|
|
2523
|
+
streamSubscribers.add(subscriber);
|
|
2524
|
+
const onAbort = () => wake();
|
|
2525
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2526
|
+
let lastDelivered = since;
|
|
2527
|
+
return {
|
|
2528
|
+
async* [Symbol.asyncIterator]() {
|
|
2529
|
+
try {
|
|
2530
|
+
const history = [...changeLog];
|
|
2531
|
+
const headVersion = history.length > 0 ? history[history.length - 1].version : since;
|
|
2532
|
+
for (const entry of history) {
|
|
2533
|
+
if (signal?.aborted)
|
|
2534
|
+
return;
|
|
2535
|
+
if (entry.version > since) {
|
|
2536
|
+
lastDelivered = entry.version;
|
|
2537
|
+
yield entry;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
while (!signal?.aborted) {
|
|
2541
|
+
while (buffer.length > 0) {
|
|
2542
|
+
const entry = buffer.shift();
|
|
2543
|
+
if (entry.version > headVersion) {
|
|
2544
|
+
lastDelivered = entry.version;
|
|
2545
|
+
yield entry;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
if (overflow) {
|
|
2549
|
+
throw new CdcConsumerSlowError(maxBuffer, lastDelivered);
|
|
2550
|
+
}
|
|
2551
|
+
if (signal?.aborted)
|
|
2552
|
+
return;
|
|
2553
|
+
await new Promise((resolve) => {
|
|
2554
|
+
waiter = resolve;
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
} finally {
|
|
2558
|
+
streamSubscribers.delete(subscriber);
|
|
2559
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
return engine;
|
|
2566
|
+
};
|
|
2567
|
+
|
|
2568
|
+
// src/engine/socket.ts
|
|
2569
|
+
import { Elysia } from "elysia";
|
|
2570
|
+
|
|
2571
|
+
// src/serializer.ts
|
|
2572
|
+
var jsonSerializer = {
|
|
2573
|
+
decode: (raw) => {
|
|
2574
|
+
if (typeof raw === "string") {
|
|
2575
|
+
try {
|
|
2576
|
+
return JSON.parse(raw);
|
|
2577
|
+
} catch {
|
|
2578
|
+
return null;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
if (raw instanceof Uint8Array) {
|
|
2582
|
+
try {
|
|
2583
|
+
return JSON.parse(new TextDecoder().decode(raw));
|
|
2584
|
+
} catch {
|
|
2585
|
+
return null;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
if (raw instanceof ArrayBuffer) {
|
|
2589
|
+
try {
|
|
2590
|
+
return JSON.parse(new TextDecoder().decode(new Uint8Array(raw)));
|
|
2591
|
+
} catch {
|
|
2592
|
+
return null;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
return raw;
|
|
2596
|
+
},
|
|
2597
|
+
encodeClient: (frame) => JSON.stringify(frame),
|
|
2598
|
+
encodeServer: (frame) => JSON.stringify(frame)
|
|
2599
|
+
};
|
|
2600
|
+
|
|
2601
|
+
// src/engine/connection.ts
|
|
2602
|
+
var parseFrame = (raw, serializer) => {
|
|
2603
|
+
let value = raw;
|
|
2604
|
+
if (typeof value === "string" || value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
2605
|
+
value = serializer.decode(raw);
|
|
2606
|
+
if (value === null)
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
if (typeof value !== "object" || value === null) {
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const frame = value;
|
|
2613
|
+
if (frame.type === "subscribe") {
|
|
2614
|
+
const since = typeof frame.since === "number" || typeof frame.since === "string" ? frame.since : undefined;
|
|
2615
|
+
return typeof frame.id === "string" && typeof frame.collection === "string" ? {
|
|
2616
|
+
type: "subscribe",
|
|
2617
|
+
id: frame.id,
|
|
2618
|
+
collection: frame.collection,
|
|
2619
|
+
params: frame.params,
|
|
2620
|
+
since
|
|
2621
|
+
} : undefined;
|
|
2622
|
+
}
|
|
2623
|
+
if (frame.type === "unsubscribe") {
|
|
2624
|
+
return typeof frame.id === "string" ? { type: "unsubscribe", id: frame.id } : undefined;
|
|
2625
|
+
}
|
|
2626
|
+
if (frame.type === "mutate") {
|
|
2627
|
+
return typeof frame.mutationId === "number" && typeof frame.name === "string" ? {
|
|
2628
|
+
type: "mutate",
|
|
2629
|
+
mutationId: frame.mutationId,
|
|
2630
|
+
name: frame.name,
|
|
2631
|
+
args: frame.args
|
|
2632
|
+
} : undefined;
|
|
2633
|
+
}
|
|
2634
|
+
if (frame.type === "presence-join") {
|
|
2635
|
+
return typeof frame.room === "string" && typeof frame.memberId === "string" ? {
|
|
2636
|
+
type: "presence-join",
|
|
2637
|
+
room: frame.room,
|
|
2638
|
+
memberId: frame.memberId,
|
|
2639
|
+
state: frame.state
|
|
2640
|
+
} : undefined;
|
|
2641
|
+
}
|
|
2642
|
+
if (frame.type === "presence-set") {
|
|
2643
|
+
return typeof frame.room === "string" ? { type: "presence-set", room: frame.room, state: frame.state } : undefined;
|
|
2644
|
+
}
|
|
2645
|
+
if (frame.type === "presence-leave") {
|
|
2646
|
+
return typeof frame.room === "string" ? { type: "presence-leave", room: frame.room } : undefined;
|
|
2647
|
+
}
|
|
2648
|
+
return;
|
|
2649
|
+
};
|
|
2650
|
+
var createSyncConnection = ({
|
|
2651
|
+
engine,
|
|
2652
|
+
ctx,
|
|
2653
|
+
send: rawSend,
|
|
2654
|
+
presence,
|
|
2655
|
+
serializer = jsonSerializer
|
|
2656
|
+
}) => {
|
|
2657
|
+
const subscriptions = new Map;
|
|
2658
|
+
const presenceRooms = new Map;
|
|
2659
|
+
let framesSent = 0;
|
|
2660
|
+
let slowSendsRecent = 0;
|
|
2661
|
+
const send = (frame) => {
|
|
2662
|
+
const ret = rawSend(frame);
|
|
2663
|
+
if (ret === -1) {
|
|
2664
|
+
slowSendsRecent += 1;
|
|
2665
|
+
} else {
|
|
2666
|
+
framesSent += 1;
|
|
2667
|
+
slowSendsRecent = 0;
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
let pending = [];
|
|
2671
|
+
let pendingVersion;
|
|
2672
|
+
let pendingCursor;
|
|
2673
|
+
let flushScheduled = false;
|
|
2674
|
+
const flush = () => {
|
|
2675
|
+
if (pending.length === 0) {
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
const diffs = pending;
|
|
2679
|
+
const version = pendingVersion;
|
|
2680
|
+
const cursor = pendingCursor;
|
|
2681
|
+
pending = [];
|
|
2682
|
+
pendingVersion = undefined;
|
|
2683
|
+
pendingCursor = undefined;
|
|
2684
|
+
if (diffs.length === 1) {
|
|
2685
|
+
const only = diffs[0];
|
|
2686
|
+
send({
|
|
2687
|
+
type: "diff",
|
|
2688
|
+
id: only.id,
|
|
2689
|
+
added: only.added,
|
|
2690
|
+
removed: only.removed,
|
|
2691
|
+
changed: only.changed,
|
|
2692
|
+
version,
|
|
2693
|
+
cursor
|
|
2694
|
+
});
|
|
2695
|
+
} else {
|
|
2696
|
+
send({ type: "frame", diffs, version, cursor });
|
|
2697
|
+
}
|
|
2698
|
+
};
|
|
2699
|
+
const scheduleFlush = () => {
|
|
2700
|
+
if (flushScheduled) {
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
flushScheduled = true;
|
|
2704
|
+
queueMicrotask(() => {
|
|
2705
|
+
flushScheduled = false;
|
|
2706
|
+
flush();
|
|
2707
|
+
});
|
|
2708
|
+
};
|
|
2709
|
+
const bufferDiff = (diff, diffVersion, cursor) => {
|
|
2710
|
+
if (pending.length > 0 && pendingVersion !== diffVersion) {
|
|
2711
|
+
flush();
|
|
2712
|
+
}
|
|
2713
|
+
pending.push(diff);
|
|
2714
|
+
pendingVersion = diffVersion;
|
|
2715
|
+
if (cursor !== undefined)
|
|
2716
|
+
pendingCursor = cursor;
|
|
2717
|
+
scheduleFlush();
|
|
2718
|
+
};
|
|
2719
|
+
const handle = async (raw) => {
|
|
2720
|
+
const frame = parseFrame(raw, serializer);
|
|
2721
|
+
if (frame === undefined) {
|
|
2722
|
+
send({ type: "error", message: "Malformed sync frame" });
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
if (frame.type === "mutate") {
|
|
2726
|
+
try {
|
|
2727
|
+
const result = await engine.runMutation(frame.name, frame.args, ctx);
|
|
2728
|
+
flush();
|
|
2729
|
+
send({ type: "ack", mutationId: frame.mutationId, result });
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
send({
|
|
2732
|
+
type: "reject",
|
|
2733
|
+
mutationId: frame.mutationId,
|
|
2734
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
if (frame.type === "unsubscribe") {
|
|
2740
|
+
subscriptions.get(frame.id)?.unsubscribe();
|
|
2741
|
+
subscriptions.delete(frame.id);
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if (frame.type === "presence-join") {
|
|
2745
|
+
if (presence === undefined) {
|
|
2746
|
+
send({ type: "error", message: "Presence is not enabled" });
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
presenceRooms.get(frame.room)?.leave();
|
|
2750
|
+
const handle2 = presence.join(frame.room, frame.memberId, frame.state, (diff) => {
|
|
2751
|
+
send({
|
|
2752
|
+
type: "presence",
|
|
2753
|
+
room: frame.room,
|
|
2754
|
+
joined: diff.joined,
|
|
2755
|
+
updated: diff.updated,
|
|
2756
|
+
left: diff.left
|
|
2757
|
+
});
|
|
2758
|
+
});
|
|
2759
|
+
presenceRooms.set(frame.room, handle2);
|
|
2760
|
+
send({
|
|
2761
|
+
type: "presence",
|
|
2762
|
+
room: frame.room,
|
|
2763
|
+
joined: handle2.members,
|
|
2764
|
+
updated: [],
|
|
2765
|
+
left: []
|
|
2766
|
+
});
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
if (frame.type === "presence-set") {
|
|
2770
|
+
presenceRooms.get(frame.room)?.set(frame.state);
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
if (frame.type === "presence-leave") {
|
|
2774
|
+
presenceRooms.get(frame.room)?.leave();
|
|
2775
|
+
presenceRooms.delete(frame.room);
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
if (subscriptions.has(frame.id)) {
|
|
2779
|
+
send({
|
|
2780
|
+
type: "error",
|
|
2781
|
+
id: frame.id,
|
|
2782
|
+
message: `Subscription id "${frame.id}" already in use`
|
|
2783
|
+
});
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
try {
|
|
2787
|
+
const subscription = await engine.subscribe({
|
|
2788
|
+
collection: frame.collection,
|
|
2789
|
+
params: frame.params,
|
|
2790
|
+
ctx,
|
|
2791
|
+
since: frame.since,
|
|
2792
|
+
onDiff: (diff, diffVersion, cursor) => {
|
|
2793
|
+
bufferDiff({
|
|
2794
|
+
id: frame.id,
|
|
2795
|
+
added: diff.added,
|
|
2796
|
+
removed: diff.removed,
|
|
2797
|
+
changed: diff.changed
|
|
2798
|
+
}, diffVersion, cursor);
|
|
2799
|
+
}
|
|
2800
|
+
});
|
|
2801
|
+
subscriptions.set(frame.id, subscription);
|
|
2802
|
+
if (subscription.catchup !== undefined) {
|
|
2803
|
+
send({
|
|
2804
|
+
type: "diff",
|
|
2805
|
+
id: frame.id,
|
|
2806
|
+
added: subscription.catchup.added,
|
|
2807
|
+
removed: subscription.catchup.removed,
|
|
2808
|
+
changed: subscription.catchup.changed,
|
|
2809
|
+
version: subscription.version,
|
|
2810
|
+
cursor: subscription.cursor
|
|
2811
|
+
});
|
|
2812
|
+
} else {
|
|
2813
|
+
send({
|
|
2814
|
+
type: "snapshot",
|
|
2815
|
+
id: frame.id,
|
|
2816
|
+
rows: subscription.initial,
|
|
2817
|
+
version: subscription.version,
|
|
2818
|
+
cursor: subscription.cursor
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
} catch (error) {
|
|
2822
|
+
send({
|
|
2823
|
+
type: "error",
|
|
2824
|
+
id: frame.id,
|
|
2825
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
const close = () => {
|
|
2830
|
+
for (const subscription of subscriptions.values()) {
|
|
2831
|
+
subscription.unsubscribe();
|
|
2832
|
+
}
|
|
2833
|
+
subscriptions.clear();
|
|
2834
|
+
for (const handle2 of presenceRooms.values()) {
|
|
2835
|
+
handle2.leave();
|
|
2836
|
+
}
|
|
2837
|
+
presenceRooms.clear();
|
|
2838
|
+
};
|
|
2839
|
+
const stats = () => ({
|
|
2840
|
+
framesSent,
|
|
2841
|
+
presenceRoomCount: presenceRooms.size,
|
|
2842
|
+
slowSendsRecent,
|
|
2843
|
+
subscriptionCount: subscriptions.size
|
|
2844
|
+
});
|
|
2845
|
+
return { close, handle, stats };
|
|
2846
|
+
};
|
|
2847
|
+
|
|
2848
|
+
// src/engine/socket.ts
|
|
2849
|
+
var syncSocket = ({
|
|
2850
|
+
engine,
|
|
2851
|
+
path = "/sync/ws",
|
|
2852
|
+
resolveContext,
|
|
2853
|
+
presence,
|
|
2854
|
+
maxBufferedBytes,
|
|
2855
|
+
onSlow,
|
|
2856
|
+
closeOnSlow = false,
|
|
2857
|
+
serializer = jsonSerializer
|
|
2858
|
+
}) => {
|
|
2859
|
+
const connections = new Map;
|
|
2860
|
+
const threshold = maxBufferedBytes ?? Infinity;
|
|
2861
|
+
const fireSlow = (event) => {
|
|
2862
|
+
if (!onSlow)
|
|
2863
|
+
return;
|
|
2864
|
+
try {
|
|
2865
|
+
const ret = onSlow(event);
|
|
2866
|
+
if (ret && typeof ret.then === "function") {
|
|
2867
|
+
ret.catch((error) => {
|
|
2868
|
+
console.error("[sync/socket] onSlow rejected:", error);
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
console.error("[sync/socket] onSlow threw:", error);
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
return new Elysia({ name: "@absolutejs/sync/socket" }).ws(path, {
|
|
2876
|
+
async open(ws) {
|
|
2877
|
+
const bunWs = ws;
|
|
2878
|
+
const tracked = {
|
|
2879
|
+
connection: null,
|
|
2880
|
+
pending: [],
|
|
2881
|
+
slowSignaled: false
|
|
2882
|
+
};
|
|
2883
|
+
connections.set(bunWs.id, tracked);
|
|
2884
|
+
const ctx = resolveContext ? await resolveContext(ws.data) : {};
|
|
2885
|
+
const connection = createSyncConnection({
|
|
2886
|
+
engine,
|
|
2887
|
+
ctx,
|
|
2888
|
+
presence,
|
|
2889
|
+
serializer,
|
|
2890
|
+
send: (frame) => {
|
|
2891
|
+
const payload = serializer.encodeServer(frame);
|
|
2892
|
+
const ret = bunWs.send(typeof payload === "string" ? payload : payload);
|
|
2893
|
+
const buffered2 = bunWs.getBufferedAmount?.() ?? 0;
|
|
2894
|
+
const overBuffer = buffered2 > threshold;
|
|
2895
|
+
const backpressure = ret === -1;
|
|
2896
|
+
if ((overBuffer || backpressure) && !tracked.slowSignaled) {
|
|
2897
|
+
tracked.slowSignaled = true;
|
|
2898
|
+
fireSlow({
|
|
2899
|
+
bufferedAmount: buffered2,
|
|
2900
|
+
reason: backpressure ? "send-backpressure" : "buffer-threshold",
|
|
2901
|
+
stats: connection.stats(),
|
|
2902
|
+
wsId: bunWs.id
|
|
2903
|
+
});
|
|
2904
|
+
if (closeOnSlow)
|
|
2905
|
+
bunWs.close?.();
|
|
2906
|
+
}
|
|
2907
|
+
return ret;
|
|
2908
|
+
}
|
|
2909
|
+
});
|
|
2910
|
+
if (!connections.has(bunWs.id)) {
|
|
2911
|
+
connection.close();
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
tracked.connection = connection;
|
|
2915
|
+
const buffered = tracked.pending;
|
|
2916
|
+
tracked.pending = [];
|
|
2917
|
+
for (const frame of buffered) {
|
|
2918
|
+
await connection.handle(frame);
|
|
2919
|
+
}
|
|
2920
|
+
},
|
|
2921
|
+
async message(ws, message) {
|
|
2922
|
+
const tracked = connections.get(ws.id);
|
|
2923
|
+
if (!tracked)
|
|
2924
|
+
return;
|
|
2925
|
+
if (tracked.connection) {
|
|
2926
|
+
await tracked.connection.handle(message);
|
|
2927
|
+
} else {
|
|
2928
|
+
tracked.pending.push(message);
|
|
2929
|
+
}
|
|
2930
|
+
},
|
|
2931
|
+
drain(ws) {
|
|
2932
|
+
const tracked = connections.get(ws.id);
|
|
2933
|
+
if (tracked)
|
|
2934
|
+
tracked.slowSignaled = false;
|
|
2935
|
+
},
|
|
2936
|
+
close(ws) {
|
|
2937
|
+
const tracked = connections.get(ws.id);
|
|
2938
|
+
if (tracked) {
|
|
2939
|
+
tracked.connection?.close();
|
|
2940
|
+
connections.delete(ws.id);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
};
|
|
2945
|
+
|
|
2946
|
+
// src/plugin.ts
|
|
2947
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
2948
|
+
|
|
2949
|
+
// src/reactiveHub.ts
|
|
2950
|
+
var SYNC_OPEN_TOPIC = "@absolutejs/sync:open";
|
|
2951
|
+
var createReactiveHub = () => {
|
|
2952
|
+
const subscriptions = new Set;
|
|
2953
|
+
const matches = (subscription, topic) => {
|
|
2954
|
+
if (subscription.exact.has(topic)) {
|
|
2955
|
+
return true;
|
|
2956
|
+
}
|
|
2957
|
+
return subscription.prefixes.some((prefix) => topic.startsWith(prefix));
|
|
2958
|
+
};
|
|
2959
|
+
return {
|
|
2960
|
+
publish: (topic, payload) => {
|
|
2961
|
+
const event = { topic, at: Date.now(), payload };
|
|
2962
|
+
for (const subscription of subscriptions) {
|
|
2963
|
+
if (matches(subscription, topic)) {
|
|
2964
|
+
subscription.listener(event);
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
},
|
|
2968
|
+
subscribe: (topics, listener) => {
|
|
2969
|
+
const exact = new Set;
|
|
2970
|
+
const prefixes = [];
|
|
2971
|
+
for (const topic of topics) {
|
|
2972
|
+
if (topic.endsWith("*")) {
|
|
2973
|
+
prefixes.push(topic.slice(0, -1));
|
|
2974
|
+
} else {
|
|
2975
|
+
exact.add(topic);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
const subscription = { exact, prefixes, listener };
|
|
2979
|
+
subscriptions.add(subscription);
|
|
2980
|
+
return () => {
|
|
2981
|
+
subscriptions.delete(subscription);
|
|
2982
|
+
};
|
|
2983
|
+
},
|
|
2984
|
+
subscriberCount: (topic) => {
|
|
2985
|
+
if (topic === undefined) {
|
|
2986
|
+
return subscriptions.size;
|
|
2987
|
+
}
|
|
2988
|
+
let count = 0;
|
|
2989
|
+
for (const subscription of subscriptions) {
|
|
2990
|
+
if (matches(subscription, topic)) {
|
|
2991
|
+
count += 1;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
return count;
|
|
2995
|
+
}
|
|
2996
|
+
};
|
|
2997
|
+
};
|
|
2998
|
+
|
|
2999
|
+
// src/plugin.ts
|
|
3000
|
+
var defaultResolveTopics = (context) => (context.query.topics ?? "").split(",").map((topic) => topic.trim()).filter(Boolean);
|
|
3001
|
+
var sync = ({
|
|
3002
|
+
hub,
|
|
3003
|
+
path = "/sync",
|
|
3004
|
+
resolveTopics = defaultResolveTopics,
|
|
3005
|
+
heartbeatMs = 25000
|
|
3006
|
+
}) => {
|
|
3007
|
+
const app = new Elysia2({ name: "@absolutejs/sync" }).get(path, async (context) => {
|
|
3008
|
+
const topics = await resolveTopics({
|
|
3009
|
+
query: context.query,
|
|
3010
|
+
request: context.request
|
|
3011
|
+
});
|
|
3012
|
+
const encoder = new TextEncoder;
|
|
3013
|
+
const stream = new ReadableStream({
|
|
3014
|
+
start(controller) {
|
|
3015
|
+
const write = (chunk) => {
|
|
3016
|
+
try {
|
|
3017
|
+
controller.enqueue(encoder.encode(chunk));
|
|
3018
|
+
} catch {}
|
|
3019
|
+
};
|
|
3020
|
+
const send = (event) => {
|
|
3021
|
+
write(`data: ${JSON.stringify(event)}
|
|
3022
|
+
|
|
3023
|
+
`);
|
|
3024
|
+
};
|
|
3025
|
+
send({
|
|
3026
|
+
topic: SYNC_OPEN_TOPIC,
|
|
3027
|
+
at: Date.now(),
|
|
3028
|
+
payload: { topics }
|
|
3029
|
+
});
|
|
3030
|
+
const unsubscribe = topics.length > 0 ? hub.subscribe(topics, send) : () => {};
|
|
3031
|
+
const heartbeat = setInterval(() => write(`: ping
|
|
3032
|
+
|
|
3033
|
+
`), heartbeatMs);
|
|
3034
|
+
context.request.signal.addEventListener("abort", () => {
|
|
3035
|
+
clearInterval(heartbeat);
|
|
3036
|
+
unsubscribe();
|
|
3037
|
+
try {
|
|
3038
|
+
controller.close();
|
|
3039
|
+
} catch {}
|
|
3040
|
+
}, { once: true });
|
|
3041
|
+
}
|
|
3042
|
+
});
|
|
3043
|
+
return new Response(stream, {
|
|
3044
|
+
headers: {
|
|
3045
|
+
"cache-control": "no-cache, no-transform",
|
|
3046
|
+
connection: "keep-alive",
|
|
3047
|
+
"content-type": "text/event-stream",
|
|
3048
|
+
"x-accel-buffering": "no"
|
|
3049
|
+
}
|
|
3050
|
+
});
|
|
3051
|
+
});
|
|
3052
|
+
return app;
|
|
3053
|
+
};
|
|
3054
|
+
|
|
3055
|
+
// src/platform.ts
|
|
3056
|
+
var PLATFORM_SYNC_ENVIRONMENT_KEY = "ABSOLUTE_SYNC_RUNTIME";
|
|
3057
|
+
var PLATFORM_SYNC_HEALTH_PATH = "/.well-known/absolute/sync";
|
|
3058
|
+
var PlatformSyncConfigurationSchema = Type.Object({
|
|
3059
|
+
changeLogRetainMs: Type.Integer({
|
|
3060
|
+
maximum: 604800000,
|
|
3061
|
+
minimum: 1000
|
|
3062
|
+
}),
|
|
3063
|
+
changeLogSize: Type.Integer({ maximum: 1e4, minimum: 1 }),
|
|
3064
|
+
closeOnSlow: Type.Boolean(),
|
|
3065
|
+
heartbeatMs: Type.Integer({ maximum: 120000, minimum: 1000 }),
|
|
3066
|
+
instanceId: Type.String({
|
|
3067
|
+
maxLength: 128,
|
|
3068
|
+
minLength: 1,
|
|
3069
|
+
pattern: "^[A-Za-z0-9_.:-]+$"
|
|
3070
|
+
}),
|
|
3071
|
+
maxBufferedBytes: Type.Integer({
|
|
3072
|
+
maximum: 16777216,
|
|
3073
|
+
minimum: 65536
|
|
3074
|
+
}),
|
|
3075
|
+
mutationConcurrency: Type.Integer({ maximum: 64, minimum: 1 }),
|
|
3076
|
+
mutationQueueLimit: Type.Integer({ maximum: 1e4, minimum: 0 }),
|
|
3077
|
+
pushPath: Type.String({
|
|
3078
|
+
maxLength: 128,
|
|
3079
|
+
minLength: 1,
|
|
3080
|
+
pattern: "^/[^?#]*$"
|
|
3081
|
+
}),
|
|
3082
|
+
socketPath: Type.String({
|
|
3083
|
+
maxLength: 128,
|
|
3084
|
+
minLength: 1,
|
|
3085
|
+
pattern: "^/[^?#]*$"
|
|
3086
|
+
}),
|
|
3087
|
+
tier: Type.Union([
|
|
3088
|
+
Type.Literal("push"),
|
|
3089
|
+
Type.Literal("engine"),
|
|
3090
|
+
Type.Literal("both")
|
|
3091
|
+
]),
|
|
3092
|
+
version: Type.Literal(1)
|
|
3093
|
+
}, { additionalProperties: false });
|
|
3094
|
+
|
|
3095
|
+
class PlatformSyncConfigurationError extends Error {
|
|
3096
|
+
}
|
|
3097
|
+
var readPlatformSyncConfiguration = (environment = process.env) => {
|
|
3098
|
+
const serialized = environment[PLATFORM_SYNC_ENVIRONMENT_KEY]?.trim();
|
|
3099
|
+
if (!serialized)
|
|
3100
|
+
return null;
|
|
3101
|
+
let parsed;
|
|
3102
|
+
try {
|
|
3103
|
+
parsed = JSON.parse(serialized);
|
|
3104
|
+
} catch {
|
|
3105
|
+
throw new PlatformSyncConfigurationError("Platform Sync configuration is not valid JSON");
|
|
3106
|
+
}
|
|
3107
|
+
if (!Value.Check(PlatformSyncConfigurationSchema, parsed))
|
|
3108
|
+
throw new PlatformSyncConfigurationError("Platform Sync configuration is invalid");
|
|
3109
|
+
return parsed;
|
|
3110
|
+
};
|
|
3111
|
+
var createPlatformSyncRuntime = (options = {}) => {
|
|
3112
|
+
const configuration = options.configuration ?? readPlatformSyncConfiguration();
|
|
3113
|
+
if (!configuration)
|
|
3114
|
+
throw new PlatformSyncConfigurationError("Platform Sync is not configured");
|
|
3115
|
+
const pushEnabled = configuration.tier !== "engine";
|
|
3116
|
+
const engineEnabled = configuration.tier !== "push";
|
|
3117
|
+
const hub = pushEnabled ? options.hub ?? createReactiveHub() : undefined;
|
|
3118
|
+
const engine = engineEnabled ? createSyncEngine({
|
|
3119
|
+
...options.engineOptions,
|
|
3120
|
+
changeLogRetainMs: configuration.changeLogRetainMs,
|
|
3121
|
+
changeLogSize: configuration.changeLogSize,
|
|
3122
|
+
instanceId: configuration.instanceId,
|
|
3123
|
+
mutationConcurrency: configuration.mutationConcurrency,
|
|
3124
|
+
mutationQueueLimit: configuration.mutationQueueLimit
|
|
3125
|
+
}) : undefined;
|
|
3126
|
+
const app = new Elysia3({ name: "@absolutejs/sync/platform" }).use(hub ? sync({
|
|
3127
|
+
heartbeatMs: configuration.heartbeatMs,
|
|
3128
|
+
hub,
|
|
3129
|
+
path: configuration.pushPath,
|
|
3130
|
+
...options.resolveTopics ? { resolveTopics: options.resolveTopics } : {}
|
|
3131
|
+
}) : new Elysia3({ name: "@absolutejs/sync/platform/no-push" })).use(engine ? syncSocket({
|
|
3132
|
+
closeOnSlow: configuration.closeOnSlow,
|
|
3133
|
+
engine,
|
|
3134
|
+
maxBufferedBytes: configuration.maxBufferedBytes,
|
|
3135
|
+
path: configuration.socketPath,
|
|
3136
|
+
...options.onSlow ? { onSlow: options.onSlow } : {},
|
|
3137
|
+
...options.resolveContext ? { resolveContext: options.resolveContext } : {}
|
|
3138
|
+
}) : new Elysia3({ name: "@absolutejs/sync/platform/no-engine" })).get(PLATFORM_SYNC_HEALTH_PATH, () => ({
|
|
3139
|
+
contract: 1,
|
|
3140
|
+
instanceId: configuration.instanceId,
|
|
3141
|
+
ready: true,
|
|
3142
|
+
tier: configuration.tier
|
|
3143
|
+
}));
|
|
3144
|
+
return {
|
|
3145
|
+
app,
|
|
3146
|
+
configuration,
|
|
3147
|
+
engine,
|
|
3148
|
+
hub,
|
|
3149
|
+
metrics: () => engine?.metrics() ?? null
|
|
3150
|
+
};
|
|
3151
|
+
};
|
|
3152
|
+
export {
|
|
3153
|
+
readPlatformSyncConfiguration,
|
|
3154
|
+
createPlatformSyncRuntime,
|
|
3155
|
+
PlatformSyncConfigurationSchema,
|
|
3156
|
+
PlatformSyncConfigurationError,
|
|
3157
|
+
PLATFORM_SYNC_HEALTH_PATH,
|
|
3158
|
+
PLATFORM_SYNC_ENVIRONMENT_KEY
|
|
3159
|
+
};
|
|
3160
|
+
|
|
3161
|
+
//# debugId=3093A4FC38575BA364756E2164756E21
|
|
3162
|
+
//# sourceMappingURL=platform.js.map
|