@absolutejs/sync 1.10.0 → 1.12.0
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/codeMode.d.ts +45 -0
- package/dist/codeMode.js +33 -1
- package/dist/codeMode.js.map +3 -3
- package/dist/engine/devtools.d.ts +9 -0
- package/dist/engine/index.js +72 -3
- package/dist/engine/index.js.map +4 -4
- package/dist/engine/sandbox.d.ts +46 -0
- package/dist/engine/syncEngine.d.ts +22 -0
- package/dist/index.js +72 -3
- package/dist/index.js.map +4 -4
- package/dist/testing.js +72 -3
- package/dist/testing.js.map +4 -4
- package/package.json +1 -1
package/dist/engine/sandbox.d.ts
CHANGED
|
@@ -171,6 +171,52 @@ export type SandboxConfig = {
|
|
|
171
171
|
* reachable (e.g. CI with a known image).
|
|
172
172
|
*/
|
|
173
173
|
backend?: 'auto' | 'ffi' | 'worker';
|
|
174
|
+
/**
|
|
175
|
+
* **Escape hatch.** Map of host functions the sandboxed handler may
|
|
176
|
+
* call as `unsafeHost.fnName(...args)`. The name is deliberately
|
|
177
|
+
* loud: anyone reading the source must see immediately that a
|
|
178
|
+
* sandboxed mutation is reaching through to non-deterministic host
|
|
179
|
+
* code (third-party API, queue push, email send, anything that
|
|
180
|
+
* touches the outside world).
|
|
181
|
+
*
|
|
182
|
+
* Without this option, the sandbox is hermetic — only `args`,
|
|
183
|
+
* `ctx`, and `actions` are reachable. Declare an entry here, name
|
|
184
|
+
* it visibly (e.g. `chargeStripe`, `sendSlackPing`), and the engine
|
|
185
|
+
* exposes it on the sandbox-side `unsafeHost` Proxy. The fn runs on
|
|
186
|
+
* the host; its return value is structured-cloned back across the
|
|
187
|
+
* isolate boundary; thrown errors propagate into the sandbox as
|
|
188
|
+
* normal JS errors the handler can catch.
|
|
189
|
+
*
|
|
190
|
+
* The deterministic-mutation guarantees stop at the call site —
|
|
191
|
+
* retries WILL re-fire these host fns (they're outside the
|
|
192
|
+
* transaction). Treat them as side effects and either make them
|
|
193
|
+
* idempotent or pair them with explicit compensation in the
|
|
194
|
+
* handler. Convex's actions are the same model; this is the same
|
|
195
|
+
* trade.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* defineMutation({
|
|
200
|
+
* name: 'payments:checkout',
|
|
201
|
+
* sandboxedHandler: `async (args, ctx, actions, unsafeHost) => {
|
|
202
|
+
* const order = await actions.insert('orders', { ...args, status: 'pending' });
|
|
203
|
+
* const receipt = await unsafeHost.chargeStripe({
|
|
204
|
+
* amount: order.amount,
|
|
205
|
+
* token: args.token,
|
|
206
|
+
* });
|
|
207
|
+
* await actions.update('orders', { id: order.id, status: 'paid', receipt });
|
|
208
|
+
* return order;
|
|
209
|
+
* }`,
|
|
210
|
+
* sandbox: {
|
|
211
|
+
* unsafeHost: {
|
|
212
|
+
* chargeStripe: ({ amount, token }) =>
|
|
213
|
+
* stripe.charges.create({ amount, source: token }),
|
|
214
|
+
* },
|
|
215
|
+
* },
|
|
216
|
+
* });
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
unsafeHost?: Record<string, (...args: any[]) => unknown | Promise<unknown>>;
|
|
174
220
|
};
|
|
175
221
|
/**
|
|
176
222
|
* Build a lazy runner for one mutation's sandboxed source. The first call
|
|
@@ -178,6 +178,28 @@ export type SyncEngine = {
|
|
|
178
178
|
* mutation / a handler throw. Drive this from the transport's mutate frame.
|
|
179
179
|
*/
|
|
180
180
|
runMutation: (name: string, args: unknown, ctx: unknown) => Promise<unknown>;
|
|
181
|
+
/**
|
|
182
|
+
* Atomically run N mutations in a single transaction (sync 1.11+).
|
|
183
|
+
* Each `{ name, args }` spec is authorized, then handlers fire in
|
|
184
|
+
* order against shared buffered changes. If any handler throws, the
|
|
185
|
+
* entire transaction rolls back — no partial commits, no fanned-out
|
|
186
|
+
* diffs. On success the accumulated changes apply as ONE live batch
|
|
187
|
+
* and the per-mutation results return in order.
|
|
188
|
+
*
|
|
189
|
+
* No retry policy applies to batches in v0.2; configure per-mutation
|
|
190
|
+
* retries on individual `runMutation` calls when atomicity isn't
|
|
191
|
+
* needed. A failed batch passes the original error through with no
|
|
192
|
+
* wrapping.
|
|
193
|
+
*
|
|
194
|
+
* Requires `transaction` to be set in {@link SyncEngineOptions} for
|
|
195
|
+
* actual DB-level atomicity; without it the batch still buffers
|
|
196
|
+
* changes into one fan-out but the underlying adapter writes
|
|
197
|
+
* piecemeal.
|
|
198
|
+
*/
|
|
199
|
+
runMutations: (specs: Array<{
|
|
200
|
+
name: string;
|
|
201
|
+
args: unknown;
|
|
202
|
+
}>, ctx: unknown) => Promise<unknown[]>;
|
|
181
203
|
/**
|
|
182
204
|
* A point-in-time snapshot of the engine for devtools: registered collections
|
|
183
205
|
* (+ kind, tables, live subscription counts), mutations, schedules, readers,
|
package/dist/index.js
CHANGED
|
@@ -740,7 +740,7 @@ var wrap = (source) => `
|
|
|
740
740
|
const userFn = (${source});
|
|
741
741
|
if (typeof userFn !== 'function') {
|
|
742
742
|
throw new Error(
|
|
743
|
-
'sandboxedHandler must evaluate to (args, ctx, actions) => result; got ' +
|
|
743
|
+
'sandboxedHandler must evaluate to (args, ctx, actions, unsafeHost) => result; got ' +
|
|
744
744
|
typeof userFn
|
|
745
745
|
);
|
|
746
746
|
}
|
|
@@ -752,12 +752,24 @@ var wrap = (source) => `
|
|
|
752
752
|
now: () => __dispatch(__callId, 'now'),
|
|
753
753
|
fetch: (url, init) => __dispatch(__callId, 'fetch', url, init)
|
|
754
754
|
};
|
|
755
|
-
|
|
755
|
+
// Escape hatch \u2014 host fns the mutation explicitly opted in to.
|
|
756
|
+
// The Proxy means every property access is a host call; the
|
|
757
|
+
// engine throws if the property name isn't declared in the
|
|
758
|
+
// mutation's sandbox.unsafeHost map.
|
|
759
|
+
const unsafeHost = new Proxy({}, {
|
|
760
|
+
get: (_target, fnName) => {
|
|
761
|
+
if (typeof fnName !== 'string') return undefined;
|
|
762
|
+
return (...callArgs) =>
|
|
763
|
+
__dispatch(__callId, 'unsafeHost', fnName, callArgs);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
return userFn(args, ctx, actions, unsafeHost);
|
|
756
767
|
}
|
|
757
768
|
`;
|
|
758
769
|
var compile = async (source, config, bridgeFetch) => {
|
|
759
770
|
const { Reference, createIsolatedRunner, resolveIsolatePolicy } = await loadIsolatedJsc();
|
|
760
771
|
const callMap = new Map;
|
|
772
|
+
const unsafeHost = config.unsafeHost;
|
|
761
773
|
const dispatch = new Reference((callId, op, ...rest) => {
|
|
762
774
|
const a = callMap.get(callId);
|
|
763
775
|
if (a === undefined) {
|
|
@@ -776,6 +788,14 @@ var compile = async (source, config, bridgeFetch) => {
|
|
|
776
788
|
return a.now();
|
|
777
789
|
case "fetch":
|
|
778
790
|
return runBridgeFetch(bridgeFetch, rest[0], rest[1]);
|
|
791
|
+
case "unsafeHost": {
|
|
792
|
+
const fnName = rest[0];
|
|
793
|
+
const callArgs = rest[1] ?? [];
|
|
794
|
+
if (unsafeHost === undefined || typeof unsafeHost[fnName] !== "function") {
|
|
795
|
+
throw new Error(`sandboxedHandler called unsafeHost.${fnName}() but it was not declared in the mutation's sandbox.unsafeHost config. Declare it (and only the host fns you intend to expose) to opt in to the escape hatch.`);
|
|
796
|
+
}
|
|
797
|
+
return unsafeHost[fnName](...callArgs);
|
|
798
|
+
}
|
|
779
799
|
default:
|
|
780
800
|
throw new Error(`unknown sandbox action op: ${String(op)}`);
|
|
781
801
|
}
|
|
@@ -2065,6 +2085,55 @@ var createSyncEngine = (options = {}) => {
|
|
|
2065
2085
|
}
|
|
2066
2086
|
throw lastError;
|
|
2067
2087
|
},
|
|
2088
|
+
runMutations: async (specs, ctx) => {
|
|
2089
|
+
if (specs.length === 0)
|
|
2090
|
+
return [];
|
|
2091
|
+
const resolved = specs.map((spec) => {
|
|
2092
|
+
const mutation = mutations.get(spec.name);
|
|
2093
|
+
if (mutation === undefined) {
|
|
2094
|
+
throw new Error(`Unknown mutation "${spec.name}"`);
|
|
2095
|
+
}
|
|
2096
|
+
return { args: spec.args, mutation, name: spec.name };
|
|
2097
|
+
});
|
|
2098
|
+
const runBatch = async (tx) => {
|
|
2099
|
+
const results = [];
|
|
2100
|
+
const accumulated = [];
|
|
2101
|
+
for (const { args, mutation, name } of resolved) {
|
|
2102
|
+
if (mutation.authorize !== undefined) {
|
|
2103
|
+
const allowed = await mutation.authorize(args, ctx);
|
|
2104
|
+
if (!allowed) {
|
|
2105
|
+
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
const sandboxRunner = sandboxRunners.get(name);
|
|
2109
|
+
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions2) => Promise.resolve(mutation.handler(a, c, actions2));
|
|
2110
|
+
const { actions, buffered } = makeActions(tx, ctx, true);
|
|
2111
|
+
const result = await invokeHandler(args, ctx, actions);
|
|
2112
|
+
results.push(result);
|
|
2113
|
+
accumulated.push(...buffered);
|
|
2114
|
+
}
|
|
2115
|
+
return { accumulated, results };
|
|
2116
|
+
};
|
|
2117
|
+
try {
|
|
2118
|
+
const { accumulated, results } = runInTransaction !== undefined ? await runInTransaction((tx) => runBatch(tx)) : await runBatch(undefined);
|
|
2119
|
+
await applyChangeBatch(accumulated);
|
|
2120
|
+
emitActivity({
|
|
2121
|
+
type: "mutationBatch",
|
|
2122
|
+
at: Date.now(),
|
|
2123
|
+
names: resolved.map((entry) => entry.name),
|
|
2124
|
+
status: "ok"
|
|
2125
|
+
});
|
|
2126
|
+
return results;
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
emitActivity({
|
|
2129
|
+
type: "mutationBatch",
|
|
2130
|
+
at: Date.now(),
|
|
2131
|
+
names: resolved.map((entry) => entry.name),
|
|
2132
|
+
status: "error"
|
|
2133
|
+
});
|
|
2134
|
+
throw error;
|
|
2135
|
+
}
|
|
2136
|
+
},
|
|
2068
2137
|
registerSchedule: (schedule) => {
|
|
2069
2138
|
schedules.set(schedule.name, schedule);
|
|
2070
2139
|
},
|
|
@@ -2626,5 +2695,5 @@ export {
|
|
|
2626
2695
|
createPresenceHub
|
|
2627
2696
|
};
|
|
2628
2697
|
|
|
2629
|
-
//# debugId=
|
|
2698
|
+
//# debugId=DA5074BFF8CB679464756E2164756E21
|
|
2630
2699
|
//# sourceMappingURL=index.js.map
|