@absolutejs/sync 1.7.4 → 1.7.6
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/index.d.ts +1 -1
- package/dist/engine/index.js +86 -26
- package/dist/engine/index.js.map +4 -4
- package/dist/engine/sandbox.d.ts +79 -10
- package/dist/engine/syncEngine.d.ts +18 -0
- package/dist/index.js +86 -26
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/engine/sandbox.d.ts
CHANGED
|
@@ -37,6 +37,60 @@
|
|
|
37
37
|
* needed — the OS reaps the workers when the engine's host process exits.
|
|
38
38
|
*/
|
|
39
39
|
import type { MutationActions } from './mutation';
|
|
40
|
+
/**
|
|
41
|
+
* Per-call metrics record emitted by `sandboxedHandler` when the engine
|
|
42
|
+
* is configured with {@link SyncEngineOptions.handlerMetrics}. One record
|
|
43
|
+
* per invocation, fired AFTER the call completes (success or failure).
|
|
44
|
+
*
|
|
45
|
+
* Use this for per-tenant dashboards ("which tenant is burning the most
|
|
46
|
+
* CPU?"), runtime alerting ("this handler is timing out repeatedly"),
|
|
47
|
+
* cost attribution, and post-mortem replay of slow / failed mutations.
|
|
48
|
+
*
|
|
49
|
+
* Sample wiring pattern — publish to a sync collection users can
|
|
50
|
+
* subscribe to like any other:
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* const engine = createSyncEngine({
|
|
54
|
+
* handlerMetrics: (record) => {
|
|
55
|
+
* metricsCollection.insert(record); // your own collection / sink
|
|
56
|
+
* },
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export type HandlerMetricsRecord = {
|
|
61
|
+
/** Globally-unique id for this call (random). Useful as a join key. */
|
|
62
|
+
id: string;
|
|
63
|
+
/** Name passed to `defineMutation`. */
|
|
64
|
+
mutationName: string;
|
|
65
|
+
/** Wall-clock duration from call entry to result resolution (ms). */
|
|
66
|
+
durationMs: number;
|
|
67
|
+
/**
|
|
68
|
+
* CPU time spent inside the JSC sandbox (ms). Comes from
|
|
69
|
+
* `Script.runWithMetrics` — does NOT include host-side message-passing
|
|
70
|
+
* overhead on the Worker backend. Sub-millisecond runs round to 0.
|
|
71
|
+
*/
|
|
72
|
+
cpuMs: number;
|
|
73
|
+
/**
|
|
74
|
+
* Heap size (bytes) measured immediately after the script returned.
|
|
75
|
+
* Not the run's peak — a true peak needs continuous polling.
|
|
76
|
+
*/
|
|
77
|
+
heapBytes: number;
|
|
78
|
+
/** `true` if the handler returned normally; `false` if it threw. */
|
|
79
|
+
ok: boolean;
|
|
80
|
+
/** Error name (`TimeoutError`, `MemoryLimitError`, `Error`, …) on failure. */
|
|
81
|
+
errorName?: string;
|
|
82
|
+
/** Error message on failure. */
|
|
83
|
+
errorMessage?: string;
|
|
84
|
+
/** `Date.now()` at the moment the call ended. */
|
|
85
|
+
timestamp: number;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Per-call hook invoked once each `sandboxedHandler` invocation finishes
|
|
89
|
+
* (success or failure). Synchronous return is the common case; an async
|
|
90
|
+
* return is awaited but its rejection is swallowed (a metrics hook that
|
|
91
|
+
* crashes must NOT also crash the caller's mutation path).
|
|
92
|
+
*/
|
|
93
|
+
export type HandlerMetricsHook = (record: HandlerMetricsRecord) => void | Promise<void>;
|
|
40
94
|
/** Per-mutation sandbox configuration. */
|
|
41
95
|
export type SandboxConfig = {
|
|
42
96
|
/** Heap memory cap (MB). Default 32. */
|
|
@@ -60,15 +114,30 @@ export type SandboxConfig = {
|
|
|
60
114
|
};
|
|
61
115
|
/**
|
|
62
116
|
* Build a lazy runner for one mutation's sandboxed source. The first call
|
|
63
|
-
* compiles the isolate + context +
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
117
|
+
* compiles the isolate + context + dispatch Reference + callable;
|
|
118
|
+
* subsequent calls only generate a fresh callId, register the per-call
|
|
119
|
+
* `actions` in the callMap, and invoke `callable.call([callId, args,
|
|
120
|
+
* ctx])`. Per-call cost on FFI: one JSObjectCallAsFunction + three
|
|
121
|
+
* cheap primitive packings. No per-call Reference allocation, no
|
|
122
|
+
* setGlobal, no eval.
|
|
123
|
+
*
|
|
124
|
+
* Concurrency-safe by construction: each call has its own callId →
|
|
125
|
+
* its own actions slot in the callMap.
|
|
126
|
+
*
|
|
127
|
+
* If the isolate has been disposed (timeout, memory cap), the next
|
|
128
|
+
* call re-spawns transparently.
|
|
129
|
+
*/
|
|
130
|
+
export declare const makeSandboxedHandler: (source: string, config?: SandboxConfig,
|
|
131
|
+
/**
|
|
132
|
+
* Optional hook + tagging. When `onMetrics` is supplied, every call
|
|
133
|
+
* uses `callable.callWithMetrics` (slightly costlier) and fires the
|
|
134
|
+
* hook on completion. Without it, the cheap `callable.call` path is
|
|
135
|
+
* used and nothing changes vs the pre-1.7.6 contract.
|
|
68
136
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* promise queue needed. (Reference allocation cost is ~0.003 ms per
|
|
72
|
-
* call — negligible.)
|
|
137
|
+
* `mutationName` only matters when `onMetrics` is set — it's the
|
|
138
|
+
* `mutationName` field of the emitted record.
|
|
73
139
|
*/
|
|
74
|
-
|
|
140
|
+
metricsHook?: {
|
|
141
|
+
mutationName: string;
|
|
142
|
+
onMetrics: HandlerMetricsHook;
|
|
143
|
+
}) => ((args: unknown, ctx: unknown, actions: MutationActions) => Promise<unknown>);
|
|
@@ -2,6 +2,7 @@ import type { CollectionContext, CollectionDefinition, JoinCollectionDefinition
|
|
|
2
2
|
import type { GraphCollectionDefinition } from './graph';
|
|
3
3
|
import type { MutationDefinition, TableWriter, TransactionRunner } from './mutation';
|
|
4
4
|
import type { ReactiveQueryDefinition, TableReader } from './reactive';
|
|
5
|
+
import { type HandlerMetricsHook } from './sandbox';
|
|
5
6
|
import type { PermissionsDefinition, TablePermissions } from './permissions';
|
|
6
7
|
import type { SearchCollectionDefinition } from './search';
|
|
7
8
|
import type { ScheduleDefinition } from './schedule';
|
|
@@ -308,6 +309,23 @@ export type SyncEngineOptions = {
|
|
|
308
309
|
max?: number;
|
|
309
310
|
ttlMs?: number;
|
|
310
311
|
};
|
|
312
|
+
/**
|
|
313
|
+
* Per-call telemetry for `sandboxedHandler` mutations. When set, every
|
|
314
|
+
* sandboxed call fires `onMetrics(record)` after completion with
|
|
315
|
+
* `{ id, mutationName, durationMs, cpuMs, heapBytes, ok, errorName,
|
|
316
|
+
* errorMessage, timestamp }`. Wire to a sync collection, your
|
|
317
|
+
* observability backend, a Drizzle table, anything you want.
|
|
318
|
+
*
|
|
319
|
+
* Hook failures are swallowed (a misbehaving metrics sink must NOT
|
|
320
|
+
* crash the caller's mutation). Adding the hook switches the runner
|
|
321
|
+
* to `callable.callWithMetrics`, which is a small per-call cost
|
|
322
|
+
* (~0.05 ms) — disable for hot-path mutations that don't need it.
|
|
323
|
+
*
|
|
324
|
+
* Off by default.
|
|
325
|
+
*
|
|
326
|
+
* @see {@link HandlerMetricsRecord}
|
|
327
|
+
*/
|
|
328
|
+
handlerMetrics?: HandlerMetricsHook;
|
|
311
329
|
};
|
|
312
330
|
/**
|
|
313
331
|
* The Tier 3 sync engine: a registry of collections plus the view syncer. It is
|
package/dist/index.js
CHANGED
|
@@ -725,7 +725,7 @@ var loadIsolatedJsc = async () => {
|
|
|
725
725
|
}
|
|
726
726
|
};
|
|
727
727
|
var wrap = (source) => `
|
|
728
|
-
function (args, ctx
|
|
728
|
+
function (__callId, args, ctx) {
|
|
729
729
|
const userFn = (${source});
|
|
730
730
|
if (typeof userFn !== 'function') {
|
|
731
731
|
throw new Error(
|
|
@@ -734,30 +734,52 @@ var wrap = (source) => `
|
|
|
734
734
|
);
|
|
735
735
|
}
|
|
736
736
|
const actions = {
|
|
737
|
-
insert: (table, data) => __dispatch('insert', table, data),
|
|
738
|
-
update: (table, data) => __dispatch('update', table, data),
|
|
739
|
-
delete: (table, row) => __dispatch('delete', table, row),
|
|
740
|
-
change: (collection, change) => __dispatch('change', collection, change)
|
|
737
|
+
insert: (table, data) => __dispatch(__callId, 'insert', table, data),
|
|
738
|
+
update: (table, data) => __dispatch(__callId, 'update', table, data),
|
|
739
|
+
delete: (table, row) => __dispatch(__callId, 'delete', table, row),
|
|
740
|
+
change: (collection, change) => __dispatch(__callId, 'change', collection, change)
|
|
741
741
|
};
|
|
742
742
|
return userFn(args, ctx, actions);
|
|
743
743
|
}
|
|
744
744
|
`;
|
|
745
745
|
var compile = async (source, config) => {
|
|
746
|
-
const { createIsolate } = await loadIsolatedJsc();
|
|
746
|
+
const { createIsolate, Reference } = await loadIsolatedJsc();
|
|
747
747
|
const isolate = await createIsolate({
|
|
748
748
|
backend: config.backend ?? "auto",
|
|
749
749
|
memoryLimit: config.memoryLimit ?? 32
|
|
750
750
|
});
|
|
751
751
|
const context = await isolate.createContext();
|
|
752
|
+
const callMap = new Map;
|
|
753
|
+
const dispatch = new Reference((callId, op, ...rest) => {
|
|
754
|
+
const a = callMap.get(callId);
|
|
755
|
+
if (a === undefined) {
|
|
756
|
+
throw new Error(`__dispatch invoked for orphan callId ${String(callId)}`);
|
|
757
|
+
}
|
|
758
|
+
switch (op) {
|
|
759
|
+
case "insert":
|
|
760
|
+
return a.insert(rest[0], rest[1]);
|
|
761
|
+
case "update":
|
|
762
|
+
return a.update(rest[0], rest[1]);
|
|
763
|
+
case "delete":
|
|
764
|
+
return a.delete(rest[0], rest[1]);
|
|
765
|
+
case "change":
|
|
766
|
+
return a.change(rest[0], rest[1]);
|
|
767
|
+
default:
|
|
768
|
+
throw new Error(`unknown sandbox action op: ${String(op)}`);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
await context.setGlobal("__dispatch", dispatch);
|
|
752
772
|
const callable = await context.compileCallable(wrap(source));
|
|
753
773
|
return {
|
|
754
774
|
callable,
|
|
775
|
+
callMap,
|
|
755
776
|
context,
|
|
756
777
|
isolate,
|
|
778
|
+
nextCallId: 1,
|
|
757
779
|
timeoutMs: config.timeout ?? 5000
|
|
758
780
|
};
|
|
759
781
|
};
|
|
760
|
-
var makeSandboxedHandler = (source, config = {}) => {
|
|
782
|
+
var makeSandboxedHandler = (source, config = {}, metricsHook) => {
|
|
761
783
|
let pending;
|
|
762
784
|
const getCompiled = async () => {
|
|
763
785
|
if (pending !== undefined) {
|
|
@@ -770,27 +792,62 @@ var makeSandboxedHandler = (source, config = {}) => {
|
|
|
770
792
|
return pending;
|
|
771
793
|
};
|
|
772
794
|
return async (args, ctx, actions) => {
|
|
773
|
-
const { Reference } = await loadIsolatedJsc();
|
|
774
795
|
const compiled = await getCompiled();
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
return actions.change(rest[0], rest[1]);
|
|
785
|
-
default:
|
|
786
|
-
throw new Error(`unknown sandbox action op: ${String(op)}`);
|
|
796
|
+
const callId = compiled.nextCallId++;
|
|
797
|
+
compiled.callMap.set(callId, actions);
|
|
798
|
+
if (metricsHook === undefined) {
|
|
799
|
+
try {
|
|
800
|
+
return await compiled.callable.call([callId, args, ctx], {
|
|
801
|
+
timeout: compiled.timeoutMs
|
|
802
|
+
});
|
|
803
|
+
} finally {
|
|
804
|
+
compiled.callMap.delete(callId);
|
|
787
805
|
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
806
|
+
}
|
|
807
|
+
const startedAt = performance.now();
|
|
808
|
+
const id = makeRandomId();
|
|
809
|
+
try {
|
|
810
|
+
const { result, metrics } = await compiled.callable.callWithMetrics([callId, args, ctx], { timeout: compiled.timeoutMs });
|
|
811
|
+
fireMetrics(metricsHook.onMetrics, {
|
|
812
|
+
cpuMs: metrics.cpuMs,
|
|
813
|
+
durationMs: performance.now() - startedAt,
|
|
814
|
+
heapBytes: metrics.heapBytes,
|
|
815
|
+
id,
|
|
816
|
+
mutationName: metricsHook.mutationName,
|
|
817
|
+
ok: true,
|
|
818
|
+
timestamp: Date.now()
|
|
819
|
+
});
|
|
820
|
+
return result;
|
|
821
|
+
} catch (error) {
|
|
822
|
+
fireMetrics(metricsHook.onMetrics, {
|
|
823
|
+
cpuMs: 0,
|
|
824
|
+
durationMs: performance.now() - startedAt,
|
|
825
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
826
|
+
errorName: error instanceof Error ? error.name : "Error",
|
|
827
|
+
heapBytes: 0,
|
|
828
|
+
id,
|
|
829
|
+
mutationName: metricsHook.mutationName,
|
|
830
|
+
ok: false,
|
|
831
|
+
timestamp: Date.now()
|
|
832
|
+
});
|
|
833
|
+
throw error;
|
|
834
|
+
} finally {
|
|
835
|
+
compiled.callMap.delete(callId);
|
|
836
|
+
}
|
|
792
837
|
};
|
|
793
838
|
};
|
|
839
|
+
var makeRandomId = () => `hm_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
840
|
+
var fireMetrics = (hook, record) => {
|
|
841
|
+
let outcome;
|
|
842
|
+
try {
|
|
843
|
+
outcome = hook(record);
|
|
844
|
+
} catch {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
if (outcome instanceof Promise) {
|
|
848
|
+
outcome.catch(() => {});
|
|
849
|
+
}
|
|
850
|
+
};
|
|
794
851
|
|
|
795
852
|
// src/engine/search.ts
|
|
796
853
|
var SEARCH_SCORE_FIELD = "_score";
|
|
@@ -1786,7 +1843,10 @@ var createSyncEngine = (options = {}) => {
|
|
|
1786
1843
|
}
|
|
1787
1844
|
mutations.set(mutation.name, mutation);
|
|
1788
1845
|
if (mutation.sandboxedHandler !== undefined) {
|
|
1789
|
-
sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox
|
|
1846
|
+
sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox, options.handlerMetrics === undefined ? undefined : {
|
|
1847
|
+
mutationName: mutation.name,
|
|
1848
|
+
onMetrics: options.handlerMetrics
|
|
1849
|
+
}));
|
|
1790
1850
|
}
|
|
1791
1851
|
},
|
|
1792
1852
|
registerWriter: (table, writer) => {
|
|
@@ -2310,5 +2370,5 @@ export {
|
|
|
2310
2370
|
createPresenceHub
|
|
2311
2371
|
};
|
|
2312
2372
|
|
|
2313
|
-
//# debugId=
|
|
2373
|
+
//# debugId=9570274420A04D4164756E2164756E21
|
|
2314
2374
|
//# sourceMappingURL=index.js.map
|