@absolutejs/sync 1.7.5 → 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 +52 -5
- package/dist/engine/index.js.map +4 -4
- package/dist/engine/sandbox.d.ts +68 -1
- package/dist/engine/syncEngine.d.ts +18 -0
- package/dist/index.js +52 -5
- 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. */
|
|
@@ -73,4 +127,17 @@ export type SandboxConfig = {
|
|
|
73
127
|
* If the isolate has been disposed (timeout, memory cap), the next
|
|
74
128
|
* call re-spawns transparently.
|
|
75
129
|
*/
|
|
76
|
-
export declare const makeSandboxedHandler: (source: string, config?: SandboxConfig
|
|
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.
|
|
136
|
+
*
|
|
137
|
+
* `mutationName` only matters when `onMetrics` is set — it's the
|
|
138
|
+
* `mutationName` field of the emitted record.
|
|
139
|
+
*/
|
|
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
|
@@ -779,7 +779,7 @@ var compile = async (source, config) => {
|
|
|
779
779
|
timeoutMs: config.timeout ?? 5000
|
|
780
780
|
};
|
|
781
781
|
};
|
|
782
|
-
var makeSandboxedHandler = (source, config = {}) => {
|
|
782
|
+
var makeSandboxedHandler = (source, config = {}, metricsHook) => {
|
|
783
783
|
let pending;
|
|
784
784
|
const getCompiled = async () => {
|
|
785
785
|
if (pending !== undefined) {
|
|
@@ -795,15 +795,59 @@ var makeSandboxedHandler = (source, config = {}) => {
|
|
|
795
795
|
const compiled = await getCompiled();
|
|
796
796
|
const callId = compiled.nextCallId++;
|
|
797
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);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const startedAt = performance.now();
|
|
808
|
+
const id = makeRandomId();
|
|
798
809
|
try {
|
|
799
|
-
|
|
800
|
-
|
|
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()
|
|
801
832
|
});
|
|
833
|
+
throw error;
|
|
802
834
|
} finally {
|
|
803
835
|
compiled.callMap.delete(callId);
|
|
804
836
|
}
|
|
805
837
|
};
|
|
806
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
|
+
};
|
|
807
851
|
|
|
808
852
|
// src/engine/search.ts
|
|
809
853
|
var SEARCH_SCORE_FIELD = "_score";
|
|
@@ -1799,7 +1843,10 @@ var createSyncEngine = (options = {}) => {
|
|
|
1799
1843
|
}
|
|
1800
1844
|
mutations.set(mutation.name, mutation);
|
|
1801
1845
|
if (mutation.sandboxedHandler !== undefined) {
|
|
1802
|
-
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
|
+
}));
|
|
1803
1850
|
}
|
|
1804
1851
|
},
|
|
1805
1852
|
registerWriter: (table, writer) => {
|
|
@@ -2323,5 +2370,5 @@ export {
|
|
|
2323
2370
|
createPresenceHub
|
|
2324
2371
|
};
|
|
2325
2372
|
|
|
2326
|
-
//# debugId=
|
|
2373
|
+
//# debugId=9570274420A04D4164756E2164756E21
|
|
2327
2374
|
//# sourceMappingURL=index.js.map
|