@absolutejs/sync 1.7.3 → 1.7.5

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.
@@ -8,30 +8,30 @@
8
8
  *
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
- * `args` / `ctx` clones and the `actions` References we pass in.
12
- * - First call per mutation pays a Worker spawn + compile (~30 ms). Every
13
- * subsequent call reuses the isolate and only spends ~0.5 ms creating a
14
- * fresh context.
11
+ * `args` / `ctx` clones and the `actions` Reference we pass in.
12
+ * - First call per mutation pays an isolate spawn + compile (~3–25 ms
13
+ * depending on backend). Every subsequent call is a single
14
+ * `JSObjectCallAsFunction` (FFI) or one postMessage (Worker) — no
15
+ * per-call eval, no per-call `setGlobal`.
15
16
  * - Timeout terminates the isolate (the sandbox runner detects this and
16
17
  * lazily re-spawns on the next call). On the FFI backend timeouts throw
17
18
  * a TerminationException without killing the isolate; sync's runner
18
19
  * treats both shapes the same.
19
- * - Each per-call context retains some JSC metadata until the isolate's
20
- * next GC sweep. Empirically ~2 MB residual per call (Worker backend).
21
- * For long-lived mutations choose `memoryLimit` ≥ 128 (the default 32
22
- * trips after a few dozen calls without pressure for GC).
23
20
  *
24
- * **Backend default: `'auto'`** — isolated-jsc 0.4 added an async host-fn
25
- * pump on the FFI backend (alternates Bun event-loop yields with JSC
26
- * microtask drains, bounded by `Script.run`'s `timeout`), so the
27
- * `actions.insert/update/delete/change` async References settle on FFI
28
- * just like they do on Worker. `'auto'` picks FFI when libJSC is reachable
21
+ * **Backend default: `'auto'`** — `'auto'` picks FFI when libJSC is reachable
29
22
  * (~300 KB cold heap, interrupt-driven CPU timeouts) and falls back to
30
23
  * Worker (~46 MB cold heap, postMessage round-trips) otherwise. Pin to
31
24
  * `'worker'` if you specifically need Web APIs (`URL`, `TextEncoder`,
32
25
  * `WebSocket`) inside your handler — those live in the Bun-Worker
33
26
  * environment, not the bare JSC C API.
34
27
  *
28
+ * **Per-call hot path (since 1.7.4 / isolated-jsc 0.6).** Each mutation is
29
+ * compiled to a {@link Callable} once — a precompiled function expression
30
+ * the sandbox owns by reference. Per call we invoke
31
+ * `callable.call([args, ctx, dispatch])` where `dispatch` is a Reference
32
+ * that bridges `actions.*` back to the host. No globals, no eval per call,
33
+ * no shared-slot serialization machinery.
34
+ *
35
35
  * The runner is built lazily per-mutation: nothing is spawned until the
36
36
  * mutation actually runs for the first time. No engine teardown hook is
37
37
  * needed — the OS reaps the workers when the engine's host process exits.
@@ -45,27 +45,32 @@ export type SandboxConfig = {
45
45
  timeout?: number;
46
46
  /**
47
47
  * isolated-jsc backend. Defaults to `'auto'` (FFI when libJSC is
48
- * reachable, Worker otherwise) since isolated-jsc 0.4 added async
49
- * host-fn support on FFI `actions.insert/update/delete/change`
50
- * now settle on both backends.
48
+ * reachable, Worker otherwise). Both backends now run the same
49
+ * `Context.compileCallable`-based hot path; the choice trades cold
50
+ * spawn (FFI wins ~6×) against Web API availability (Worker only).
51
51
  *
52
52
  * Pin to `'worker'` if your handler needs Web APIs (`URL`,
53
53
  * `TextEncoder`, `WebSocket`) — those live in the Bun-Worker
54
54
  * environment, not the bare JSC C API.
55
55
  *
56
- * Pin to `'ffi'` for hot-path read-only handlers (~300 KB cold heap
57
- * vs ~46 MB on Worker, interrupt-driven CPU timeouts).
56
+ * Pin to `'ffi'` to bypass the auto-probe when you know libJSC is
57
+ * reachable (e.g. CI with a known image).
58
58
  */
59
59
  backend?: 'auto' | 'ffi' | 'worker';
60
60
  };
61
61
  /**
62
62
  * Build a lazy runner for one mutation's sandboxed source. The first call
63
- * compiles + spawns; subsequent calls reuse the isolate AND the context
64
- * (the router Reference is installed once on isolate creation; per-call
65
- * cost is just two `setGlobal`s for `args` + `ctx`). Calls are serialised
66
- * via a promise queue so the shared-slot router stays coherent. The
67
- * context is recycled every {@link DEFAULT_RECYCLE_CONTEXT_AFTER} calls
68
- * to bound JSC per-call metadata accumulation. If the isolate has been
69
- * disposed (timeout, memory cap), the next call re-spawns transparently.
63
+ * compiles the isolate + context + dispatch Reference + callable;
64
+ * subsequent calls only generate a fresh callId, register the per-call
65
+ * `actions` in the callMap, and invoke `callable.call([callId, args,
66
+ * ctx])`. Per-call cost on FFI: one JSObjectCallAsFunction + three
67
+ * cheap primitive packings. No per-call Reference allocation, no
68
+ * setGlobal, no eval.
69
+ *
70
+ * Concurrency-safe by construction: each call has its own callId →
71
+ * its own actions slot in the callMap.
72
+ *
73
+ * If the isolate has been disposed (timeout, memory cap), the next
74
+ * call re-spawns transparently.
70
75
  */
71
76
  export declare const makeSandboxedHandler: (source: string, config?: SandboxConfig) => ((args: unknown, ctx: unknown, actions: MutationActions) => Promise<unknown>);
package/dist/index.js CHANGED
@@ -725,7 +725,7 @@ var loadIsolatedJsc = async () => {
725
725
  }
726
726
  };
