@absolutejs/sync 1.7.2 → 1.7.4

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,30 @@ 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 + callable; subsequent calls reuse all
64
+ * three and only pack the per-call args (`args`, `ctx`, and a fresh
65
+ * dispatch Reference closed over this call's `actions`). If the isolate
66
+ * has been disposed (timeout, memory cap), the next call re-spawns
67
+ * transparently.
68
+ *
69
+ * Concurrency-safe by construction: every call gets its own fresh
70
+ * dispatch Reference closed over its own `actions`. No shared slot, no
71
+ * promise queue needed. (Reference allocation cost is ~0.003 ms per
72
+ * call — negligible.)
70
73
  */
71
74
  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
- (async () => {
728
+ function (args, ctx, __dispatch) {
729
729
  const userFn = (${source});
730
730
  if (typeof userFn !== 'function') {
731
731
  throw new Error(
@@ -734,55 +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('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)
741
741
  };
742
- return await userFn(args, ctx, actions);
743
- })()
742
+ return userFn(args, ctx, actions);
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;
749
- if (a === undefined) {
750
- throw new Error("__syncAction invoked outside an active sandboxed call (shared-slot router)");
751
- }
752
- switch (op) {
753
- case "insert":
754
- return a.insert(rest[0], rest[1]);
755
- case "update":
756
- return a.update(rest[0], rest[1]);
757
- case "delete":
758
- return a.delete(rest[0], rest[1]);
759
- case "change":
760
- return a.change(rest[0], rest[1]);
761
- default:
762
- throw new Error(`unknown sandbox action op: ${String(op)}`);
763
- }
764
- });
765
- await context.setGlobal("__syncAction", router);
766
- };
767
745
  var compile = async (source, config) => {
768
- const { createIsolate, Reference } = await loadIsolatedJsc();
746
+ const { createIsolate } = await loadIsolatedJsc();
769
747
  const isolate = await createIsolate({
770
748
  backend: config.backend ?? "auto",
771
749
  memoryLimit: config.memoryLimit ?? 32
772
750
  });
773
- const script = await isolate.compileScript(wrap(source));
774
751
  const context = await isolate.createContext();
775
- const currentActions = {
776
- value: undefined
777
- };
778
- await installRouter(context, currentActions, Reference);
752
+ const callable = await context.compileCallable(wrap(source));
779
753
  return {
754
+ callable,
780
755
  context,
781
- currentActions,
782
756
  isolate,
783
- runQueue: Promise.resolve(undefined),
784
- script,
785
- servedCalls: 0,
786
757
  timeoutMs: config.timeout ?? 5000
787
758
  };
788
759
  };
@@ -798,37 +769,26 @@ var makeSandboxedHandler = (source, config = {}) => {
798
769
  pending = compile(source, config);
799
770
  return pending;
800
771
  };
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
772
  return async (args, ctx, actions) => {
773
+ const { Reference } = await loadIsolatedJsc();
811
774
  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;
775
+ const dispatch = new Reference((op, ...rest) => {
776
+ switch (op) {
777
+ case "insert":
778
+ return actions.insert(rest[0], rest[1]);
779
+ case "update":
780
+ return actions.update(rest[0], rest[1]);
781
+ case "delete":
782
+ return actions.delete(rest[0], rest[1]);
783
+ case "change":
784
+ return actions.change(rest[0], rest[1]);
785
+ default:
786
+ throw new Error(`unknown sandbox action op: ${String(op)}`);
826
787
  }
827
788
  });
828
- compiled.runQueue = turn.catch(() => {
829
- return;
789
+ return compiled.callable.call([args, ctx, dispatch], {
790
+ timeout: compiled.timeoutMs
830
791
  });
831
- return turn;
832
792
  };
833
793
  };
834
794
 
@@ -2350,5 +2310,5 @@ export {
2350
2310
  createPresenceHub
2351
2311
  };
2352
2312
 
2353
- //# debugId=3D61A1E35B6A5BE864756E2164756E21
2313
+ //# debugId=E4BD5E29981CFB6664756E2164756E21
2354
2314
  //# sourceMappingURL=index.js.map