@gonvex/local-runtime 0.5.2-staging.12 → 0.5.2-staging.14

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,32 @@
1
+ import { type LocalExecutor, type LocalWorkerEndpoint } from './worker-client.js';
2
+ /** The native view is only an execution sandbox. Durable state stays in the SDK replica/outbox. */
3
+ export declare class NativeReducerBridge implements LocalWorkerEndpoint {
4
+ readonly id: number;
5
+ private readonly onClose;
6
+ private listeners;
7
+ private sender?;
8
+ private stopped;
9
+ readonly executor: LocalExecutor;
10
+ constructor(id: number, onClose: () => void);
11
+ addEventListener(type: string, listener: (event: any) => void): void;
12
+ private emit;
13
+ attach(sender: (message: unknown) => void): void;
14
+ receive(encoded: string): void;
15
+ fail(message: string): void;
16
+ postMessage(message: unknown): void;
17
+ terminate(): void;
18
+ }
19
+ /** Generated bindings create this registry; generated UI mounts one sandbox per client. */
20
+ export declare function nativeReducerHosts(): {
21
+ snapshot: () => readonly NativeReducerBridge[];
22
+ subscribe(listener: () => void): () => void;
23
+ create(): LocalExecutor;
24
+ };
25
+ /** Serialize the bridge envelope as data for WebView's script injection transport. */
26
+ export declare function nativeMessageScript(message: unknown): string;
27
+ /** Called by generated Expo bindings before constructing the client. */
28
+ export declare function installNativeRuntimeGlobals(random: {
29
+ getRandomValues(array: Uint8Array): Uint8Array;
30
+ randomUUID(): string;
31
+ digest?(data: BufferSource): Promise<ArrayBuffer>;
32
+ }): void;
@@ -0,0 +1,93 @@
1
+ import clone from '@ungap/structured-clone';
2
+ import { createLocalReducerWorker } from './worker-client.js';
3
+ /** The native view is only an execution sandbox. Durable state stays in the SDK replica/outbox. */
4
+ export class NativeReducerBridge {
5
+ id;
6
+ onClose;
7
+ listeners = new Map();
8
+ sender;
9
+ stopped = false;
10
+ executor;
11
+ constructor(id, onClose) {
12
+ this.id = id;
13
+ this.onClose = onClose;
14
+ this.executor = createLocalReducerWorker(this);
15
+ }
16
+ addEventListener(type, listener) {
17
+ this.listeners.set(type, [...this.listeners.get(type) ?? [], listener]);
18
+ }
19
+ emit(type, event) {
20
+ for (const listener of this.listeners.get(type) ?? [])
21
+ listener(event);
22
+ }
23
+ attach(sender) { this.sender = sender; }
24
+ receive(encoded) {
25
+ if (this.stopped)
26
+ return;
27
+ try {
28
+ const message = JSON.parse(encoded);
29
+ if (Array.isArray(message.result?.rejected)) {
30
+ for (const rejected of message.result.rejected) {
31
+ if (rejected.error)
32
+ rejected.error = Object.assign(new Error(rejected.error.message), { name: rejected.error.name });
33
+ }
34
+ }
35
+ this.emit('message', { data: message });
36
+ }
37
+ catch {
38
+ this.emit('messageerror', {});
39
+ }
40
+ }
41
+ fail(message) { this.emit('error', { message }); }
42
+ postMessage(message) {
43
+ if (this.stopped || !this.sender)
44
+ throw new Error('Native Reducer host is unavailable');
45
+ this.sender(message);
46
+ }
47
+ terminate() {
48
+ if (this.stopped)
49
+ return;
50
+ this.stopped = true;
51
+ this.sender = undefined;
52
+ this.onClose();
53
+ }
54
+ }
55
+ /** Generated bindings create this registry; generated UI mounts one sandbox per client. */
56
+ export function nativeReducerHosts() {
57
+ let nextId = 0;
58
+ let hosts = [];
59
+ const listeners = new Set();
60
+ const emit = () => { for (const listener of listeners)
61
+ listener(); };
62
+ return {
63
+ snapshot: () => hosts,
64
+ subscribe(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; },
65
+ create() {
66
+ const id = ++nextId;
67
+ const host = new NativeReducerBridge(id, () => { hosts = hosts.filter(entry => entry.id !== id); emit(); });
68
+ hosts = [...hosts, host];
69
+ emit();
70
+ return host.executor;
71
+ },
72
+ };
73
+ }
74
+ /** Serialize the bridge envelope as data for WebView's script injection transport. */
75
+ export function nativeMessageScript(message) {
76
+ const encoded = JSON.stringify(JSON.stringify(message)).replaceAll('\u2028', '\\u2028').replaceAll('\u2029', '\\u2029');
77
+ return `window.dispatchEvent(new MessageEvent('message',{data:JSON.parse(${encoded})}));true;`;
78
+ }
79
+ /** Called by generated Expo bindings before constructing the client. */
80
+ export function installNativeRuntimeGlobals(random) {
81
+ if (typeof globalThis.structuredClone !== 'function')
82
+ Object.defineProperty(globalThis, 'structuredClone', { value: (value) => clone(value, { lossy: false }), configurable: true });
83
+ if (typeof globalThis.crypto?.getRandomValues !== 'function') {
84
+ Object.defineProperty(globalThis, 'crypto', { value: { getRandomValues: random.getRandomValues, randomUUID: random.randomUUID }, configurable: true });
85
+ }
86
+ if (!globalThis.crypto.subtle && random.digest)
87
+ Object.defineProperty(globalThis.crypto, 'subtle', { value: { digest: (algorithm, data) => {
88
+ if (algorithm !== 'SHA-256')
89
+ throw new Error('Unsupported native digest');
90
+ return random.digest(data);
91
+ } }, configurable: true });
92
+ }
93
+ //# sourceMappingURL=native-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-bridge.js","sourceRoot":"","sources":["../src/native-bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { LocalExecutor } from "./worker-client.js";
2
+ import { type PortableReducerOptions } from "./portable.js";
3
+ /** Native JS and browsers use the same executor; no SQL engine or WebView. */
4
+ export declare function createPortableReducer(options: PortableReducerOptions): LocalExecutor;
@@ -0,0 +1,71 @@
1
+ import { PortableReducerRuntime, } from "./portable.js";
2
+ import { memoryReadView, overlayReadView } from "./read-view.js";
3
+ /** Native JS and browsers use the same executor; no SQL engine or WebView. */
4
+ export function createPortableReducer(options) {
5
+ const runtime = new PortableReducerRuntime(options);
6
+ let closed = false;
7
+ const execute = (...args) => {
8
+ if (closed)
9
+ return Promise.reject(new Error("Local reducer executor is closed"));
10
+ return runtime.execute(...args);
11
+ };
12
+ const coverage = (snapshot) => Object.fromEntries(Object.entries(snapshot.tables).map(([table, data]) => [
13
+ table,
14
+ {
15
+ key: options.schema[table]?.key ?? "_id",
16
+ complete: data.complete,
17
+ columns: data.columns,
18
+ },
19
+ ]));
20
+ const view = (snapshot) => memoryReadView(new Map(Object.entries(snapshot.tables).map(([table, data]) => [
21
+ table,
22
+ new Map(data.rows.map((row) => [
23
+ String(row[options.schema[table]?.key ?? "_id"]),
24
+ row,
25
+ ])),
26
+ ])), coverage(snapshot));
27
+ return {
28
+ ready: Promise.resolve(),
29
+ prepare: path => closed ? Promise.reject(new Error("Local reducer executor is closed")) : runtime.prepare(path),
30
+ executeRead: execute,
31
+ execute: (path, args, snapshot, execution) => execute(path, args, view(snapshot), execution),
32
+ replay: async (snapshot, intents) => {
33
+ let source = view(snapshot);
34
+ const transactions = [];
35
+ const rejected = [];
36
+ const ids = new Set();
37
+ for (const intent of intents) {
38
+ if (intent.execution.scope !== snapshot.scope)
39
+ throw new Error("Local reducer scope mismatch");
40
+ if (intent.execution.artifactHash !== options.artifactHash)
41
+ throw new Error("Local reducer artifact mismatch");
42
+ if (ids.has(intent.execution.commandId))
43
+ throw new Error("Duplicate local command id");
44
+ ids.add(intent.execution.commandId);
45
+ }
46
+ for (const intent of intents) {
47
+ try {
48
+ const transaction = await execute(intent.path, intent.args, source, intent.execution);
49
+ transactions.push({
50
+ commandId: intent.execution.commandId,
51
+ transaction,
52
+ });
53
+ source = overlayReadView(source, coverage(snapshot), transaction.patches);
54
+ }
55
+ catch (error) {
56
+ if (error instanceof Error && error.name === "IncompleteReplicaError")
57
+ throw error;
58
+ rejected.push({
59
+ commandId: intent.execution.commandId,
60
+ error: error instanceof Error ? error : new Error(String(error)),
61
+ });
62
+ }
63
+ }
64
+ return { transactions, rejected };
65
+ },
66
+ close: () => {
67
+ closed = true;
68
+ },
69
+ };
70
+ }
71
+ //# sourceMappingURL=portable-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portable-client.js","sourceRoot":"","sources":["../src/portable-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import type { DataRead, JsonObject, JsonValue, ReducerDefinition } from "@gonvex/module-sdk";
2
+ import type { LocalExecution, LocalTransactionResult } from "./index.js";
3
+ import type { LocalSchema } from "./schema.js";
4
+ /** A transaction-consistent SDK read view. Implementations may use RAM or disk.
5
+ * Rows are detached values; complete describes this read, not the whole database.
6
+ */
7
+ export type ReducerReadView = {
8
+ readonly keepAliveFor?: <T>(promise: Promise<T>) => Promise<T>;
9
+ select(read: DataRead,
10
+ /** Base rows fully replaced or removed by the SDK's pending journal. */
11
+ excludedRowIds?: readonly string[]): Promise<{
12
+ rows: readonly JsonObject[];
13
+ complete: boolean;
14
+ }>;
15
+ };
16
+ export declare class MissingReducerDataError extends Error {
17
+ readonly read: DataRead;
18
+ constructor(read: DataRead);
19
+ }
20
+ export declare class UnsupportedLocalOperationError extends Error {
21
+ constructor(message: string);
22
+ }
23
+ export type PortableReducerOptions = {
24
+ schema: LocalSchema;
25
+ reducers: Readonly<Record<string, ReducerDefinition<any, any> | (() => Promise<ReducerDefinition<any, any>>)>>;
26
+ artifactHash: string;
27
+ };
28
+ export declare function orderDataRows(rows: JsonObject[], read: DataRead): void;
29
+ /** Shared reducer execution with no SQL engine and no collection snapshot copies. */
30
+ export declare class PortableReducerRuntime {
31
+ private readonly options;
32
+ private readonly definitions;
33
+ constructor(options: PortableReducerOptions);
34
+ private definition;
35
+ prepare(path: string): Promise<void>;
36
+ execute(path: string, args: JsonValue, source: ReducerReadView, execution: LocalExecution): Promise<LocalTransactionResult>;
37
+ }
@@ -0,0 +1,389 @@
1
+ import { executionReadColumns } from './read-view.js';
2
+ import { compileDataRead, orderedDataRead, matchesDataPredicate, reducerRowId, } from "@gonvex/module-sdk";
3
+ import { validateValue } from "./validation.js";
4
+ export class MissingReducerDataError extends Error {
5
+ read;
6
+ constructor(read) {
7
+ super(`Local replica for ${read.table} is incomplete; this reducer cannot decide from missing rows.`);
8
+ this.read = read;
9
+ this.name = "IncompleteReplicaError";
10
+ }
11
+ }
12
+ export class UnsupportedLocalOperationError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "UnsupportedLocalOperationError";
16
+ }
17
+ }
18
+ const copy = (value) => structuredClone(value);
19
+ function defaultValue(column, now) {
20
+ const expression = column.default?.trim();
21
+ if (!expression || /^NULL(?:::.*)?$/i.test(expression))
22
+ return null;
23
+ if (column.type === "timestamp with time zone" &&
24
+ /^(?:clock_timestamp|now)\(\)$/i.test(expression))
25
+ return new Date(now).toISOString();
26
+ if (/^(true|false)$/i.test(expression))
27
+ return expression.toLowerCase() === "true";
28
+ if (/^-?\d+(?:\.\d+)?$/.test(expression))
29
+ return Number(expression);
30
+ const literal = /^'((?:[^']|'')*)'(?:::(?:[a-z ]+)(?:\[\])?)?$/i.exec(expression);
31
+ if (literal) {
32
+ const text = literal[1].replaceAll("''", "'");
33
+ if (column.type === "jsonb" || column.type === "json")
34
+ return JSON.parse(text);
35
+ if (column.type === "boolean")
36
+ return text === "true";
37
+ if ([
38
+ "integer",
39
+ "bigint",
40
+ "smallint",
41
+ "numeric",
42
+ "double precision",
43
+ "real",
44
+ ].includes(column.type))
45
+ return Number(text);
46
+ return text;
47
+ }
48
+ throw new UnsupportedLocalOperationError(`Default ${expression} must be supplied explicitly by the shared reducer`);
49
+ }
50
+ function columnValue(table, name, column, value) {
51
+ if (value == null) {
52
+ if (!column.nullable)
53
+ throw new Error(`Required field ${table}.${name} is missing`);
54
+ return null;
55
+ }
56
+ const type = column.type;
57
+ if (["integer", "bigint", "smallint"].includes(type)) {
58
+ const number = typeof value === "string" && /^[-+]?\d+$/.test(value.trim())
59
+ ? Number(value)
60
+ : value;
61
+ if (typeof number !== "number" || !Number.isSafeInteger(number))
62
+ throw new Error(`Invalid integer for ${table}.${name}`);
63
+ if (type === "integer" && (number < -2147483648 || number > 2147483647))
64
+ throw new Error(`Integer out of range for ${table}.${name}`);
65
+ if (type === "smallint" && (number < -32768 || number > 32767))
66
+ throw new Error(`Integer out of range for ${table}.${name}`);
67
+ return number;
68
+ }
69
+ if (["double precision", "real", "numeric"].includes(type)) {
70
+ const number = typeof value === "string" && value.trim() ? Number(value) : value;
71
+ if (typeof number !== "number" || !Number.isFinite(number))
72
+ throw new Error(`Invalid number for ${table}.${name}`);
73
+ return number;
74
+ }
75
+ if (type === "boolean") {
76
+ if (typeof value !== "boolean")
77
+ throw new Error(`Invalid boolean for ${table}.${name}`);
78
+ return value;
79
+ }
80
+ if (type === "uuid") {
81
+ if (typeof value !== "string" ||
82
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value))
83
+ throw new Error(`Invalid UUID for ${table}.${name}`);
84
+ return value.toLowerCase();
85
+ }
86
+ if (type === "text" || type.startsWith("character varying")) {
87
+ if (typeof value !== "string")
88
+ throw new Error(`Invalid text for ${table}.${name}`);
89
+ return value;
90
+ }
91
+ if (type === "jsonb" || type === "json")
92
+ return copy(value);
93
+ if (type === "timestamp with time zone") {
94
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value)))
95
+ throw new Error(`Invalid timestamp for ${table}.${name}`);
96
+ return new Date(value).toISOString();
97
+ }
98
+ throw new UnsupportedLocalOperationError(`Column type ${type} requires a portable adapter`);
99
+ }
100
+ export function orderDataRows(rows, read) {
101
+ if (!read.orderBy?.length)
102
+ return;
103
+ rows.sort((a, b) => {
104
+ for (const order of read.orderBy) {
105
+ const sortValue = (row) => {
106
+ const value = row[order.column];
107
+ if (order.transform !== "numericSuffix" || value == null)
108
+ return value;
109
+ if (typeof value !== "string")
110
+ throw new Error("Numeric suffix requires text");
111
+ const suffix = value.match(/[0-9]+$/)?.[0];
112
+ return suffix === undefined ? null : BigInt(suffix);
113
+ };
114
+ const left = sortValue(a), right = sortValue(b);
115
+ if (left === right)
116
+ continue;
117
+ const direction = order.direction === "desc" ? -1 : 1;
118
+ const nullFirst = (order.nulls ?? (direction === 1 ? "last" : "first")) === "first";
119
+ if (left == null)
120
+ return nullFirst ? -1 : 1;
121
+ if (right == null)
122
+ return nullFirst ? 1 : -1;
123
+ if (typeof left !== typeof right || typeof left === "object")
124
+ throw new Error(`Incompatible ordering for ${order.column}`);
125
+ return (left < right ? -1 : 1) * direction;
126
+ }
127
+ return 0;
128
+ });
129
+ }
130
+ /** Shared reducer execution with no SQL engine and no collection snapshot copies. */
131
+ export class PortableReducerRuntime {
132
+ options;
133
+ definitions = new Map();
134
+ constructor(options) {
135
+ this.options = options;
136
+ }
137
+ definition(path) {
138
+ const entry = this.options.reducers[path];
139
+ if (!entry)
140
+ return Promise.reject(new Error(`No public local reducer registered for ${path}`));
141
+ if (typeof entry !== 'function')
142
+ return Promise.resolve(entry);
143
+ let pending = this.definitions.get(path);
144
+ if (!pending) {
145
+ pending = Promise.resolve().then(entry).catch(error => { this.definitions.delete(path); throw error; });
146
+ this.definitions.set(path, pending);
147
+ }
148
+ return pending;
149
+ }
150
+ async prepare(path) { await this.definition(path); }
151
+ async execute(path, args, source, execution) {
152
+ if (execution.artifactHash !== this.options.artifactHash)
153
+ throw new Error("Local reducer artifact mismatch");
154
+ const definition = await this.definition(path);
155
+ if (!definition ||
156
+ definition.kind !== "reducer" ||
157
+ definition.internal ||
158
+ !definition.handler)
159
+ throw new Error(`No public local reducer registered for ${path}`);
160
+ validateValue(definition.options.args, args);
161
+ const changes = new Map();
162
+ const patches = [];
163
+ const deferred = [];
164
+ const reads = new Set();
165
+ const primaryReads = new Map();
166
+ const tableSchema = (table) => {
167
+ const schema = this.options.schema[table];
168
+ if (!schema)
169
+ throw new Error(`Unknown local table ${table}`);
170
+ return schema;
171
+ };
172
+ const changed = (table) => {
173
+ let rows = changes.get(table);
174
+ if (!rows) {
175
+ rows = new Map();
176
+ changes.set(table, rows);
177
+ }
178
+ return rows;
179
+ };
180
+ const select = async (read) => {
181
+ read = orderedDataRead({
182
+ ...read,
183
+ key: read.key ?? tableSchema(read.table).key,
184
+ });
185
+ compileDataRead(read);
186
+ const schema = tableSchema(read.table);
187
+ const columns = new Set(read.columns ?? []);
188
+ const collect = (predicate) => {
189
+ if ("and" in predicate)
190
+ predicate.and.forEach(collect);
191
+ else if ("or" in predicate)
192
+ predicate.or.forEach(collect);
193
+ else
194
+ columns.add(predicate.column);
195
+ };
196
+ if (read.where)
197
+ collect(read.where);
198
+ for (const order of read.orderBy ?? [])
199
+ columns.add(order.column);
200
+ for (const column of columns)
201
+ if (!schema.columns[column])
202
+ throw new Error(`Unknown column ${read.table}.${column}`);
203
+ reads.add(read.table);
204
+ if (read.limit === 0)
205
+ return [];
206
+ const staged = changes.get(read.table);
207
+ // Fetch enough ordered candidates to replace rows removed by this transaction.
208
+ const result = await source.select({
209
+ ...read,
210
+ columns: executionReadColumns(read, schema.key),
211
+ limit: read.limit === undefined
212
+ ? undefined
213
+ : read.limit + (staged?.size ?? 0),
214
+ });
215
+ if (!result.complete)
216
+ throw new MissingReducerDataError(read);
217
+ const rows = new Map(result.rows.map((row) => [String(row[schema.key]), row]));
218
+ for (const [id, row] of staged ?? []) {
219
+ rows.delete(id);
220
+ if (row && (!read.where || matchesDataPredicate(row, read.where)))
221
+ rows.set(id, row);
222
+ }
223
+ const ordered = [...rows.values()];
224
+ orderDataRows(ordered, read);
225
+ const window = read.limit === undefined ? ordered : ordered.slice(0, read.limit);
226
+ return window.map((row) => copy(read.columns
227
+ ? Object.fromEntries(read.columns.map((column) => [column, row[column] ?? null]))
228
+ : row));
229
+ };
230
+ const get = async (table, id) => {
231
+ const schema = tableSchema(table);
232
+ const staged = changes.get(table);
233
+ if (staged?.has(id))
234
+ return copy(staged.get(id) ?? undefined);
235
+ let tableReads = primaryReads.get(table);
236
+ if (!tableReads) {
237
+ tableReads = new Map();
238
+ primaryReads.set(table, tableReads);
239
+ }
240
+ let pending = tableReads.get(id);
241
+ if (!pending) {
242
+ pending = select({
243
+ table,
244
+ key: schema.key,
245
+ where: { column: schema.key, op: "eq", value: id },
246
+ limit: 1,
247
+ }).then((rows) => rows[0]);
248
+ tableReads.set(id, pending);
249
+ }
250
+ return copy(await pending);
251
+ };
252
+ const context = {
253
+ ...copy(execution.identity),
254
+ now: execution.now,
255
+ intentEntropy: execution.intentEntropy,
256
+ invocation: {
257
+ channel: "ui",
258
+ rootChannel: "ui",
259
+ commandId: execution.commandId,
260
+ rootCommandId: execution.commandId,
261
+ artifactHash: execution.artifactHash,
262
+ actorAccountId: execution.identity.auth.account?.id ?? null,
263
+ actorMemberId: execution.identity.member?.id ?? null,
264
+ onBehalfOfMemberId: null,
265
+ },
266
+ db: {
267
+ keepAliveFor: source.keepAliveFor,
268
+ tables: Object.keys(this.options.schema),
269
+ select,
270
+ lock: async () => {
271
+ /* The SDK serializes local intent execution. */
272
+ },
273
+ atomic: async (run) => {
274
+ const prior = new Map([...changes].map(([table, rows]) => [table, new Map(rows)]));
275
+ const length = patches.length;
276
+ try {
277
+ return await run();
278
+ }
279
+ catch (error) {
280
+ changes.clear();
281
+ for (const [table, rows] of prior)
282
+ changes.set(table, rows);
283
+ patches.length = length;
284
+ throw error;
285
+ }
286
+ },
287
+ query: async () => {
288
+ throw new UnsupportedLocalOperationError("Raw SQL is not available in portable reducers; use structured database reads");
289
+ },
290
+ insert: async (table, input, allocation) => {
291
+ const schema = tableSchema(table);
292
+ const row = copy(input);
293
+ const allocated = allocation?.generatedId !== undefined &&
294
+ (row[schema.key] == null ||
295
+ row[schema.key] === allocation.generatedId);
296
+ if (row[schema.key] == null)
297
+ row[schema.key] =
298
+ allocation?.generatedId ??
299
+ (await reducerRowId(context, table, 0));
300
+ if (schema.columns._creationTime && row._creationTime == null)
301
+ row._creationTime = execution.now;
302
+ for (const name of Object.keys(row))
303
+ if (!schema.columns[name])
304
+ throw new Error(`Unknown column ${table}.${name}`);
305
+ for (const [name, column] of Object.entries(schema.columns))
306
+ row[name] = columnValue(table, name, column, row[name] === undefined
307
+ ? defaultValue(column, execution.now)
308
+ : row[name]);
309
+ const id = String(row[schema.key]);
310
+ // An intent-owned ID is new by construction. Requiring a complete
311
+ // history/log collection to prove its absence defeats bounded offline
312
+ // storage. Authoritative replay still enforces uniqueness.
313
+ if (allocated ? changed(table).has(id) : await get(table, id))
314
+ throw new Error(`Row ${table}/${id} already exists`);
315
+ changed(table).set(id, row);
316
+ patches.push({
317
+ entity: table,
318
+ rowId: id,
319
+ op: "insert",
320
+ fields: copy(row),
321
+ });
322
+ return copy(row);
323
+ },
324
+ update: async (table, id, input) => {
325
+ const schema = tableSchema(table);
326
+ if (Object.hasOwn(input, schema.key))
327
+ throw new Error("Reducer update cannot change a row id");
328
+ const before = await get(table, id);
329
+ if (!before)
330
+ throw new Error(`Row ${table}/${id} not found`);
331
+ const fields = {};
332
+ for (const [name, value] of Object.entries(input)) {
333
+ if (value === undefined)
334
+ continue;
335
+ const column = schema.columns[name];
336
+ if (!column)
337
+ throw new Error(`Unknown column ${table}.${name}`);
338
+ fields[name] = columnValue(table, name, column, value);
339
+ }
340
+ const row = { ...before, ...fields };
341
+ if (Object.keys(fields).length) {
342
+ changed(table).set(id, row);
343
+ patches.push({ entity: table, rowId: id, op: "patch", fields });
344
+ }
345
+ return copy(row);
346
+ },
347
+ delete: async (table, id) => {
348
+ if ((await select({ table, columns: [tableSchema(table).key], where: { column: tableSchema(table).key, op: "eq", value: id }, limit: 1 })).length) {
349
+ changed(table).set(id, null);
350
+ patches.push({ entity: table, rowId: id, op: "delete" });
351
+ }
352
+ },
353
+ deleteMany: async (table, ids) => {
354
+ let deleted = 0;
355
+ for (const id of new Set(ids)) {
356
+ if (typeof id !== "string")
357
+ throw new Error("Delete requires a string id");
358
+ if ((await select({ table, columns: [tableSchema(table).key], where: { column: tableSchema(table).key, op: "eq", value: id }, limit: 1 })).length) {
359
+ await context.db.delete(table, id);
360
+ deleted++;
361
+ }
362
+ }
363
+ return { deleted };
364
+ },
365
+ },
366
+ actions: {
367
+ enqueue: async (path, args) => {
368
+ const id = await reducerRowId(context, "deferred:action", deferred.filter((d) => d.kind === "action").length);
369
+ deferred.push({ id, kind: "action", path, args: copy(args) });
370
+ return id;
371
+ },
372
+ },
373
+ scheduler: {
374
+ runAfter: async (delay, path, args = {}) => context.scheduler.runAt(execution.now + delay, path, args),
375
+ runAt: async (at, path, args = {}) => {
376
+ if (!Number.isSafeInteger(at))
377
+ throw new Error("Invalid schedule time");
378
+ const id = `job_${await reducerRowId(context, "deferred:schedule", deferred.filter((d) => d.kind === "schedule").length)}`;
379
+ deferred.push({ id, kind: "schedule", path, args: copy(args), at });
380
+ return id;
381
+ },
382
+ },
383
+ };
384
+ const result = copy(await definition.handler(context, copy(args)));
385
+ validateValue(definition.options.result, result);
386
+ return { result, patches, deferred, readTables: [...reads].sort() };
387
+ }
388
+ }
389
+ //# sourceMappingURL=portable.js.map