727
727
  var wrap = (source) => `
728
- (() => {
728
+ function (__callId, args, ctx) {
729
729
  const userFn = (${source});
730
730
  if (typeof userFn !== 'function') {
731
731
  throw new Error(
@@ -734,20 +734,26 @@ var wrap = (source) => `
734
734
  );
735
735
  }
736
736
  const actions = {
737
- insert: (table, data) => __syncAction('insert', table, data),
738
- update: (table, data) => __syncAction('update', table, data),
739
- delete: (table, row) => __syncAction('delete', table, row),
740
- change: (collection, change) => __syncAction('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
- var DEFAULT_RECYCLE_CONTEXT_AFTER = 256;
746
- var installRouter = async (context, currentActions, Reference) => {
747
- const router = new Reference((op, ...rest) => {
748
- const a = currentActions.value;
745
+ var compile = async (source, config) => {
746
+ const { createIsolate, Reference } = await loadIsolatedJsc();
747
+ const isolate = await createIsolate({
748
+ backend: config.backend ?? "auto",
749
+ memoryLimit: config.memoryLimit ?? 32
750
+ });
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);
749
755
  if (a === undefined) {
750
- throw new Error("__syncAction invoked outside an active sandboxed call (shared-slot router)");
756
+ throw new Error(`__dispatch invoked for orphan callId ${String(callId)}`);
751
757
  }
752
758
  switch (op) {
753
759
  case "insert":
@@ -762,27 +768,14 @@ var installRouter = async (context, currentActions, Reference) => {
762
768
  throw new Error(`unknown sandbox action op: ${String(op)}`);
763
769
  }
764
770
  });
765
- await context.setGlobal("__syncAction", router);
766
- };
767
- var compile = async (source, config) => {
768
- const { createIsolate, Reference } = await loadIsolatedJsc();
769
- const isolate = await createIsolate({
770
- backend: config.backend ?? "auto",
771
- memoryLimit: config.memoryLimit ?? 32
772
- });
773
- const script = await isolate.compileScript(wrap(source));
774
- const context = await isolate.createContext();
775
- const currentActions = {
776
- value: undefined
777
- };
778
- await installRouter(context, currentActions, Reference);
771
+ await context.setGlobal("__dispatch", dispatch);
772
+ const callable = await context.compileCallable(wrap(source));
779
773
  return {
774
+ callable,
775
+ callMap,
780
776
  context,
781
- currentActions,
782
777
  isolate,
783
- runQueue: Promise.resolve(undefined),
784
- script,
785
- servedCalls: 0,
778
+ nextCallId: 1,
786
779
  timeoutMs: config.timeout ?? 5000
787
780
  };
788
781
  };
@@ -798,37 +791,17 @@ var makeSandboxedHandler = (source, config = {}) => {
798
791
  pending = compile(source, config);
799
792
  return pending;
800
793
  };
801
- const recycleContextIfNeeded = async (compiled) => {
802
- if (compiled.servedCalls < DEFAULT_RECYCLE_CONTEXT_AFTER)
803
- return;
804
- const { Reference } = await loadIsolatedJsc();
805
- await compiled.context.dispose().catch(() => {});
806
- compiled.context = await compiled.isolate.createContext();
807
- await installRouter(compiled.context, compiled.currentActions, Reference);
808
- compiled.servedCalls = 0;
809
- };
810
794
  return async (args, ctx, actions) => {
811
795
  const compiled = await getCompiled();
812
- const prev = compiled.runQueue;
813
- const turn = prev.then(async () => {
814
- await recycleContextIfNeeded(compiled);
815
- compiled.currentActions.value = actions;
816
- try {
817
- await compiled.context.setGlobal("args", args);
818
- await compiled.context.setGlobal("ctx", ctx);
819
- const result = await compiled.script.run(compiled.context, {
820
- timeout: compiled.timeoutMs
821
- });
822
- return result;
823
- } finally {
824
- compiled.currentActions.value = undefined;
825
- compiled.servedCalls += 1;
826
- }
827
- });
828
- compiled.runQueue = turn.catch(() => {
829
- return;
830
- });
831
- return turn;
796
+ const callId = compiled.nextCallId++;
797
+ compiled.callMap.set(callId, actions);
798
+ try {
799
+ return await compiled.callable.call([callId, args, ctx], {
800
+ timeout: compiled.timeoutMs
801
+ });
802
+ } finally {
803
+ compiled.callMap.delete(callId);
804
+ }
832
805
  };
833
806
  };
834
807
 
@@ -2350,5 +2323,5 @@ export {
2350
2323
  createPresenceHub
2351
2324
  };
2352
2325
 
2353
- //# debugId=5E4321DCD1FB65A764756E2164756E21
2326
+ //# debugId=B9056CA0A3DAE81164756E2164756E21
2354
2327
  //# sourceMappingURL=index.js.map