@absolutejs/sync 1.9.2 → 1.11.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.
@@ -0,0 +1,175 @@
1
+ /**
2
+ * `@absolutejs/sync/code-mode` — wraps engine mutations as Code Mode host
3
+ * tools.
4
+ *
5
+ * The Code Mode pattern (Cloudflare Dynamic Workers / Anthropic
6
+ * programmatic tool calling, both ~2026) replaces "N tool calls per
7
+ * turn" with "1 tool call whose body is JS that chains the underlying
8
+ * fns." Sync's contribution: the engine's mutation surface is the
9
+ * underlying fns. The model writes
10
+ *
11
+ * ```js
12
+ * const c = await runMutation('comments:create', { body: '@bob …', resourceId });
13
+ * await runMutation('comments:toggleReaction', { commentId: c.id, emoji: '👍' });
14
+ * return c.id;
15
+ * ```
16
+ *
17
+ * and three would-be tool turns collapse into one — only the final
18
+ * `return` enters the conversation context.
19
+ *
20
+ * The factory here returns a **host-tool map** that's shape-compatible
21
+ * with `@absolutejs/ai`'s `codeModeTool({ tools })` option. We don't
22
+ * import `@absolutejs/ai` because the consumer is responsible for
23
+ * wiring the two; sync stays decoupled from the AI SDK.
24
+ *
25
+ * ## v0.1 semantics (read carefully)
26
+ *
27
+ * - Each `runMutation` call runs in its own DB transaction (the
28
+ * engine's per-call retry/transaction wrapper). If mutation 3/5
29
+ * fails, mutations 1–2 are already committed; the model receives
30
+ * the error and decides whether to compensate (e.g. by calling a
31
+ * delete mutation).
32
+ * - Cross-mutation atomicity (all-or-nothing across N runMutations) is
33
+ * NOT provided here — it would need a new engine batch primitive.
34
+ * That's a deliberate v0.2 followup; this v0.1 ships the integration
35
+ * without lying about transactional semantics.
36
+ * - The host-side `ctx` factory runs ONCE per `run_mutations` tool
37
+ * call; the same ctx is threaded through every mutation in that
38
+ * script. This matches the "one model turn ≈ one user request"
39
+ * shape, where the user identity doesn't change mid-script.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { engineMutationsAsHostTools } from '@absolutejs/sync/code-mode';
44
+ * import { codeModeTool } from '@absolutejs/ai/tools';
45
+ *
46
+ * const sessionCtx = () => ({ userId: currentSessionUserId() });
47
+ * const hostTools = engineMutationsAsHostTools({
48
+ * ctx: sessionCtx,
49
+ * engine,
50
+ * mutations: [
51
+ * {
52
+ * description: 'Post a comment on a resource.',
53
+ * name: 'comments:create',
54
+ * tsSignature:
55
+ * '(args: { resourceId: string; body: string }) => ' +
56
+ * 'Promise<{ id: string; authorId: string; body: string }>',
57
+ * },
58
+ * {
59
+ * description: 'Toggle a reaction emoji on a comment.',
60
+ * name: 'comments:toggleReaction',
61
+ * tsSignature:
62
+ * '(args: { commentId: string; emoji: string }) => ' +
63
+ * 'Promise<{ added: boolean }>',
64
+ * },
65
+ * ],
66
+ * });
67
+ *
68
+ * const aiTool = codeModeTool({ tools: hostTools });
69
+ * ```
70
+ */
71
+ import type { SyncEngine } from './engine/syncEngine';
72
+ /** One engine mutation surfaced to the model as a host function. */
73
+ export type MutationToolDescriptor = {
74
+ /** Engine-registered mutation name (e.g. `'comments:create'`). */
75
+ name: string;
76
+ /**
77
+ * One-line description shown to the model in the Code Mode prompt.
78
+ * Should describe *what the mutation does*, not just its inputs.
79
+ */
80
+ description: string;
81
+ /**
82
+ * TypeScript signature shown to the model. Default
83
+ * `'(args: any) => Promise<any>'` — provide a real signature so the
84
+ * model knows the input shape.
85
+ */
86
+ tsSignature?: string;
87
+ /**
88
+ * Override the host-fn name surfaced to the model. Default: the
89
+ * mutation `name` with `:` replaced by `_` (engine names are not
90
+ * valid JS identifiers). For `'comments:create'` the model sees
91
+ * `comments_create` by default.
92
+ */
93
+ hostFnName?: string;
94
+ };
95
+ /** Options for {@link engineMutationsAsHostTools}. */
96
+ export type EngineMutationsAsHostToolsOptions<Ctx> = {
97
+ /** The engine whose mutations should be exposed. */
98
+ engine: SyncEngine;
99
+ /**
100
+ * Resolve the per-call ctx — invoked ONCE at the start of each
101
+ * Code Mode `run_mutations` call and threaded through every
102
+ * mutation in that script. Typically returns the current
103
+ * session/user identity. May be sync or async.
104
+ */
105
+ ctx: () => Ctx | Promise<Ctx>;
106
+ /** The mutation set to expose. */
107
+ mutations: MutationToolDescriptor[];
108
+ };
109
+ /** Options for {@link transactionalBatchAsHostTool}. */
110
+ export type TransactionalBatchOptions<Ctx> = {
111
+ /** The engine to call `runMutations` on. */
112
+ engine: SyncEngine;
113
+ /** Resolve per-call ctx — same shape as the other factory. */
114
+ ctx: () => Ctx | Promise<Ctx>;
115
+ /**
116
+ * Allowlist of mutation names the model may include in a batch.
117
+ * The host-fn checks every entry against this set before calling
118
+ * `runMutations`, so a hallucinated name fails fast with a clear
119
+ * error instead of bubbling up from the engine.
120
+ */
121
+ allowedMutations: string[];
122
+ /** Override the model-visible host-fn description. */
123
+ description?: string;
124
+ /** Override the TS signature shown to the model. */
125
+ tsSignature?: string;
126
+ };
127
+ /**
128
+ * Shape of one entry in the host-tool map. Mirrors
129
+ * `@absolutejs/ai/tools`'s `CodeModeHostTool` exactly — no import
130
+ * needed; the AI SDK consumes any record with this shape.
131
+ */
132
+ export type CodeModeHostTool = {
133
+ description: string;
134
+ tsSignature: string;
135
+ handler: (...args: unknown[]) => unknown;
136
+ };
137
+ /** Map of model-visible host-fn name → host tool. */
138
+ export type CodeModeHostToolMap = Record<string, CodeModeHostTool>;
139
+ /**
140
+ * Build a host-tool map that exposes the engine's mutation surface to a
141
+ * Code Mode tool. Pass the result to `codeModeTool({ tools })` from
142
+ * `@absolutejs/ai/tools`.
143
+ *
144
+ * The function throws synchronously at build time if any descriptor
145
+ * names a mutation that isn't registered on the engine, so a typo'd
146
+ * allowlist surfaces at boot, not at the first model call.
147
+ */
148
+ export declare const engineMutationsAsHostTools: <Ctx>(options: EngineMutationsAsHostToolsOptions<Ctx>) => CodeModeHostToolMap;
149
+ /**
150
+ * A single Code Mode host tool wrapping `engine.runMutations(specs, ctx)`
151
+ * (sync 1.11+). Returns the per-mutation results array; rolls every
152
+ * accumulated write back on any thrown error. Plug the returned
153
+ * `CodeModeHostTool` into a `codeModeTool({ tools: { ..., run_transaction:
154
+ * /* this *\/ } })` map under whatever name fits your prompt strategy.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * const hostTools = {
159
+ * ...engineMutationsAsHostTools({ engine, ctx, mutations }),
160
+ * run_transaction: transactionalBatchAsHostTool({
161
+ * engine,
162
+ * ctx,
163
+ * allowedMutations: ['comments:create', 'notifications:notify'],
164
+ * }),
165
+ * };
166
+ * const tool = codeModeTool({ tools: hostTools });
167
+ *
168
+ * // Model can branch on a per-mutation call OR use the atomic batch:
169
+ * // await run_transaction([
170
+ * // { name: 'comments:create', args: { resourceId, body } },
171
+ * // { name: 'notifications:notify', args: { actorId, kind, ... } },
172
+ * // ]);
173
+ * ```
174
+ */
175
+ export declare const transactionalBatchAsHostTool: <Ctx>(options: TransactionalBatchOptions<Ctx>) => CodeModeHostTool;
@@ -0,0 +1,84 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __require = import.meta.require;
17
+
18
+ // src/codeMode.ts
19
+ var DEFAULT_TS_SIGNATURE = "(args: any) => Promise<any>";
20
+ var defaultHostFnName = (mutationName) => mutationName.replace(/[^A-Za-z0-9_]/g, "_");
21
+ var engineMutationsAsHostTools = (options) => {
22
+ const { engine, ctx, mutations } = options;
23
+ const registered = new Set(engine.inspect().mutations);
24
+ const map = {};
25
+ const seenHostFnNames = new Set;
26
+ for (const descriptor of mutations) {
27
+ if (!registered.has(descriptor.name)) {
28
+ throw new Error(`engineMutationsAsHostTools: mutation "${descriptor.name}" is not registered on the engine. ` + `Register it first, or remove it from the mutations list.`);
29
+ }
30
+ const hostFnName = descriptor.hostFnName ?? defaultHostFnName(descriptor.name);
31
+ if (seenHostFnNames.has(hostFnName)) {
32
+ throw new Error(`engineMutationsAsHostTools: duplicate host-fn name "${hostFnName}". ` + `Two mutations map to the same identifier \u2014 set hostFnName explicitly on one of them.`);
33
+ }
34
+ seenHostFnNames.add(hostFnName);
35
+ map[hostFnName] = {
36
+ description: descriptor.description,
37
+ handler: async (...args) => {
38
+ const payload = args[0];
39
+ const resolvedCtx = await ctx();
40
+ return engine.runMutation(descriptor.name, payload, resolvedCtx);
41
+ },
42
+ tsSignature: descriptor.tsSignature ?? DEFAULT_TS_SIGNATURE
43
+ };
44
+ }
45
+ return map;
46
+ };
47
+ var DEFAULT_TRANSACTION_TS_SIGNATURE = "(specs: Array<{ name: string; args: any }>) => Promise<any[]>";
48
+ var DEFAULT_TRANSACTION_DESCRIPTION = "Run an ARRAY of mutations atomically in a single DB transaction. " + "Each entry is `{ name, args }` where `name` is an engine mutation " + "name. If any mutation throws, the entire batch rolls back \u2014 no " + "partial commits. Returns the per-mutation results in order. Use " + "this when you need all-or-nothing semantics; use the individual " + "host functions when you need to branch on intermediate results.";
49
+ var transactionalBatchAsHostTool = (options) => {
50
+ const allowed = new Set(options.allowedMutations);
51
+ return {
52
+ description: options.description ?? DEFAULT_TRANSACTION_DESCRIPTION,
53
+ handler: async (...args) => {
54
+ const specsInput = args[0];
55
+ if (!Array.isArray(specsInput)) {
56
+ throw new Error("transactionalBatchAsHostTool: expected one positional " + "arg \u2014 an array of { name, args }. Got " + typeof specsInput);
57
+ }
58
+ const specs = [];
59
+ for (const entry of specsInput) {
60
+ if (entry === null || typeof entry !== "object" || typeof entry.name !== "string") {
61
+ throw new Error("transactionalBatchAsHostTool: every spec must be " + "`{ name: string; args: any }`.");
62
+ }
63
+ const name = entry.name;
64
+ if (!allowed.has(name)) {
65
+ throw new Error(`transactionalBatchAsHostTool: mutation "${name}" ` + "is not in the allowlist.");
66
+ }
67
+ specs.push({
68
+ args: entry.args,
69
+ name
70
+ });
71
+ }
72
+ const resolvedCtx = await options.ctx();
73
+ return options.engine.runMutations(specs, resolvedCtx);
74
+ },
75
+ tsSignature: options.tsSignature ?? DEFAULT_TRANSACTION_TS_SIGNATURE
76
+ };
77
+ };
78
+ export {
79
+ transactionalBatchAsHostTool,
80
+ engineMutationsAsHostTools
81
+ };
82
+
83
+ //# debugId=7E23BE34D770BCA364756E2164756E21
84
+ //# sourceMappingURL=codeMode.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/codeMode.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * `@absolutejs/sync/code-mode` — wraps engine mutations as Code Mode host\n * tools.\n *\n * The Code Mode pattern (Cloudflare Dynamic Workers / Anthropic\n * programmatic tool calling, both ~2026) replaces \"N tool calls per\n * turn\" with \"1 tool call whose body is JS that chains the underlying\n * fns.\" Sync's contribution: the engine's mutation surface is the\n * underlying fns. The model writes\n *\n * ```js\n * const c = await runMutation('comments:create', { body: '@bob …', resourceId });\n * await runMutation('comments:toggleReaction', { commentId: c.id, emoji: '👍' });\n * return c.id;\n * ```\n *\n * and three would-be tool turns collapse into one — only the final\n * `return` enters the conversation context.\n *\n * The factory here returns a **host-tool map** that's shape-compatible\n * with `@absolutejs/ai`'s `codeModeTool({ tools })` option. We don't\n * import `@absolutejs/ai` because the consumer is responsible for\n * wiring the two; sync stays decoupled from the AI SDK.\n *\n * ## v0.1 semantics (read carefully)\n *\n * - Each `runMutation` call runs in its own DB transaction (the\n * engine's per-call retry/transaction wrapper). If mutation 3/5\n * fails, mutations 1–2 are already committed; the model receives\n * the error and decides whether to compensate (e.g. by calling a\n * delete mutation).\n * - Cross-mutation atomicity (all-or-nothing across N runMutations) is\n * NOT provided here — it would need a new engine batch primitive.\n * That's a deliberate v0.2 followup; this v0.1 ships the integration\n * without lying about transactional semantics.\n * - The host-side `ctx` factory runs ONCE per `run_mutations` tool\n * call; the same ctx is threaded through every mutation in that\n * script. This matches the \"one model turn ≈ one user request\"\n * shape, where the user identity doesn't change mid-script.\n *\n * @example\n * ```ts\n * import { engineMutationsAsHostTools } from '@absolutejs/sync/code-mode';\n * import { codeModeTool } from '@absolutejs/ai/tools';\n *\n * const sessionCtx = () => ({ userId: currentSessionUserId() });\n * const hostTools = engineMutationsAsHostTools({\n * ctx: sessionCtx,\n * engine,\n * mutations: [\n * {\n * description: 'Post a comment on a resource.',\n * name: 'comments:create',\n * tsSignature:\n * '(args: { resourceId: string; body: string }) => ' +\n * 'Promise<{ id: string; authorId: string; body: string }>',\n * },\n * {\n * description: 'Toggle a reaction emoji on a comment.',\n * name: 'comments:toggleReaction',\n * tsSignature:\n * '(args: { commentId: string; emoji: string }) => ' +\n * 'Promise<{ added: boolean }>',\n * },\n * ],\n * });\n *\n * const aiTool = codeModeTool({ tools: hostTools });\n * ```\n */\n\nimport type { SyncEngine } from './engine/syncEngine';\n\n/** One engine mutation surfaced to the model as a host function. */\nexport type MutationToolDescriptor = {\n\t/** Engine-registered mutation name (e.g. `'comments:create'`). */\n\tname: string;\n\t/**\n\t * One-line description shown to the model in the Code Mode prompt.\n\t * Should describe *what the mutation does*, not just its inputs.\n\t */\n\tdescription: string;\n\t/**\n\t * TypeScript signature shown to the model. Default\n\t * `'(args: any) => Promise<any>'` — provide a real signature so the\n\t * model knows the input shape.\n\t */\n\ttsSignature?: string;\n\t/**\n\t * Override the host-fn name surfaced to the model. Default: the\n\t * mutation `name` with `:` replaced by `_` (engine names are not\n\t * valid JS identifiers). For `'comments:create'` the model sees\n\t * `comments_create` by default.\n\t */\n\thostFnName?: string;\n};\n\n/** Options for {@link engineMutationsAsHostTools}. */\nexport type EngineMutationsAsHostToolsOptions<Ctx> = {\n\t/** The engine whose mutations should be exposed. */\n\tengine: SyncEngine;\n\t/**\n\t * Resolve the per-call ctx — invoked ONCE at the start of each\n\t * Code Mode `run_mutations` call and threaded through every\n\t * mutation in that script. Typically returns the current\n\t * session/user identity. May be sync or async.\n\t */\n\tctx: () => Ctx | Promise<Ctx>;\n\t/** The mutation set to expose. */\n\tmutations: MutationToolDescriptor[];\n};\n\n/** Options for {@link transactionalBatchAsHostTool}. */\nexport type TransactionalBatchOptions<Ctx> = {\n\t/** The engine to call `runMutations` on. */\n\tengine: SyncEngine;\n\t/** Resolve per-call ctx — same shape as the other factory. */\n\tctx: () => Ctx | Promise<Ctx>;\n\t/**\n\t * Allowlist of mutation names the model may include in a batch.\n\t * The host-fn checks every entry against this set before calling\n\t * `runMutations`, so a hallucinated name fails fast with a clear\n\t * error instead of bubbling up from the engine.\n\t */\n\tallowedMutations: string[];\n\t/** Override the model-visible host-fn description. */\n\tdescription?: string;\n\t/** Override the TS signature shown to the model. */\n\ttsSignature?: string;\n};\n\n/**\n * Shape of one entry in the host-tool map. Mirrors\n * `@absolutejs/ai/tools`'s `CodeModeHostTool` exactly — no import\n * needed; the AI SDK consumes any record with this shape.\n */\nexport type CodeModeHostTool = {\n\tdescription: string;\n\ttsSignature: string;\n\thandler: (...args: unknown[]) => unknown;\n};\n\n/** Map of model-visible host-fn name → host tool. */\nexport type CodeModeHostToolMap = Record<string, CodeModeHostTool>;\n\nconst DEFAULT_TS_SIGNATURE = '(args: any) => Promise<any>';\n\nconst defaultHostFnName = (mutationName: string): string =>\n\tmutationName.replace(/[^A-Za-z0-9_]/g, '_');\n\n/**\n * Build a host-tool map that exposes the engine's mutation surface to a\n * Code Mode tool. Pass the result to `codeModeTool({ tools })` from\n * `@absolutejs/ai/tools`.\n *\n * The function throws synchronously at build time if any descriptor\n * names a mutation that isn't registered on the engine, so a typo'd\n * allowlist surfaces at boot, not at the first model call.\n */\nexport const engineMutationsAsHostTools = <Ctx>(\n\toptions: EngineMutationsAsHostToolsOptions<Ctx>\n): CodeModeHostToolMap => {\n\tconst { engine, ctx, mutations } = options;\n\tconst registered = new Set(engine.inspect().mutations);\n\tconst map: CodeModeHostToolMap = {};\n\tconst seenHostFnNames = new Set<string>();\n\n\tfor (const descriptor of mutations) {\n\t\tif (!registered.has(descriptor.name)) {\n\t\t\tthrow new Error(\n\t\t\t\t`engineMutationsAsHostTools: mutation \"${descriptor.name}\" is not registered on the engine. ` +\n\t\t\t\t\t`Register it first, or remove it from the mutations list.`\n\t\t\t);\n\t\t}\n\t\tconst hostFnName =\n\t\t\tdescriptor.hostFnName ?? defaultHostFnName(descriptor.name);\n\t\tif (seenHostFnNames.has(hostFnName)) {\n\t\t\tthrow new Error(\n\t\t\t\t`engineMutationsAsHostTools: duplicate host-fn name \"${hostFnName}\". ` +\n\t\t\t\t\t`Two mutations map to the same identifier — set hostFnName explicitly on one of them.`\n\t\t\t);\n\t\t}\n\t\tseenHostFnNames.add(hostFnName);\n\n\t\tmap[hostFnName] = {\n\t\t\tdescription: descriptor.description,\n\t\t\thandler: async (...args: unknown[]): Promise<unknown> => {\n\t\t\t\t// Code Mode passes positional args from the model's JS call.\n\t\t\t\t// Engine mutations take a single `args` value — first\n\t\t\t\t// positional arg is the mutation payload.\n\t\t\t\tconst payload = args[0];\n\t\t\t\tconst resolvedCtx = await ctx();\n\t\t\t\treturn engine.runMutation(\n\t\t\t\t\tdescriptor.name,\n\t\t\t\t\tpayload,\n\t\t\t\t\tresolvedCtx\n\t\t\t\t);\n\t\t\t},\n\t\t\ttsSignature: descriptor.tsSignature ?? DEFAULT_TS_SIGNATURE\n\t\t};\n\t}\n\n\treturn map;\n};\n\nconst DEFAULT_TRANSACTION_TS_SIGNATURE =\n\t'(specs: Array<{ name: string; args: any }>) => Promise<any[]>';\n\nconst DEFAULT_TRANSACTION_DESCRIPTION =\n\t'Run an ARRAY of mutations atomically in a single DB transaction. ' +\n\t'Each entry is `{ name, args }` where `name` is an engine mutation ' +\n\t'name. If any mutation throws, the entire batch rolls back — no ' +\n\t'partial commits. Returns the per-mutation results in order. Use ' +\n\t'this when you need all-or-nothing semantics; use the individual ' +\n\t'host functions when you need to branch on intermediate results.';\n\n/**\n * A single Code Mode host tool wrapping `engine.runMutations(specs, ctx)`\n * (sync 1.11+). Returns the per-mutation results array; rolls every\n * accumulated write back on any thrown error. Plug the returned\n * `CodeModeHostTool` into a `codeModeTool({ tools: { ..., run_transaction:\n * /* this *\\/ } })` map under whatever name fits your prompt strategy.\n *\n * @example\n * ```ts\n * const hostTools = {\n * ...engineMutationsAsHostTools({ engine, ctx, mutations }),\n * run_transaction: transactionalBatchAsHostTool({\n * engine,\n * ctx,\n * allowedMutations: ['comments:create', 'notifications:notify'],\n * }),\n * };\n * const tool = codeModeTool({ tools: hostTools });\n *\n * // Model can branch on a per-mutation call OR use the atomic batch:\n * // await run_transaction([\n * // { name: 'comments:create', args: { resourceId, body } },\n * // { name: 'notifications:notify', args: { actorId, kind, ... } },\n * // ]);\n * ```\n */\nexport const transactionalBatchAsHostTool = <Ctx>(\n\toptions: TransactionalBatchOptions<Ctx>\n): CodeModeHostTool => {\n\tconst allowed = new Set(options.allowedMutations);\n\treturn {\n\t\tdescription: options.description ?? DEFAULT_TRANSACTION_DESCRIPTION,\n\t\thandler: async (...args: unknown[]): Promise<unknown> => {\n\t\t\t// The model writes `await run_transaction([...])` — Code Mode\n\t\t\t// passes that array as the first positional arg.\n\t\t\tconst specsInput = args[0];\n\t\t\tif (!Array.isArray(specsInput)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'transactionalBatchAsHostTool: expected one positional ' +\n\t\t\t\t\t\t'arg — an array of { name, args }. Got ' +\n\t\t\t\t\t\ttypeof specsInput\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst specs: Array<{ name: string; args: unknown }> = [];\n\t\t\tfor (const entry of specsInput as unknown[]) {\n\t\t\t\tif (\n\t\t\t\t\tentry === null ||\n\t\t\t\t\ttypeof entry !== 'object' ||\n\t\t\t\t\ttypeof (entry as { name?: unknown }).name !== 'string'\n\t\t\t\t) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'transactionalBatchAsHostTool: every spec must be ' +\n\t\t\t\t\t\t\t'`{ name: string; args: any }`.'\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst name = (entry as { name: string }).name;\n\t\t\t\tif (!allowed.has(name)) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`transactionalBatchAsHostTool: mutation \"${name}\" ` +\n\t\t\t\t\t\t\t'is not in the allowlist.'\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tspecs.push({\n\t\t\t\t\targs: (entry as { args?: unknown }).args,\n\t\t\t\t\tname\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst resolvedCtx = await options.ctx();\n\t\t\treturn options.engine.runMutations(specs, resolvedCtx);\n\t\t},\n\t\ttsSignature: options.tsSignature ?? DEFAULT_TRANSACTION_TS_SIGNATURE\n\t};\n};\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;;AAiJA,IAAM,uBAAuB;AAE7B,IAAM,oBAAoB,CAAC,iBAC1B,aAAa,QAAQ,kBAAkB,GAAG;AAWpC,IAAM,6BAA6B,CACzC,YACyB;AAAA,EACzB,QAAQ,QAAQ,KAAK,cAAc;AAAA,EACnC,MAAM,aAAa,IAAI,IAAI,OAAO,QAAQ,EAAE,SAAS;AAAA,EACrD,MAAM,MAA2B,CAAC;AAAA,EAClC,MAAM,kBAAkB,IAAI;AAAA,EAE5B,WAAW,cAAc,WAAW;AAAA,IACnC,IAAI,CAAC,WAAW,IAAI,WAAW,IAAI,GAAG;AAAA,MACrC,MAAM,IAAI,MACT,yCAAyC,WAAW,4CACnD,0DACF;AAAA,IACD;AAAA,IACA,MAAM,aACL,WAAW,cAAc,kBAAkB,WAAW,IAAI;AAAA,IAC3D,IAAI,gBAAgB,IAAI,UAAU,GAAG;AAAA,MACpC,MAAM,IAAI,MACT,uDAAuD,kBACtD,2FACF;AAAA,IACD;AAAA,IACA,gBAAgB,IAAI,UAAU;AAAA,IAE9B,IAAI,cAAc;AAAA,MACjB,aAAa,WAAW;AAAA,MACxB,SAAS,UAAU,SAAsC;AAAA,QAIxD,MAAM,UAAU,KAAK;AAAA,QACrB,MAAM,cAAc,MAAM,IAAI;AAAA,QAC9B,OAAO,OAAO,YACb,WAAW,MACX,SACA,WACD;AAAA;AAAA,MAED,aAAa,WAAW,eAAe;AAAA,IACxC;AAAA,EACD;AAAA,EAEA,OAAO;AAAA;AAGR,IAAM,mCACL;AAED,IAAM,kCACL,sEACA,uEACA,yEACA,qEACA,qEACA;AA4BM,IAAM,+BAA+B,CAC3C,YACsB;AAAA,EACtB,MAAM,UAAU,IAAI,IAAI,QAAQ,gBAAgB;AAAA,EAChD,OAAO;AAAA,IACN,aAAa,QAAQ,eAAe;AAAA,IACpC,SAAS,UAAU,SAAsC;AAAA,MAGxD,MAAM,aAAa,KAAK;AAAA,MACxB,IAAI,CAAC,MAAM,QAAQ,UAAU,GAAG;AAAA,QAC/B,MAAM,IAAI,MACT,2DACC,gDACA,OAAO,UACT;AAAA,MACD;AAAA,MACA,MAAM,QAAgD,CAAC;AAAA,MACvD,WAAW,SAAS,YAAyB;AAAA,QAC5C,IACC,UAAU,QACV,OAAO,UAAU,YACjB,OAAQ,MAA6B,SAAS,UAC7C;AAAA,UACD,MAAM,IAAI,MACT,sDACC,gCACF;AAAA,QACD;AAAA,QACA,MAAM,OAAQ,MAA2B;AAAA,QACzC,IAAI,CAAC,QAAQ,IAAI,IAAI,GAAG;AAAA,UACvB,MAAM,IAAI,MACT,2CAA2C,WAC1C,0BACF;AAAA,QACD;AAAA,QACA,MAAM,KAAK;AAAA,UACV,MAAO,MAA6B;AAAA,UACpC;AAAA,QACD,CAAC;AAAA,MACF;AAAA,MACA,MAAM,cAAc,MAAM,QAAQ,IAAI;AAAA,MACtC,OAAO,QAAQ,OAAO,aAAa,OAAO,WAAW;AAAA;AAAA,IAEtD,aAAa,QAAQ,eAAe;AAAA,EACrC;AAAA;",
8
+ "debugId": "7E23BE34D770BCA364756E2164756E21",
9
+ "names": []
10
+ }
@@ -64,6 +64,15 @@ export type EngineActivity = {
64
64
  at: number;
65
65
  name: string;
66
66
  status: 'ok' | 'error';
67
+ } | {
68
+ /** Emitted by `engine.runMutations(...)` — a batch of mutations
69
+ * run in a single transaction. `names` is the list in batch
70
+ * order; on error the entire batch rolls back, so the status
71
+ * applies to the whole list, not any individual mutation. */
72
+ type: 'mutationBatch';
73
+ at: number;
74
+ names: string[];
75
+ status: 'ok' | 'error';
67
76
  } | {
68
77
  /** Emitted between attempts of a retried mutation. `attempt` is the
69
78
  * attempt that just failed (1-indexed); `delayMs` is the wait before
@@ -2428,6 +2428,55 @@ var createSyncEngine = (options = {}) => {
2428
2428
  }
2429
2429
  throw lastError;
2430
2430
  },
2431
+ runMutations: async (specs, ctx) => {
2432
+ if (specs.length === 0)
2433
+ return [];
2434
+ const resolved = specs.map((spec) => {
2435
+ const mutation = mutations.get(spec.name);
2436
+ if (mutation === undefined) {
2437
+ throw new Error(`Unknown mutation "${spec.name}"`);
2438
+ }
2439
+ return { args: spec.args, mutation, name: spec.name };
2440
+ });
2441
+ const runBatch = async (tx) => {
2442
+ const results = [];
2443
+ const accumulated = [];
2444
+ for (const { args, mutation, name } of resolved) {
2445
+ if (mutation.authorize !== undefined) {
2446
+ const allowed = await mutation.authorize(args, ctx);
2447
+ if (!allowed) {
2448
+ throw new UnauthorizedError(`run mutation "${name}"`);
2449
+ }
2450
+ }
2451
+ const sandboxRunner = sandboxRunners.get(name);
2452
+ const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions2) => Promise.resolve(mutation.handler(a, c, actions2));
2453
+ const { actions, buffered } = makeActions(tx, ctx, true);
2454
+ const result = await invokeHandler(args, ctx, actions);
2455
+ results.push(result);
2456
+ accumulated.push(...buffered);
2457
+ }
2458
+ return { accumulated, results };
2459
+ };
2460
+ try {
2461
+ const { accumulated, results } = runInTransaction !== undefined ? await runInTransaction((tx) => runBatch(tx)) : await runBatch(undefined);
2462
+ await applyChangeBatch(accumulated);
2463
+ emitActivity({
2464
+ type: "mutationBatch",
2465
+ at: Date.now(),
2466
+ names: resolved.map((entry) => entry.name),
2467
+ status: "ok"
2468
+ });
2469
+ return results;
2470
+ } catch (error) {
2471
+ emitActivity({
2472
+ type: "mutationBatch",
2473
+ at: Date.now(),
2474
+ names: resolved.map((entry) => entry.name),
2475
+ status: "error"
2476
+ });
2477
+ throw error;
2478
+ }
2479
+ },
2431
2480
  registerSchedule: (schedule) => {
2432
2481
  schedules.set(schedule.name, schedule);
2433
2482
  },
@@ -3091,5 +3140,5 @@ export {
3091
3140
  CdcConsumerSlowError
3092
3141
  };
3093
3142
 
3094
- //# debugId=5962FD669C5BC3D964756E2164756E21
3143
+ //# debugId=C14BF50A13D2477E64756E2164756E21
3095
3144
  //# sourceMappingURL=index.js.map