@absolutejs/sync 1.8.0 → 1.9.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/dist/engine/devtools.d.ts +27 -0
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/index.js +203 -26
- package/dist/engine/index.js.map +7 -6
- package/dist/engine/pack.d.ts +138 -0
- package/dist/engine/sandbox.d.ts +20 -20
- package/dist/engine/schedule.d.ts +14 -0
- package/dist/engine/syncEngine.d.ts +13 -0
- package/dist/index.js +201 -26
- package/dist/index.js.map +6 -5
- package/package.json +4 -4
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync packs — Convex Components without the lock-in. A pack bundles
|
|
3
|
+
* schemas + collections + mutations + scheduled + permissions + readers/
|
|
4
|
+
* writers + CRDT field declarations into one npm-distributable module
|
|
5
|
+
* registered with a single {@link SyncEngine.registerPack} call.
|
|
6
|
+
*
|
|
7
|
+
* See `syncPacks.design.md` for the rationale and the worked examples.
|
|
8
|
+
*
|
|
9
|
+
* Pack design rules (enforced at register time):
|
|
10
|
+
*
|
|
11
|
+
* - A pack declares which tables it `ownsTables`. Two registered packs
|
|
12
|
+
* cannot claim the same table; the host app's directly-registered
|
|
13
|
+
* tables are NOT counted (host registrations always win).
|
|
14
|
+
* - Name construction (namespacing) is the pack's job — each published
|
|
15
|
+
* pack ships as a factory `create<Name>Pack(config)` that builds names
|
|
16
|
+
* from a `tablePrefix` and the host's user/auth context.
|
|
17
|
+
* - Packs compose via the subscription layer (read each other's
|
|
18
|
+
* collections), never via cross-pack `runMutation` calls.
|
|
19
|
+
*/
|
|
20
|
+
import type { CollectionDefinition, JoinCollectionDefinition } from './collection';
|
|
21
|
+
import type { GraphCollectionDefinition } from './graph';
|
|
22
|
+
import type { MutationDefinition, TableWriter } from './mutation';
|
|
23
|
+
import type { PermissionsDefinition } from './permissions';
|
|
24
|
+
import type { ReactiveQueryDefinition, TableReader } from './reactive';
|
|
25
|
+
import type { ScheduleDefinition } from './schedule';
|
|
26
|
+
import type { SchemaDefinition } from './schema';
|
|
27
|
+
import type { SearchCollectionDefinition } from './search';
|
|
28
|
+
import type { CrdtMergeable } from '../crdt';
|
|
29
|
+
/**
|
|
30
|
+
* Same shape as the engine's per-table CRDT field map (see
|
|
31
|
+
* {@link SyncEngine.registerCrdt}). A pack declares CRDT field types
|
|
32
|
+
* here; the engine wires them on registration.
|
|
33
|
+
*/
|
|
34
|
+
export type CrdtFieldsMap = Record<string, Record<string, CrdtMergeable<unknown>>>;
|
|
35
|
+
/**
|
|
36
|
+
* A pack — a self-contained bundle of every registration the engine
|
|
37
|
+
* already accepts. The engine's `registerPack(pack)` dispatches each
|
|
38
|
+
* field to the matching `engine.register*` method. There is no new
|
|
39
|
+
* persistence path; packs are pure composition.
|
|
40
|
+
*/
|
|
41
|
+
export type SyncPack = {
|
|
42
|
+
/**
|
|
43
|
+
* Pack identifier. Used for devtools labelling and conflict
|
|
44
|
+
* diagnostics. Should match the npm package name (e.g.
|
|
45
|
+
* "@absolutejs/sync-pack-presence").
|
|
46
|
+
*/
|
|
47
|
+
name: string;
|
|
48
|
+
/**
|
|
49
|
+
* Pack semver. Surfaced in {@link EngineInspection.packs} and in
|
|
50
|
+
* conflict diagnostics (e.g. "table 'comments' is owned by
|
|
51
|
+
* sync-pack-comments@2.1.0").
|
|
52
|
+
*/
|
|
53
|
+
version: string;
|
|
54
|
+
/**
|
|
55
|
+
* Tables this pack OWNS. The engine rejects another pack that also
|
|
56
|
+
* claims one of these. Direct host registrations (e.g.
|
|
57
|
+
* `engine.registerSchema("foo", ...)`) are NOT tracked as ownership,
|
|
58
|
+
* so the host can still extend a pack's table or override its
|
|
59
|
+
* schema/permissions.
|
|
60
|
+
*/
|
|
61
|
+
ownsTables: string[];
|
|
62
|
+
/**
|
|
63
|
+
* Tables this pack reads but does NOT own (e.g. a comments pack
|
|
64
|
+
* reads the host's `users` table for author info). Reported in
|
|
65
|
+
* {@link EngineInspection.packs}; not enforced unless
|
|
66
|
+
* {@link requireDependencies} is `true`.
|
|
67
|
+
*/
|
|
68
|
+
readsTables?: string[];
|
|
69
|
+
/**
|
|
70
|
+
* When `true`, the engine throws {@link PackMissingDependencyError}
|
|
71
|
+
* at register time if any table in `readsTables` has no registered
|
|
72
|
+
* reader. Default `false` — host-app reads can be wired lazily.
|
|
73
|
+
*/
|
|
74
|
+
requireDependencies?: boolean;
|
|
75
|
+
schemas?: SchemaDefinition;
|
|
76
|
+
permissions?: PermissionsDefinition<any>;
|
|
77
|
+
readers?: Record<string, TableReader<any>>;
|
|
78
|
+
writers?: Record<string, TableWriter<any, any, any>>;
|
|
79
|
+
crdt?: CrdtFieldsMap;
|
|
80
|
+
collections?: CollectionDefinition<any, any, any>[];
|
|
81
|
+
joinCollections?: JoinCollectionDefinition<any, any, any, any, any>[];
|
|
82
|
+
graphCollections?: GraphCollectionDefinition<any, any, any>[];
|
|
83
|
+
searchCollections?: SearchCollectionDefinition<any, any, any>[];
|
|
84
|
+
reactiveQueries?: ReactiveQueryDefinition<any, any, any>[];
|
|
85
|
+
mutations?: MutationDefinition<any, any, any>[];
|
|
86
|
+
schedules?: ScheduleDefinition[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Pack metadata stored on the engine and surfaced via
|
|
90
|
+
* {@link EngineInspection.packs}.
|
|
91
|
+
*/
|
|
92
|
+
export type RegisteredPack = {
|
|
93
|
+
name: string;
|
|
94
|
+
version: string;
|
|
95
|
+
ownsTables: string[];
|
|
96
|
+
readsTables: string[];
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Thrown by {@link SyncEngine.registerPack} when a pack claims a table
|
|
100
|
+
* that another registered pack already owns. The message names both
|
|
101
|
+
* packs and the colliding table so the operator can pick a
|
|
102
|
+
* `tablePrefix`.
|
|
103
|
+
*/
|
|
104
|
+
export declare class PackTableConflictError extends Error {
|
|
105
|
+
readonly table: string;
|
|
106
|
+
readonly existingPack: string;
|
|
107
|
+
readonly newPack: string;
|
|
108
|
+
constructor(table: string, existingPack: string, newPack: string);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Thrown by {@link SyncEngine.registerPack} when a pack has
|
|
112
|
+
* `requireDependencies: true` and at least one table in
|
|
113
|
+
* {@link SyncPack.readsTables} has no registered reader at register
|
|
114
|
+
* time. Pack authors opt into this when their pack cannot function
|
|
115
|
+
* without the host having wired the dependency up front.
|
|
116
|
+
*/
|
|
117
|
+
export declare class PackMissingDependencyError extends Error {
|
|
118
|
+
readonly pack: string;
|
|
119
|
+
readonly missingTable: string;
|
|
120
|
+
constructor(pack: string, missingTable: string);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Identity helper. A pack is plain data — the helper exists for type
|
|
124
|
+
* inference, not for runtime behavior.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* export const createPresencePack = (config: PresencePackConfig) =>
|
|
128
|
+
* defineSyncPack({
|
|
129
|
+
* name: '@absolutejs/sync-pack-presence',
|
|
130
|
+
* version: '0.1.0',
|
|
131
|
+
* ownsTables: [resolveTableName('presence', config.tablePrefix)],
|
|
132
|
+
* schemas: { ... },
|
|
133
|
+
* collections: [ ... ],
|
|
134
|
+
* mutations: [ ... ],
|
|
135
|
+
* schedules: [ ... ],
|
|
136
|
+
* });
|
|
137
|
+
*/
|
|
138
|
+
export declare const defineSyncPack: (pack: SyncPack) => SyncPack;
|
package/dist/engine/sandbox.d.ts
CHANGED
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
* - Handler must be a string. It evaluates inside the isolate's JSC VM, with
|
|
10
10
|
* no access to the host's modules, closures, or globals — only the
|
|
11
11
|
* `args` / `ctx` clones and the `actions` Reference we pass in.
|
|
12
|
-
* - First call per mutation pays an
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* per-call eval, no per-call `setGlobal`.
|
|
12
|
+
* - First call per mutation pays an isolated-jsc runner warmup + callable
|
|
13
|
+
* compile. Every subsequent call is served from the runner's keyed
|
|
14
|
+
* callable cache — no per-call eval, no per-call `setGlobal`.
|
|
16
15
|
* - Timeout terminates the isolate (the sandbox runner detects this and
|
|
17
16
|
* lazily re-spawns on the next call). On the FFI backend timeouts throw
|
|
18
17
|
* a TerminationException without killing the isolate; sync's runner
|
|
@@ -25,12 +24,11 @@
|
|
|
25
24
|
* `WebSocket`) inside your handler — those live in the Bun-Worker
|
|
26
25
|
* environment, not the bare JSC C API.
|
|
27
26
|
*
|
|
28
|
-
* **Per-call hot path (since
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* that
|
|
33
|
-
* no shared-slot serialization machinery.
|
|
27
|
+
* **Per-call hot path (since isolated-jsc 0.8).** Each mutation owns a
|
|
28
|
+
* `createIsolatedRunner()` instance. The runner applies the `tenant-script`
|
|
29
|
+
* policy, keeps a keyed isolate pool, and caches the wrapped mutation as a
|
|
30
|
+
* precompiled callable. Per call we invoke `runner.call(name, source, args)`
|
|
31
|
+
* with a call id that routes `actions.*` back through a host Reference.
|
|
34
32
|
*
|
|
35
33
|
* The runner is built lazily per-mutation: nothing is spawned until the
|
|
36
34
|
* mutation actually runs for the first time. No engine teardown hook is
|
|
@@ -66,10 +64,12 @@ export type HandlerMetricsRecord = {
|
|
|
66
64
|
durationMs: number;
|
|
67
65
|
/**
|
|
68
66
|
* CPU time spent inside the JSC sandbox (ms). Comes from
|
|
69
|
-
*
|
|
67
|
+
* isolated-jsc runner metrics — does NOT include host-side message-passing
|
|
70
68
|
* overhead on the Worker backend. Sub-millisecond runs round to 0.
|
|
71
69
|
*/
|
|
72
70
|
cpuMs: number;
|
|
71
|
+
/** isolated-jsc backend that executed the call. */
|
|
72
|
+
backend?: 'ffi' | 'worker';
|
|
73
73
|
/**
|
|
74
74
|
* Heap size (bytes) measured immediately after the script returned.
|
|
75
75
|
* Not the run's peak — a true peak needs continuous polling.
|
|
@@ -159,9 +159,9 @@ export type SandboxConfig = {
|
|
|
159
159
|
timeout?: number;
|
|
160
160
|
/**
|
|
161
161
|
* isolated-jsc backend. Defaults to `'auto'` (FFI when libJSC is
|
|
162
|
-
* reachable, Worker otherwise). Both backends now run the same
|
|
163
|
-
* `
|
|
164
|
-
*
|
|
162
|
+
* reachable, Worker otherwise). Both backends now run through the same
|
|
163
|
+
* `createIsolatedRunner().call()` hot path; the choice trades cold spawn
|
|
164
|
+
* (FFI wins ~6×) against Web API availability (Worker only).
|
|
165
165
|
*
|
|
166
166
|
* Pin to `'worker'` if your handler needs Web APIs (`URL`,
|
|
167
167
|
* `TextEncoder`, `WebSocket`) — those live in the Bun-Worker
|
|
@@ -174,12 +174,11 @@ export type SandboxConfig = {
|
|
|
174
174
|
};
|
|
175
175
|
/**
|
|
176
176
|
* Build a lazy runner for one mutation's sandboxed source. The first call
|
|
177
|
-
* compiles the
|
|
177
|
+
* compiles the runner + dispatch Reference + callable;
|
|
178
178
|
* subsequent calls only generate a fresh callId, register the per-call
|
|
179
|
-
* `actions` in the callMap, and invoke `
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* setGlobal, no eval.
|
|
179
|
+
* `actions` in the callMap, and invoke `runner.call(...)`. Per-call cost on
|
|
180
|
+
* FFI: one JSObjectCallAsFunction + three cheap primitive packings. No
|
|
181
|
+
* per-call Reference allocation, no setGlobal, no eval.
|
|
183
182
|
*
|
|
184
183
|
* Concurrency-safe by construction: each call has its own callId →
|
|
185
184
|
* its own actions slot in the callMap.
|
|
@@ -191,7 +190,8 @@ export declare const makeSandboxedHandler: (source: string, config?: SandboxConf
|
|
|
191
190
|
/**
|
|
192
191
|
* Engine-level extras the per-mutation config doesn't carry:
|
|
193
192
|
* - `metricsHook` enables per-call telemetry via
|
|
194
|
-
* `
|
|
193
|
+
* `runner.call(..., { withMetrics: true })` (small cost; off without
|
|
194
|
+
* the hook).
|
|
195
195
|
* - `bridgeFetch` enables `actions.fetch(url, init)` inside the
|
|
196
196
|
* sandbox with host-side allowlist + auth injection.
|
|
197
197
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MutationActions } from './mutation';
|
|
2
2
|
import type { ReadHandle } from './reactive';
|
|
3
|
+
import type { RetryPolicy } from './retry';
|
|
3
4
|
/**
|
|
4
5
|
* Scheduled functions — server-triggered work whose effects flow through the
|
|
5
6
|
* change feed, so what a schedule writes goes live to subscribers with no extra
|
|
@@ -30,6 +31,19 @@ export type ScheduleDefinition = {
|
|
|
30
31
|
pattern: string;
|
|
31
32
|
/** The work to run on each fire. Writes via `ctx.actions` go live. */
|
|
32
33
|
run: (ctx: ScheduleContext) => Promise<void> | void;
|
|
34
|
+
/**
|
|
35
|
+
* Opt-in retry of the whole handler on classified-as-retryable errors —
|
|
36
|
+
* same shape and defaults as {@link MutationDefinition.retry}. When set
|
|
37
|
+
* and `run` throws a retryable error, the engine discards the buffered
|
|
38
|
+
* changes, awaits a backoff, and re-runs the handler with a fresh
|
|
39
|
+
* transaction. The handler MUST be idempotent under retry (external
|
|
40
|
+
* side effects fire more than once).
|
|
41
|
+
*
|
|
42
|
+
* For per-item retry (e.g. one of many emails failing), write that
|
|
43
|
+
* loop inside the handler — this outer retry covers transient
|
|
44
|
+
* infrastructure failures of the whole fire, not per-item logic.
|
|
45
|
+
*/
|
|
46
|
+
retry?: RetryPolicy;
|
|
33
47
|
};
|
|
34
48
|
/**
|
|
35
49
|
* Define a scheduled function. Identity at runtime (for type inference). Register
|
|
@@ -4,6 +4,7 @@ import type { MutationDefinition, TableWriter, TransactionRunner } from './mutat
|
|
|
4
4
|
import type { ReactiveQueryDefinition, TableReader } from './reactive';
|
|
5
5
|
import { type BridgeFetchConfig, type HandlerMetricsHook } from './sandbox';
|
|
6
6
|
import type { PermissionsDefinition, TablePermissions } from './permissions';
|
|
7
|
+
import type { SyncPack } from './pack';
|
|
7
8
|
import type { SearchCollectionDefinition } from './search';
|
|
8
9
|
import type { ScheduleDefinition } from './schedule';
|
|
9
10
|
import type { EngineActivity, EngineInspection } from './devtools';
|
|
@@ -213,6 +214,18 @@ export type SyncEngine = {
|
|
|
213
214
|
* }
|
|
214
215
|
*/
|
|
215
216
|
streamChanges: (options?: StreamChangesOptions) => AsyncIterable<LoggedChange>;
|
|
217
|
+
/**
|
|
218
|
+
* Register a {@link SyncPack} — a self-contained bundle of schemas,
|
|
219
|
+
* permissions, readers/writers, collections, mutations, and schedules.
|
|
220
|
+
* Dispatches each field to the matching `register*` method. Rejects
|
|
221
|
+
* with {@link PackTableConflictError} if the pack claims a table
|
|
222
|
+
* another registered pack already owns; with
|
|
223
|
+
* {@link PackMissingDependencyError} if `requireDependencies` is set
|
|
224
|
+
* and a `readsTables` entry has no registered reader.
|
|
225
|
+
*
|
|
226
|
+
* See `syncPacks.design.md` for the rationale.
|
|
227
|
+
*/
|
|
228
|
+
registerPack: (pack: SyncPack) => void;
|
|
216
229
|
};
|
|
217
230
|
/**
|
|
218
231
|
* A single committed change as it appears in the engine's change log and on
|
package/dist/index.js
CHANGED
|
@@ -759,12 +759,7 @@ var wrap = (source) => `
|
|
|
759
759
|
}
|
|
760
760
|
`;
|
|
761
761
|
var compile = async (source, config, bridgeFetch) => {
|
|
762
|
-
const {
|
|
763
|
-
const isolate = await createIsolate({
|
|
764
|
-
backend: config.backend ?? "auto",
|
|
765
|
-
memoryLimit: config.memoryLimit ?? 32
|
|
766
|
-
});
|
|
767
|
-
const context = await isolate.createContext();
|
|
762
|
+
const { Reference, createIsolatedRunner, resolveIsolatePolicy } = await loadIsolatedJsc();
|
|
768
763
|
const callMap = new Map;
|
|
769
764
|
const dispatch = new Reference((callId, op, ...rest) => {
|
|
770
765
|
const a = callMap.get(callId);
|
|
@@ -788,15 +783,25 @@ var compile = async (source, config, bridgeFetch) => {
|
|
|
788
783
|
throw new Error(`unknown sandbox action op: ${String(op)}`);
|
|
789
784
|
}
|
|
790
785
|
});
|
|
791
|
-
|
|
792
|
-
const
|
|
786
|
+
const timeoutMs = config.timeout ?? 5000;
|
|
787
|
+
const sourceToCall = wrap(source);
|
|
788
|
+
const policy = resolveIsolatePolicy("tenant-script", {
|
|
789
|
+
allowWorkerFallback: true,
|
|
790
|
+
backend: config.backend ?? "auto",
|
|
791
|
+
memoryLimit: config.memoryLimit ?? 32,
|
|
792
|
+
timeout: timeoutMs
|
|
793
|
+
});
|
|
794
|
+
const runner = createIsolatedRunner({
|
|
795
|
+
globals: { __dispatch: dispatch },
|
|
796
|
+
policy
|
|
797
|
+
});
|
|
798
|
+
await runner.precompile("sandboxedHandler", sourceToCall);
|
|
793
799
|
return {
|
|
794
|
-
callable,
|
|
795
800
|
callMap,
|
|
796
|
-
context,
|
|
797
|
-
isolate,
|
|
798
801
|
nextCallId: 1,
|
|
799
|
-
|
|
802
|
+
runner,
|
|
803
|
+
source: sourceToCall,
|
|
804
|
+
timeoutMs
|
|
800
805
|
};
|
|
801
806
|
};
|
|
802
807
|
var runBridgeFetch = async (config, url, init) => {
|
|
@@ -852,10 +857,7 @@ var makeSandboxedHandler = (source, config = {}, engineExtras) => {
|
|
|
852
857
|
const bridgeFetch = engineExtras?.bridgeFetch;
|
|
853
858
|
const getCompiled = async () => {
|
|
854
859
|
if (pending !== undefined) {
|
|
855
|
-
|
|
856
|
-
if (!compiled.isolate.isDisposed)
|
|
857
|
-
return compiled;
|
|
858
|
-
pending = undefined;
|
|
860
|
+
return pending;
|
|
859
861
|
}
|
|
860
862
|
pending = compile(source, config, bridgeFetch);
|
|
861
863
|
return pending;
|
|
@@ -866,9 +868,13 @@ var makeSandboxedHandler = (source, config = {}, engineExtras) => {
|
|
|
866
868
|
compiled.callMap.set(callId, actions);
|
|
867
869
|
if (metricsHook === undefined) {
|
|
868
870
|
try {
|
|
869
|
-
return await compiled.
|
|
870
|
-
|
|
871
|
-
|
|
871
|
+
return await compiled.runner.call("sandboxedHandler", compiled.source, [callId, args, ctx], { run: { timeout: compiled.timeoutMs } });
|
|
872
|
+
} catch (error) {
|
|
873
|
+
if (isIsolateDisposalError(error)) {
|
|
874
|
+
pending = undefined;
|
|
875
|
+
await disposeCompiled(compiled);
|
|
876
|
+
}
|
|
877
|
+
throw error;
|
|
872
878
|
} finally {
|
|
873
879
|
compiled.callMap.delete(callId);
|
|
874
880
|
}
|
|
@@ -876,8 +882,9 @@ var makeSandboxedHandler = (source, config = {}, engineExtras) => {
|
|
|
876
882
|
const startedAt = performance.now();
|
|
877
883
|
const id = makeRandomId();
|
|
878
884
|
try {
|
|
879
|
-
const { result, metrics } = await compiled.
|
|
885
|
+
const { result, metrics } = await compiled.runner.call("sandboxedHandler", compiled.source, [callId, args, ctx], { run: { timeout: compiled.timeoutMs }, withMetrics: true });
|
|
880
886
|
fireMetrics(metricsHook.onMetrics, {
|
|
887
|
+
backend: metrics.backend,
|
|
881
888
|
cpuMs: metrics.cpuMs,
|
|
882
889
|
durationMs: performance.now() - startedAt,
|
|
883
890
|
heapBytes: metrics.heapBytes,
|
|
@@ -888,6 +895,10 @@ var makeSandboxedHandler = (source, config = {}, engineExtras) => {
|
|
|
888
895
|
});
|
|
889
896
|
return result;
|
|
890
897
|
} catch (error) {
|
|
898
|
+
if (isIsolateDisposalError(error)) {
|
|
899
|
+
pending = undefined;
|
|
900
|
+
await disposeCompiled(compiled);
|
|
901
|
+
}
|
|
891
902
|
fireMetrics(metricsHook.onMetrics, {
|
|
892
903
|
cpuMs: 0,
|
|
893
904
|
durationMs: performance.now() - startedAt,
|
|
@@ -906,6 +917,12 @@ var makeSandboxedHandler = (source, config = {}, engineExtras) => {
|
|
|
906
917
|
};
|
|
907
918
|
};
|
|
908
919
|
var makeRandomId = () => `hm_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
920
|
+
var isIsolateDisposalError = (error) => error instanceof Error && (error.name === "TimeoutError" || error.name === "MemoryLimitError" || error.name === "IsolateDisposedError");
|
|
921
|
+
var disposeCompiled = async (compiled) => {
|
|
922
|
+
try {
|
|
923
|
+
await compiled.runner.dispose();
|
|
924
|
+
} catch {}
|
|
925
|
+
};
|
|
909
926
|
var fireMetrics = (hook, record) => {
|
|
910
927
|
let outcome;
|
|
911
928
|
try {
|
|
@@ -918,6 +935,32 @@ var fireMetrics = (hook, record) => {
|
|
|
918
935
|
}
|
|
919
936
|
};
|
|
920
937
|
|
|
938
|
+
// src/engine/pack.ts
|
|
939
|
+
class PackTableConflictError extends Error {
|
|
940
|
+
table;
|
|
941
|
+
existingPack;
|
|
942
|
+
newPack;
|
|
943
|
+
constructor(table, existingPack, newPack) {
|
|
944
|
+
super(`Pack "${newPack}" claims table "${table}", but "${existingPack}" already owns it. Use a tablePrefix on one of them.`);
|
|
945
|
+
this.name = "PackTableConflictError";
|
|
946
|
+
this.table = table;
|
|
947
|
+
this.existingPack = existingPack;
|
|
948
|
+
this.newPack = newPack;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
class PackMissingDependencyError extends Error {
|
|
953
|
+
pack;
|
|
954
|
+
missingTable;
|
|
955
|
+
constructor(pack, missingTable) {
|
|
956
|
+
super(`Pack "${pack}" requires a reader for table "${missingTable}" but none is registered. Call engine.registerReader("${missingTable}", ...) before engine.registerPack.`);
|
|
957
|
+
this.name = "PackMissingDependencyError";
|
|
958
|
+
this.pack = pack;
|
|
959
|
+
this.missingTable = missingTable;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
var defineSyncPack = (pack) => pack;
|
|
963
|
+
|
|
921
964
|
// src/engine/search.ts
|
|
922
965
|
var SEARCH_SCORE_FIELD = "_score";
|
|
923
966
|
var defineSearchCollection = (definition) => ({
|
|
@@ -1027,6 +1070,8 @@ var createSyncEngine = (options = {}) => {
|
|
|
1027
1070
|
const writers = new Map;
|
|
1028
1071
|
const readers = new Map;
|
|
1029
1072
|
const schedules = new Map;
|
|
1073
|
+
const packTableOwners = new Map;
|
|
1074
|
+
const registeredPacks = [];
|
|
1030
1075
|
const permissions = new Map;
|
|
1031
1076
|
for (const [table, rules] of Object.entries(options.permissions ?? {})) {
|
|
1032
1077
|
permissions.set(table, rules);
|
|
@@ -1758,7 +1803,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
1758
1803
|
}
|
|
1759
1804
|
};
|
|
1760
1805
|
};
|
|
1761
|
-
|
|
1806
|
+
const engine = {
|
|
1762
1807
|
register: (collection) => {
|
|
1763
1808
|
registry.set(collection.name, collection);
|
|
1764
1809
|
for (const table of collection.tables ?? [collection.name]) {
|
|
@@ -2033,13 +2078,136 @@ var createSyncEngine = (options = {}) => {
|
|
|
2033
2078
|
throw new Error(`Unknown schedule "${name}"`);
|
|
2034
2079
|
}
|
|
2035
2080
|
const runHandler = async (tx) => {
|
|
2036
|
-
const { actions, buffered
|
|
2081
|
+
const { actions, buffered } = makeActions(tx, {}, false);
|
|
2037
2082
|
const db = makeReadHandle({}, new Set, new Set, [], false);
|
|
2038
2083
|
await schedule.run({ actions, db });
|
|
2039
|
-
return
|
|
2084
|
+
return buffered;
|
|
2040
2085
|
};
|
|
2041
|
-
const
|
|
2042
|
-
|
|
2086
|
+
const retry = schedule.retry;
|
|
2087
|
+
const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
|
|
2088
|
+
const isRetryable = retry?.isRetryable ?? isSerializationFailure;
|
|
2089
|
+
const computeDelay = retry?.backoff ?? exponentialBackoff();
|
|
2090
|
+
const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
|
|
2091
|
+
const startedAt = Date.now();
|
|
2092
|
+
let lastError;
|
|
2093
|
+
let attemptsMade = 0;
|
|
2094
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2095
|
+
attemptsMade = attempt;
|
|
2096
|
+
try {
|
|
2097
|
+
const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
|
|
2098
|
+
await applyChangeBatch(buffered);
|
|
2099
|
+
emitActivity({
|
|
2100
|
+
type: "schedule",
|
|
2101
|
+
at: Date.now(),
|
|
2102
|
+
name,
|
|
2103
|
+
status: "ok"
|
|
2104
|
+
});
|
|
2105
|
+
return;
|
|
2106
|
+
} catch (error) {
|
|
2107
|
+
lastError = error;
|
|
2108
|
+
const elapsedMs = Date.now() - startedAt;
|
|
2109
|
+
const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
|
|
2110
|
+
if (!canRetry)
|
|
2111
|
+
break;
|
|
2112
|
+
const rawDelay = computeDelay(attempt);
|
|
2113
|
+
const remaining = maxElapsedMs - elapsedMs;
|
|
2114
|
+
if (remaining <= 0)
|
|
2115
|
+
break;
|
|
2116
|
+
const delayMs = Math.max(0, Math.min(rawDelay, remaining));
|
|
2117
|
+
emitActivity({
|
|
2118
|
+
type: "scheduleRetry",
|
|
2119
|
+
at: Date.now(),
|
|
2120
|
+
name,
|
|
2121
|
+
attempt,
|
|
2122
|
+
delayMs,
|
|
2123
|
+
errorName: error instanceof Error ? error.name : "Error",
|
|
2124
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2125
|
+
});
|
|
2126
|
+
if (delayMs > 0) {
|
|
2127
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
emitActivity({
|
|
2132
|
+
type: "schedule",
|
|
2133
|
+
at: Date.now(),
|
|
2134
|
+
name,
|
|
2135
|
+
status: "error"
|
|
2136
|
+
});
|
|
2137
|
+
if (attemptsMade > 1) {
|
|
2138
|
+
throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
|
|
2139
|
+
}
|
|
2140
|
+
throw lastError;
|
|
2141
|
+
},
|
|
2142
|
+
registerPack: (pack) => {
|
|
2143
|
+
for (const table of pack.ownsTables) {
|
|
2144
|
+
const existing = packTableOwners.get(table);
|
|
2145
|
+
if (existing !== undefined) {
|
|
2146
|
+
throw new PackTableConflictError(table, existing, pack.name);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
if (pack.requireDependencies === true) {
|
|
2150
|
+
for (const table of pack.readsTables ?? []) {
|
|
2151
|
+
if (!readers.has(table)) {
|
|
2152
|
+
throw new PackMissingDependencyError(pack.name, table);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
if (pack.schemas !== undefined) {
|
|
2157
|
+
for (const [table, schema] of Object.entries(pack.schemas)) {
|
|
2158
|
+
engine.registerSchema(table, schema);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
if (pack.permissions !== undefined) {
|
|
2162
|
+
for (const [table, rules] of Object.entries(pack.permissions)) {
|
|
2163
|
+
engine.registerPermissions(table, rules);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
if (pack.readers !== undefined) {
|
|
2167
|
+
for (const [table, reader] of Object.entries(pack.readers)) {
|
|
2168
|
+
engine.registerReader(table, reader);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
if (pack.writers !== undefined) {
|
|
2172
|
+
for (const [table, writer] of Object.entries(pack.writers)) {
|
|
2173
|
+
engine.registerWriter(table, writer);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
if (pack.crdt !== undefined) {
|
|
2177
|
+
for (const [table, fields] of Object.entries(pack.crdt)) {
|
|
2178
|
+
engine.registerCrdt(table, fields);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
for (const collection of pack.collections ?? []) {
|
|
2182
|
+
engine.register(collection);
|
|
2183
|
+
}
|
|
2184
|
+
for (const collection of pack.joinCollections ?? []) {
|
|
2185
|
+
engine.registerJoin(collection);
|
|
2186
|
+
}
|
|
2187
|
+
for (const collection of pack.graphCollections ?? []) {
|
|
2188
|
+
engine.registerGraph(collection);
|
|
2189
|
+
}
|
|
2190
|
+
for (const collection of pack.searchCollections ?? []) {
|
|
2191
|
+
engine.registerSearch(collection);
|
|
2192
|
+
}
|
|
2193
|
+
for (const query of pack.reactiveQueries ?? []) {
|
|
2194
|
+
engine.registerReactive(query);
|
|
2195
|
+
}
|
|
2196
|
+
for (const mutation of pack.mutations ?? []) {
|
|
2197
|
+
engine.registerMutation(mutation);
|
|
2198
|
+
}
|
|
2199
|
+
for (const schedule of pack.schedules ?? []) {
|
|
2200
|
+
engine.registerSchedule(schedule);
|
|
2201
|
+
}
|
|
2202
|
+
for (const table of pack.ownsTables) {
|
|
2203
|
+
packTableOwners.set(table, pack.name);
|
|
2204
|
+
}
|
|
2205
|
+
registeredPacks.push({
|
|
2206
|
+
name: pack.name,
|
|
2207
|
+
version: pack.version,
|
|
2208
|
+
ownsTables: [...pack.ownsTables],
|
|
2209
|
+
readsTables: [...pack.readsTables ?? []]
|
|
2210
|
+
});
|
|
2043
2211
|
},
|
|
2044
2212
|
inspect: () => {
|
|
2045
2213
|
const collections = [...registry.entries()].map(([name, def]) => {
|
|
@@ -2079,6 +2247,12 @@ var createSyncEngine = (options = {}) => {
|
|
|
2079
2247
|
version: entry.version,
|
|
2080
2248
|
table: entry.table,
|
|
2081
2249
|
op: entry.change.op
|
|
2250
|
+
})),
|
|
2251
|
+
packs: registeredPacks.map((pack) => ({
|
|
2252
|
+
name: pack.name,
|
|
2253
|
+
version: pack.version,
|
|
2254
|
+
ownsTables: [...pack.ownsTables],
|
|
2255
|
+
readsTables: [...pack.readsTables]
|
|
2082
2256
|
}))
|
|
2083
2257
|
};
|
|
2084
2258
|
},
|
|
@@ -2165,6 +2339,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2165
2339
|
};
|
|
2166
2340
|
}
|
|
2167
2341
|
};
|
|
2342
|
+
return engine;
|
|
2168
2343
|
};
|
|
2169
2344
|
|
|
2170
2345
|
// src/engine/cdc.ts
|
|
@@ -2443,5 +2618,5 @@ export {
|
|
|
2443
2618
|
createPresenceHub
|
|
2444
2619
|
};
|
|
2445
2620
|
|
|
2446
|
-
//# debugId=
|
|
2621
|
+
//# debugId=483AEB81CDF95A5764756E2164756E21
|
|
2447
2622
|
//# sourceMappingURL=index.js.map
|