@absolutejs/sync 1.7.5 → 1.7.7
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 +58 -7
- package/dist/engine/index.js.map +5 -5
- package/dist/engine/mutation.d.ts +22 -0
- package/dist/engine/sandbox.d.ts +68 -1
- package/dist/engine/syncEngine.d.ts +18 -0
- package/dist/index.js +58 -7
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
|
@@ -45,6 +45,28 @@ export type MutationActions = {
|
|
|
45
45
|
delete: (table: string, row: unknown) => Promise<void>;
|
|
46
46
|
/** Escape hatch: emit a change you persisted yourself (no writer call). */
|
|
47
47
|
change: <T>(collection: string, change: RowChange<T>) => Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Wall-clock timestamp the handler should use instead of `Date.now()`.
|
|
50
|
+
* Returns a `number` (ms since epoch).
|
|
51
|
+
*
|
|
52
|
+
* Why an injected clock? Two forward-looking reasons:
|
|
53
|
+
*
|
|
54
|
+
* 1. **Replay / rebase determinism.** When the engine re-runs a
|
|
55
|
+
* mutation against an updated state (Replicache-style mutator
|
|
56
|
+
* replay), `actions.now()` returns the ORIGINAL call's timestamp
|
|
57
|
+
* instead of the current wall clock. `Date.now()` would silently
|
|
58
|
+
* diverge between client-optimistic and server-canonical runs;
|
|
59
|
+
* `actions.now()` doesn't.
|
|
60
|
+
*
|
|
61
|
+
* 2. **Test determinism.** Test harnesses can pin time by passing a
|
|
62
|
+
* custom `now()` through {@link MutationActions} — the handler
|
|
63
|
+
* observes whatever the test wants.
|
|
64
|
+
*
|
|
65
|
+
* If the engine doesn't override it (the common case today), it just
|
|
66
|
+
* returns `Date.now()`. Use it everywhere you'd reach for
|
|
67
|
+
* `Date.now()` inside a mutation handler.
|
|
68
|
+
*/
|
|
69
|
+
now: () => number;
|
|
48
70
|
};
|
|
49
71
|
export type MutationHandler<Args, Ctx, Result> = (args: Args, ctx: Ctx, actions: MutationActions) => Promise<Result> | Result;
|
|
50
72
|
export type MutationDefinition<Args = unknown, Ctx = CollectionContext, Result = unknown> = {
|
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
|
@@ -737,7 +737,8 @@ var wrap = (source) => `
|
|
|
737
737
|
insert: (table, data) => __dispatch(__callId, 'insert', table, data),
|
|
738
738
|
update: (table, data) => __dispatch(__callId, 'update', table, data),
|
|
739
739
|
delete: (table, row) => __dispatch(__callId, 'delete', table, row),
|
|
740
|
-
change: (collection, change) => __dispatch(__callId, 'change', collection, change)
|
|
740
|
+
change: (collection, change) => __dispatch(__callId, 'change', collection, change),
|
|
741
|
+
now: () => __dispatch(__callId, 'now')
|
|
741
742
|
};
|
|
742
743
|
return userFn(args, ctx, actions);
|
|
743
744
|
}
|
|
@@ -764,6 +765,8 @@ var compile = async (source, config) => {
|
|
|
764
765
|
return a.delete(rest[0], rest[1]);
|
|
765
766
|
case "change":
|
|
766
767
|
return a.change(rest[0], rest[1]);
|
|
768
|
+
case "now":
|
|
769
|
+
return a.now();
|
|
767
770
|
default:
|
|
768
771
|
throw new Error(`unknown sandbox action op: ${String(op)}`);
|
|
769
772
|
}
|
|
@@ -779,7 +782,7 @@ var compile = async (source, config) => {
|
|
|
779
782
|
timeoutMs: config.timeout ?? 5000
|
|
780
783
|
};
|
|
781
784
|
};
|
|
782
|
-
var makeSandboxedHandler = (source, config = {}) => {
|
|
785
|
+
var makeSandboxedHandler = (source, config = {}, metricsHook) => {
|
|
783
786
|
let pending;
|
|
784
787
|
const getCompiled = async () => {
|
|
785
788
|
if (pending !== undefined) {
|
|
@@ -795,15 +798,59 @@ var makeSandboxedHandler = (source, config = {}) => {
|
|
|
795
798
|
const compiled = await getCompiled();
|
|
796
799
|
const callId = compiled.nextCallId++;
|
|
797
800
|
compiled.callMap.set(callId, actions);
|
|
801
|
+
if (metricsHook === undefined) {
|
|
802
|
+
try {
|
|
803
|
+
return await compiled.callable.call([callId, args, ctx], {
|
|
804
|
+
timeout: compiled.timeoutMs
|
|
805
|
+
});
|
|
806
|
+
} finally {
|
|
807
|
+
compiled.callMap.delete(callId);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
const startedAt = performance.now();
|
|
811
|
+
const id = makeRandomId();
|
|
798
812
|
try {
|
|
799
|
-
|
|
800
|
-
|
|
813
|
+
const { result, metrics } = await compiled.callable.callWithMetrics([callId, args, ctx], { timeout: compiled.timeoutMs });
|
|
814
|
+
fireMetrics(metricsHook.onMetrics, {
|
|
815
|
+
cpuMs: metrics.cpuMs,
|
|
816
|
+
durationMs: performance.now() - startedAt,
|
|
817
|
+
heapBytes: metrics.heapBytes,
|
|
818
|
+
id,
|
|
819
|
+
mutationName: metricsHook.mutationName,
|
|
820
|
+
ok: true,
|
|
821
|
+
timestamp: Date.now()
|
|
822
|
+
});
|
|
823
|
+
return result;
|
|
824
|
+
} catch (error) {
|
|
825
|
+
fireMetrics(metricsHook.onMetrics, {
|
|
826
|
+
cpuMs: 0,
|
|
827
|
+
durationMs: performance.now() - startedAt,
|
|
828
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
829
|
+
errorName: error instanceof Error ? error.name : "Error",
|
|
830
|
+
heapBytes: 0,
|
|
831
|
+
id,
|
|
832
|
+
mutationName: metricsHook.mutationName,
|
|
833
|
+
ok: false,
|
|
834
|
+
timestamp: Date.now()
|
|
801
835
|
});
|
|
836
|
+
throw error;
|
|
802
837
|
} finally {
|
|
803
838
|
compiled.callMap.delete(callId);
|
|
804
839
|
}
|
|
805
840
|
};
|
|
806
841
|
};
|
|
842
|
+
var makeRandomId = () => `hm_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
843
|
+
var fireMetrics = (hook, record) => {
|
|
844
|
+
let outcome;
|
|
845
|
+
try {
|
|
846
|
+
outcome = hook(record);
|
|
847
|
+
} catch {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (outcome instanceof Promise) {
|
|
851
|
+
outcome.catch(() => {});
|
|
852
|
+
}
|
|
853
|
+
};
|
|
807
854
|
|
|
808
855
|
// src/engine/search.ts
|
|
809
856
|
var SEARCH_SCORE_FIELD = "_score";
|
|
@@ -1242,7 +1289,8 @@ var createSyncEngine = (options = {}) => {
|
|
|
1242
1289
|
}
|
|
1243
1290
|
await writerFor(table).delete(row, ctx, tx);
|
|
1244
1291
|
buffered.push({ table, change: { op: "delete", row } });
|
|
1245
|
-
}
|
|
1292
|
+
},
|
|
1293
|
+
now: () => Date.now()
|
|
1246
1294
|
};
|
|
1247
1295
|
return { actions, buffered };
|
|
1248
1296
|
};
|
|
@@ -1799,7 +1847,10 @@ var createSyncEngine = (options = {}) => {
|
|
|
1799
1847
|
}
|
|
1800
1848
|
mutations.set(mutation.name, mutation);
|
|
1801
1849
|
if (mutation.sandboxedHandler !== undefined) {
|
|
1802
|
-
sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox
|
|
1850
|
+
sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox, options.handlerMetrics === undefined ? undefined : {
|
|
1851
|
+
mutationName: mutation.name,
|
|
1852
|
+
onMetrics: options.handlerMetrics
|
|
1853
|
+
}));
|
|
1803
1854
|
}
|
|
1804
1855
|
},
|
|
1805
1856
|
registerWriter: (table, writer) => {
|
|
@@ -2323,5 +2374,5 @@ export {
|
|
|
2323
2374
|
createPresenceHub
|
|
2324
2375
|
};
|
|
2325
2376
|
|
|
2326
|
-
//# debugId=
|
|
2377
|
+
//# debugId=0583BD887ED853F264756E2164756E21
|
|
2327
2378
|
//# sourceMappingURL=index.js.map
|