@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.
@@ -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) => ((args: unknown, ctx: unknown, actions: MutationActions) => Promise<unknown>);
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
- return await compiled.callable.call([callId, args, ctx], {
800
- timeout: compiled.timeoutMs
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=B9056CA0A3DAE81164756E2164756E21
2373
+ //# debugId=9570274420A04D4164756E2164756E21
2327
2374
  //# sourceMappingURL=index.js.map