orez-sync-cf-host 0.5.19 → 0.5.21

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.
Files changed (52) hide show
  1. package/README.md +49 -6
  2. package/dist/bun-wasm-loader.d.ts +1 -0
  3. package/dist/bun-wasm-loader.js +20 -0
  4. package/dist/config.d.ts +3 -0
  5. package/dist/config.js +93 -0
  6. package/{src → dist}/generated/sync_wasm_bg.wasm +0 -0
  7. package/dist/host.d.ts +12 -0
  8. package/dist/host.js +1512 -0
  9. package/dist/index.d.ts +8 -0
  10. package/dist/index.js +4 -0
  11. package/dist/mutation-error.d.ts +9 -0
  12. package/dist/mutation-error.js +23 -0
  13. package/dist/node-wasm-loader.d.ts +1 -0
  14. package/dist/node-wasm-loader.js +22 -0
  15. package/dist/post-commit.d.ts +13 -0
  16. package/dist/post-commit.js +31 -0
  17. package/dist/query-compiler.d.ts +4 -0
  18. package/dist/query-compiler.js +4 -0
  19. package/dist/sql-storage-adapter.d.ts +69 -0
  20. package/dist/sql-storage-adapter.js +194 -0
  21. package/dist/transaction-query.d.ts +72 -0
  22. package/dist/transaction-query.js +379 -0
  23. package/dist/types.d.ts +159 -0
  24. package/dist/types.js +5 -0
  25. package/dist/vite-wasm-loader.d.ts +3 -0
  26. package/dist/vite-wasm-loader.js +24 -0
  27. package/dist/wasm.d.ts +1 -0
  28. package/dist/wasm.js +7 -0
  29. package/dist/write-safeguards.d.ts +53 -0
  30. package/dist/write-safeguards.js +186 -0
  31. package/package.json +59 -19
  32. package/src/bun-wasm-loader.ts +0 -16
  33. package/src/config.ts +0 -102
  34. package/src/harness-config.ts +0 -487
  35. package/src/harness-worker.ts +0 -33
  36. package/src/host.ts +0 -1958
  37. package/src/index.ts +0 -47
  38. package/src/ingest-harness-worker.ts +0 -535
  39. package/src/mutation-error.ts +0 -28
  40. package/src/platform-probe-worker.ts +0 -418
  41. package/src/post-commit.ts +0 -40
  42. package/src/query-compiler.ts +0 -16
  43. package/src/sql-storage-adapter.ts +0 -254
  44. package/src/transaction-query.ts +0 -494
  45. package/src/types.ts +0 -195
  46. package/src/wasm-platform.ts +0 -10
  47. package/src/wasm.ts +0 -34
  48. package/src/write-safeguards.ts +0 -248
  49. /package/{src → dist}/generated/package.json +0 -0
  50. /package/{src → dist}/generated/sync_wasm.d.ts +0 -0
  51. /package/{src → dist}/generated/sync_wasm.js +0 -0
  52. /package/{src → dist}/generated/sync_wasm_bg.wasm.d.ts +0 -0
@@ -0,0 +1,379 @@
1
+ export const DEFAULT_TRANSACTION_QUERY_BUDGET = {
2
+ maxSelects: 256,
3
+ maxRows: 10_000,
4
+ };
5
+ export class TransactionQueryBudgetError extends Error {
6
+ query;
7
+ selects;
8
+ rows;
9
+ maxSelects;
10
+ maxRows;
11
+ code = 'transaction_query_budget_exceeded';
12
+ constructor(query, selects, rows, maxSelects, maxRows) {
13
+ super(`transaction_query_budget_exceeded: query=${query} selects=${selects} rows=${rows} maxSelects=${maxSelects} maxRows=${maxRows}`);
14
+ this.query = query;
15
+ this.selects = selects;
16
+ this.rows = rows;
17
+ this.maxSelects = maxSelects;
18
+ this.maxRows = maxRows;
19
+ this.name = 'TransactionQueryBudgetError';
20
+ }
21
+ }
22
+ const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
23
+ const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
24
+ const PLAN_HASH = /^[0-9a-f]{16}$/;
25
+ const COLUMN_TYPES = new Set([
26
+ 'string',
27
+ 'number',
28
+ 'boolean',
29
+ 'json',
30
+ 'null',
31
+ ]);
32
+ function isObject(value) {
33
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
34
+ }
35
+ function malformed(detail) {
36
+ return new TypeError(`compiled transaction query plan ${detail}`);
37
+ }
38
+ function assertWireValue(value, path) {
39
+ if (!isObject(value) || typeof value.kind !== 'string') {
40
+ throw malformed(`${path} has an invalid literal`);
41
+ }
42
+ switch (value.kind) {
43
+ case 'null':
44
+ return value;
45
+ case 'integer':
46
+ if (typeof value.value !== 'string' || !/^-?[0-9]+$/.test(value.value)) {
47
+ throw malformed(`${path} has an invalid integer literal`);
48
+ }
49
+ return value;
50
+ case 'real':
51
+ if (typeof value.value !== 'number' || !Number.isFinite(value.value)) {
52
+ throw malformed(`${path} has an invalid real literal`);
53
+ }
54
+ return value;
55
+ case 'text':
56
+ if (typeof value.value !== 'string') {
57
+ throw malformed(`${path} has an invalid text literal`);
58
+ }
59
+ return value;
60
+ case 'blob':
61
+ if (!Array.isArray(value.value) ||
62
+ value.value.some((byte) => !Number.isInteger(byte) || Number(byte) < 0 || Number(byte) > 255)) {
63
+ throw malformed(`${path} has an invalid blob literal`);
64
+ }
65
+ return value;
66
+ default:
67
+ throw malformed(`${path} has an unknown literal kind`);
68
+ }
69
+ }
70
+ function assertNode(value, path, depth) {
71
+ if (depth > 64)
72
+ throw malformed(`${path} exceeds the maximum relationship depth`);
73
+ if (!isObject(value))
74
+ throw malformed(`${path} must be an object`);
75
+ if (typeof value.table !== 'string' || value.table.length === 0) {
76
+ throw malformed(`${path}.table must be a non-empty string`);
77
+ }
78
+ if (typeof value.singular !== 'boolean') {
79
+ throw malformed(`${path}.singular must be a boolean`);
80
+ }
81
+ if (typeof value.sql !== 'string' || value.sql.length === 0) {
82
+ throw malformed(`${path}.sql must be a non-empty string`);
83
+ }
84
+ if (!Array.isArray(value.bindings)) {
85
+ throw malformed(`${path}.bindings must be an array`);
86
+ }
87
+ for (const [index, binding] of value.bindings.entries()) {
88
+ if (!isObject(binding))
89
+ throw malformed(`${path}.bindings[${index}] is invalid`);
90
+ if (binding.kind === 'literal') {
91
+ assertWireValue(binding.value, `${path}.bindings[${index}]`);
92
+ }
93
+ else if (binding.kind !== 'parent_field' ||
94
+ typeof binding.field !== 'string' ||
95
+ binding.field.length === 0) {
96
+ throw malformed(`${path}.bindings[${index}] is invalid`);
97
+ }
98
+ }
99
+ if (!Array.isArray(value.columns) || value.columns.length === 0) {
100
+ throw malformed(`${path}.columns must be a non-empty array`);
101
+ }
102
+ const names = new Set();
103
+ for (const [index, column] of value.columns.entries()) {
104
+ if (!isObject(column) ||
105
+ typeof column.name !== 'string' ||
106
+ column.name.length === 0 ||
107
+ typeof column.columnType !== 'string' ||
108
+ !COLUMN_TYPES.has(column.columnType)) {
109
+ throw malformed(`${path}.columns[${index}] is invalid`);
110
+ }
111
+ if (names.has(column.name)) {
112
+ throw malformed(`${path}.columns contains duplicate '${column.name}'`);
113
+ }
114
+ names.add(column.name);
115
+ }
116
+ if (!Array.isArray(value.relationships)) {
117
+ throw malformed(`${path}.relationships must be an array`);
118
+ }
119
+ const relationships = new Set();
120
+ for (const [index, relationship] of value.relationships.entries()) {
121
+ if (!isObject(relationship) ||
122
+ typeof relationship.name !== 'string' ||
123
+ relationship.name.length === 0) {
124
+ throw malformed(`${path}.relationships[${index}] is invalid`);
125
+ }
126
+ if (relationships.has(relationship.name) || names.has(relationship.name)) {
127
+ throw malformed(`${path}.relationships contains conflicting '${relationship.name}'`);
128
+ }
129
+ relationships.add(relationship.name);
130
+ assertNode(relationship.node, `${path}.relationships[${index}].node`, depth + 1);
131
+ }
132
+ }
133
+ function assertPlan(value) {
134
+ if (!isObject(value))
135
+ throw malformed('must be an object');
136
+ if (typeof value.rootTable !== 'string' || value.rootTable.length === 0) {
137
+ throw malformed('rootTable must be a non-empty string');
138
+ }
139
+ if (typeof value.planHash !== 'string' || !PLAN_HASH.test(value.planHash)) {
140
+ throw malformed('planHash must be 16 lowercase hexadecimal characters');
141
+ }
142
+ assertNode(value.root, 'root', 0);
143
+ if (value.root.table !== value.rootTable) {
144
+ throw malformed('root.table must match rootTable');
145
+ }
146
+ }
147
+ function resolveBudget(budget) {
148
+ const resolved = { ...DEFAULT_TRANSACTION_QUERY_BUDGET, ...budget };
149
+ for (const [name, value] of Object.entries(resolved)) {
150
+ if (!Number.isSafeInteger(value) || value < 1) {
151
+ throw new TypeError(`transaction query budget ${name} must be a positive safe integer`);
152
+ }
153
+ }
154
+ return resolved;
155
+ }
156
+ function decodeWireValue(value) {
157
+ switch (value.kind) {
158
+ case 'null':
159
+ return null;
160
+ case 'integer': {
161
+ const exact = BigInt(value.value);
162
+ return exact >= MIN_SAFE && exact <= MAX_SAFE ? Number(exact) : value.value;
163
+ }
164
+ case 'real':
165
+ case 'text':
166
+ return value.value;
167
+ case 'blob':
168
+ return Uint8Array.from(value.value);
169
+ }
170
+ }
171
+ function bytes(value) {
172
+ if (value instanceof ArrayBuffer)
173
+ return new Uint8Array(value);
174
+ if (ArrayBuffer.isView(value)) {
175
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
176
+ }
177
+ return undefined;
178
+ }
179
+ const FINITE_NUMBER_TEXT = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
180
+ const TIMESTAMP_TEXT = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:Z|([+-])(\d{2})(?::?(\d{2}))?)?$/;
181
+ function numberFromStorageText(value) {
182
+ if (FINITE_NUMBER_TEXT.test(value)) {
183
+ const numeric = Number(value);
184
+ if (Number.isFinite(numeric))
185
+ return numeric === 0 ? 0 : numeric;
186
+ }
187
+ const match = TIMESTAMP_TEXT.exec(value);
188
+ if (!match)
189
+ return undefined;
190
+ const year = Number(match[1]);
191
+ const month = Number(match[2]);
192
+ const day = Number(match[3]);
193
+ const hour = Number(match[4]);
194
+ const minute = Number(match[5]);
195
+ const second = Number(match[6]);
196
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
197
+ const daysInMonth = month === 2
198
+ ? leap
199
+ ? 29
200
+ : 28
201
+ : [4, 6, 9, 11].includes(month)
202
+ ? 30
203
+ : month >= 1 && month <= 12
204
+ ? 31
205
+ : 0;
206
+ if (day < 1 || day > daysInMonth || hour > 23 || minute > 59 || second > 59) {
207
+ return undefined;
208
+ }
209
+ const offsetHours = Number(match[9] ?? 0);
210
+ const offsetMinutes = Number(match[10] ?? 0);
211
+ if (offsetHours > 23 || offsetMinutes > 59)
212
+ return undefined;
213
+ const offsetSign = match[8] === '-' ? -1 : 1;
214
+ const offsetSeconds = offsetSign * (offsetHours * 3_600 + offsetMinutes * 60);
215
+ const fraction = match[7] ?? '';
216
+ const milliseconds = Number(`${fraction}00`.slice(0, 3));
217
+ // howard hinnant's civil-date conversion keeps years 0000-0099 exact,
218
+ // unlike Date.UTC, and matches the Rust sync engine's implementation.
219
+ const adjustedYear = year - Number(month <= 2);
220
+ const era = Math.floor(adjustedYear / 400);
221
+ const yearOfEra = adjustedYear - era * 400;
222
+ const shiftedMonth = month + (month > 2 ? -3 : 9);
223
+ const dayOfYear = Math.floor((153 * shiftedMonth + 2) / 5) + day - 1;
224
+ const dayOfEra = yearOfEra * 365 + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100) + dayOfYear;
225
+ const days = era * 146_097 + dayOfEra - 719_468;
226
+ return ((days * 86_400 + hour * 3_600 + minute * 60 + second - offsetSeconds) * 1_000 +
227
+ milliseconds);
228
+ }
229
+ function decodeColumn(value, columnType, path) {
230
+ if (value === null)
231
+ return null;
232
+ switch (columnType) {
233
+ case 'string':
234
+ if (typeof value === 'string')
235
+ return value;
236
+ break;
237
+ case 'number':
238
+ if (typeof value === 'number' && Number.isFinite(value))
239
+ return value;
240
+ if (typeof value === 'bigint' && value >= MIN_SAFE && value <= MAX_SAFE) {
241
+ return Number(value);
242
+ }
243
+ if (typeof value === 'string') {
244
+ const decoded = numberFromStorageText(value);
245
+ if (decoded !== undefined)
246
+ return decoded;
247
+ }
248
+ break;
249
+ case 'boolean':
250
+ if (typeof value === 'boolean')
251
+ return value;
252
+ if (value === 0 || value === 0n || value === '0')
253
+ return false;
254
+ if (value === 1 || value === 1n || value === '1')
255
+ return true;
256
+ break;
257
+ case 'json': {
258
+ if (typeof value === 'string') {
259
+ try {
260
+ return JSON.parse(value);
261
+ }
262
+ catch {
263
+ throw new TypeError(`transaction query row ${path} contains invalid JSON`);
264
+ }
265
+ }
266
+ const encoded = bytes(value);
267
+ if (encoded) {
268
+ try {
269
+ return JSON.parse(new TextDecoder().decode(encoded));
270
+ }
271
+ catch {
272
+ throw new TypeError(`transaction query row ${path} contains invalid JSON`);
273
+ }
274
+ }
275
+ if (typeof value === 'object')
276
+ return value;
277
+ break;
278
+ }
279
+ case 'null':
280
+ break;
281
+ }
282
+ throw new TypeError(`transaction query row ${path} does not match schema type ${columnType}`);
283
+ }
284
+ function decodeRow(raw, node) {
285
+ const decoded = {};
286
+ for (const column of node.columns) {
287
+ if (!Object.hasOwn(raw, column.name)) {
288
+ throw new TypeError(`transaction query row for ${node.table} is missing column ${column.name}`);
289
+ }
290
+ decoded[column.name] = decodeColumn(raw[column.name], column.columnType, `${node.table}.${column.name}`);
291
+ }
292
+ return decoded;
293
+ }
294
+ function createExecutionState(plan, options) {
295
+ assertPlan(plan);
296
+ return {
297
+ budget: resolveBudget(options.budget),
298
+ query: options.queryName?.trim() || `${plan.rootTable}:${plan.planHash}`,
299
+ rows: 0,
300
+ selects: 0,
301
+ };
302
+ }
303
+ function assertWithinBudget(state) {
304
+ if (state.selects <= state.budget.maxSelects && state.rows <= state.budget.maxRows) {
305
+ return;
306
+ }
307
+ throw new TransactionQueryBudgetError(state.query, state.selects, state.rows, state.budget.maxSelects, state.budget.maxRows);
308
+ }
309
+ function prepareSelect(state, node, parent) {
310
+ const params = node.bindings.map((binding) => {
311
+ if (binding.kind === 'literal')
312
+ return decodeWireValue(binding.value);
313
+ if (!parent || !Object.hasOwn(parent, binding.field)) {
314
+ throw new TypeError(`transaction query parent row for ${node.table} is missing field ${binding.field}`);
315
+ }
316
+ return parent[binding.field];
317
+ });
318
+ const next = { ...state, selects: state.selects + 1 };
319
+ assertWithinBudget(next);
320
+ return { params, state: next };
321
+ }
322
+ function decodeSelect(state, node, materialized) {
323
+ if (!Array.isArray(materialized)) {
324
+ throw new TypeError('transaction query select must return a materialized row array');
325
+ }
326
+ const next = { ...state, rows: state.rows + materialized.length };
327
+ assertWithinBudget(next);
328
+ const rows = materialized.map((raw) => {
329
+ if (!isObject(raw)) {
330
+ throw new TypeError(`transaction query select for ${node.table} returned a non-object row`);
331
+ }
332
+ return { decoded: decodeRow(raw, node), raw };
333
+ });
334
+ return { rows, state: next };
335
+ }
336
+ function shapeRows(node, rows) {
337
+ return node.singular ? rows[0] : rows;
338
+ }
339
+ export function executeTransactionQueryPlan(plan, execute, options = {}) {
340
+ const run = (node, state, parent) => {
341
+ const prepared = prepareSelect(state, node, parent);
342
+ const selected = decodeSelect(prepared.state, node, execute(node.sql, prepared.params));
343
+ const hydrated = [];
344
+ let current = selected.state;
345
+ for (const { decoded, raw } of selected.rows) {
346
+ for (const relationship of node.relationships) {
347
+ const related = run(relationship.node, current, raw);
348
+ current = related.state;
349
+ decoded[relationship.name] =
350
+ relationship.node.singular && related.value === undefined ? null : related.value;
351
+ }
352
+ hydrated.push(decoded);
353
+ }
354
+ return { state: current, value: shapeRows(node, hydrated) };
355
+ };
356
+ const state = createExecutionState(plan, options);
357
+ return run(plan.root, state).value;
358
+ }
359
+ export async function executeTransactionQueryPlanAsync(plan, execute, options = {}) {
360
+ const run = async (node, state, parent) => {
361
+ const prepared = prepareSelect(state, node, parent);
362
+ const selected = decodeSelect(prepared.state, node, await execute(node.sql, prepared.params));
363
+ const hydrated = [];
364
+ let current = selected.state;
365
+ for (const { decoded, raw } of selected.rows) {
366
+ for (const relationship of node.relationships) {
367
+ const related = await run(relationship.node, current, raw);
368
+ current = related.state;
369
+ decoded[relationship.name] =
370
+ relationship.node.singular && related.value === undefined ? null : related.value;
371
+ }
372
+ hydrated.push(decoded);
373
+ }
374
+ return { state: current, value: shapeRows(node, hydrated) };
375
+ };
376
+ const state = createExecutionState(plan, options);
377
+ const { value } = await run(plan.root, state);
378
+ return value;
379
+ }
@@ -0,0 +1,159 @@
1
+ import type { DeferredEffect } from './post-commit.js';
2
+ import type { TransactionQueryBudget, TransactionQueryFormat } from './transaction-query.js';
3
+ export type { DeferredEffect } from './post-commit.js';
4
+ export type JsonPrimitive = string | number | boolean | null;
5
+ export type JsonValue = JsonPrimitive | JsonValue[] | {
6
+ [key: string]: JsonValue;
7
+ };
8
+ export type NormalizedClaims = {
9
+ /** Stable consumer user id used for client-group ownership. */
10
+ userID: string;
11
+ [claim: string]: JsonValue;
12
+ };
13
+ export type ZeroSchemaConfig = {
14
+ readonly tables: Readonly<Record<string, {
15
+ readonly name?: string;
16
+ readonly serverName?: string;
17
+ readonly columns: Readonly<Record<string, {
18
+ readonly type: string;
19
+ readonly serverName?: string;
20
+ }>>;
21
+ readonly primaryKey: readonly string[];
22
+ }>>;
23
+ };
24
+ /**
25
+ * Explicit identity for a SQLite write to a published application table.
26
+ *
27
+ * The physical table installs CDC. `publicTable` is the logical Zero name
28
+ * emitted in the change feed. This replaces metadata recovered by parsing
29
+ * PostgreSQL statements.
30
+ */
31
+ export type SqlStatementMetadata = {
32
+ table: string;
33
+ publicTable: string;
34
+ kind: 'insert' | 'update' | 'delete' | 'upsert';
35
+ };
36
+ export type SQLiteExecResult = {
37
+ changes: number;
38
+ };
39
+ export interface SyncSql {
40
+ exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): SQLiteExecResult;
41
+ query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Row[];
42
+ }
43
+ export interface MutatorSql {
44
+ exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): Promise<SQLiteExecResult>;
45
+ query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<Row[]>;
46
+ /** Execute a validated Zero AST inside the current application transaction. */
47
+ queryAst<Result = unknown>(ast: JsonValue, format: TransactionQueryFormat, queryName?: string): Promise<Result>;
48
+ }
49
+ export type MutatorContext = {
50
+ claims: NormalizedClaims;
51
+ /** Current Zero mutation identity, when invoked by the production host. */
52
+ clientID?: string;
53
+ mutationID?: string | number;
54
+ defer(effect: DeferredEffect): void;
55
+ };
56
+ export type RegisteredMutator = (tx: MutatorSql, args: JsonValue, context: MutatorContext) => void | Promise<void>;
57
+ export type MutatorRegistry = Readonly<Record<string, RegisteredMutator>>;
58
+ /** Preserve mutator names while making the host-facing registry immutable. */
59
+ export declare function registerMutators<const Registry extends Record<string, RegisteredMutator>>(registry: Registry): Readonly<Registry>;
60
+ export { MutationApplicationError, isMutationApplicationError } from './mutation-error.js';
61
+ export type VisibilityFilter = {
62
+ /** SQL WHERE fragment only, without the WHERE keyword. */
63
+ sql: string;
64
+ params?: readonly JsonPrimitive[];
65
+ };
66
+ export type VisibilityConfig = {
67
+ /** True only when every predicate depends on the selected row alone. */
68
+ rowLocal: boolean | ((claims: NormalizedClaims) => boolean);
69
+ filter(table: string, claims: NormalizedClaims): VisibilityFilter | undefined;
70
+ };
71
+ export type QueryResolver = (name: string, args: readonly JsonValue[], claims: NormalizedClaims, env: SyncHostEnv) => JsonValue | Promise<JsonValue>;
72
+ export type PullCaps = {
73
+ maxChangeRows: number;
74
+ maxChangeBytes: number;
75
+ };
76
+ export interface SyncHostEnv {
77
+ SYNC_DO: DurableObjectNamespace;
78
+ ADMIN_KEY?: string;
79
+ }
80
+ export type ServiceBinding = {
81
+ fetch(input: string | Request, init?: RequestInit): Promise<Response>;
82
+ };
83
+ export type UpstreamConfig = {
84
+ /** Env key for the service binding that owns the app write endpoint and feed. */
85
+ binding: string;
86
+ /** Path to this namespace on the bound service (for example `/data/<id>`). */
87
+ namespacePath: string | ((namespace: string) => string);
88
+ /** Feed page size; the cursor loop continues until the reported head is reached. */
89
+ changeLimit?: number;
90
+ /** Active wake-socket alarm safety net. Defaults to 15 seconds. */
91
+ intervalMs?: number;
92
+ /** Billable SQLite rows written by ingest per rolling window. Defaults to 150,000. */
93
+ ingestBudgetRows?: number;
94
+ /** Rolling ingest budget window. Defaults to five minutes. */
95
+ ingestBudgetWindowMs?: number;
96
+ /** Initial breaker cooldown. Defaults to one second. */
97
+ ingestBackoffMs?: number;
98
+ /** Maximum breaker cooldown. Defaults to one minute. */
99
+ ingestMaxBackoffMs?: number;
100
+ };
101
+ export type DelegatedPushRetryConfig = {
102
+ /** Total attempts including the first request. Defaults to 3. */
103
+ maxAttempts?: number;
104
+ /** Initial exponential delay. Defaults to 100ms. */
105
+ initialBackoffMs?: number;
106
+ /** Delay cap. Defaults to 1,000ms. */
107
+ maxBackoffMs?: number;
108
+ /** Per-attempt service-binding timeout. Defaults to 5,000ms. */
109
+ timeoutMs?: number;
110
+ };
111
+ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv> = {
112
+ hostVersion: string;
113
+ schema: ZeroSchemaConfig;
114
+ mutators?: MutatorRegistry;
115
+ /**
116
+ * absolute app push path on the delegated mutation service. a successful
117
+ * response must be causally visible through the configured upstream data
118
+ * feed before the app returns, because the host ingests effects before it
119
+ * records the mutation's lmid.
120
+ */
121
+ mutateUrl?: string;
122
+ /** Absolute origin used for delegated push requests through the service binding. */
123
+ mutateOrigin?: string;
124
+ /**
125
+ * env binding for delegated pushes; defaults to upstream.binding. the bound
126
+ * service and upstream feed must satisfy the mutateUrl causality contract.
127
+ */
128
+ mutateBinding?: string;
129
+ delegatedPushRetry?: DelegatedPushRetryConfig;
130
+ /** Required for delegated push; forbidden with local mutators (no dual apply). */
131
+ upstream?: UpstreamConfig;
132
+ /** Application DDL and optional seed, called before sync-core schema init. */
133
+ initialize(sql: SyncSql): void;
134
+ authenticate(request: Request, env: Env): NormalizedClaims | null | Promise<NormalizedClaims | null>;
135
+ /** Authorize the advisory wake socket before selecting a namespace DO.
136
+ * Browser clients should present a short-lived, namespace-scoped capability
137
+ * in the query string because WebSocket cannot set request headers. */
138
+ authorizeWake(request: Request, env: Env): boolean | Promise<boolean>;
139
+ /** Authorize upstream service notifications before selecting a namespace DO. */
140
+ authorizeNotify(request: Request, env: Env): boolean | Promise<boolean>;
141
+ /** Resolve the first path component or another consumer-defined namespace. */
142
+ namespace(request: Request): string | null;
143
+ visibility?: VisibilityConfig;
144
+ /** Enable desired-query pulls for this namespace and resolve named queries
145
+ * into validated Zero ASTs before they reach sync-core. */
146
+ queryAware?: boolean | ((claims: NormalizedClaims) => boolean);
147
+ resolveQuery?: QueryResolver;
148
+ /** Server-owned invalidation epoch for permission/schema transforms. */
149
+ queryTransformVersion?: number | ((claims: NormalizedClaims) => number);
150
+ /** Enable consumer visibility from the first request. Defaults to false for harnesses. */
151
+ visibilityEnabled?: boolean;
152
+ retainChanges?: number;
153
+ caps?: Partial<PullCaps>;
154
+ idleTeardownMs?: number;
155
+ wakeCoalesceMs?: number;
156
+ /** per-query guard for recursive transaction query materialization. */
157
+ transactionQueryBudget?: Partial<TransactionQueryBudget>;
158
+ authorizeAdmin?: (request: Request, env: Env) => boolean | Promise<boolean>;
159
+ };
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /** Preserve mutator names while making the host-facing registry immutable. */
2
+ export function registerMutators(registry) {
3
+ return Object.freeze({ ...registry });
4
+ }
5
+ export { MutationApplicationError, isMutationApplicationError } from './mutation-error.js';
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'vite';
2
+ /** Load the sync engine for Vite's Node serve, SSR, and production build paths. */
3
+ export declare function orezSyncCfHostWasm(): Plugin;
@@ -0,0 +1,24 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ const wasmModuleID = 'orez-sync-cf-host/wasm-module.wasm';
3
+ const resolvedWasmModuleID = `\0${wasmModuleID}`;
4
+ /** Load the sync engine for Vite's Node serve, SSR, and production build paths. */
5
+ export function orezSyncCfHostWasm() {
6
+ return {
7
+ name: 'orez-sync-cf-host-wasm',
8
+ enforce: 'pre',
9
+ config() {
10
+ return { ssr: { noExternal: ['orez-sync-cf-host'] } };
11
+ },
12
+ resolveId(source) {
13
+ return source === wasmModuleID ? resolvedWasmModuleID : null;
14
+ },
15
+ async load(id) {
16
+ if (id !== resolvedWasmModuleID)
17
+ return null;
18
+ const bytes = await readFile(new URL('./generated/sync_wasm_bg.wasm', import.meta.url));
19
+ return `import { Buffer } from 'node:buffer'
20
+ export default new WebAssembly.Module(Buffer.from('${bytes.toString('base64')}', 'base64'))
21
+ `;
22
+ },
23
+ };
24
+ }
package/dist/wasm.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { engine_apply_snapshot_changes, engine_apply_snapshot_page, engine_apply_upstream, engine_assemble_push_response, engine_begin_snapshot_generation, engine_compile_query, engine_finalize, engine_finalize_snapshot_generation, engine_handle_pull, engine_handle_query_pull, engine_init_query_schema, engine_init_schema, engine_invalidate, engine_memory_bytes, engine_preflight, engine_prune, engine_push_validate, engine_read_snapshot_progress, engine_record_app_error, engine_state, engine_version, } from './generated/sync_wasm.js';
package/dist/wasm.js ADDED
@@ -0,0 +1,7 @@
1
+ import wasmModule from 'orez-sync-cf-host/wasm-module.wasm';
2
+ import { initSync } from './generated/sync_wasm.js';
3
+ if (!(wasmModule instanceof WebAssembly.Module)) {
4
+ throw new TypeError('.wasm import did not resolve to WebAssembly.Module; see the orez-sync-cf-host README Query compiler runtimes matrix');
5
+ }
6
+ initSync({ module: wasmModule });
7
+ export { engine_apply_snapshot_changes, engine_apply_snapshot_page, engine_apply_upstream, engine_assemble_push_response, engine_begin_snapshot_generation, engine_compile_query, engine_finalize, engine_finalize_snapshot_generation, engine_handle_pull, engine_handle_query_pull, engine_init_query_schema, engine_init_schema, engine_invalidate, engine_memory_bytes, engine_preflight, engine_prune, engine_push_validate, engine_read_snapshot_progress, engine_record_app_error, engine_state, engine_version, } from './generated/sync_wasm.js';
@@ -0,0 +1,53 @@
1
+ export type IngestBreakerReason = 'ingestBudgetExceeded' | 'ingestCursorStalled';
2
+ export type IngestBreakerOptions = {
3
+ budgetRows: number;
4
+ windowMs: number;
5
+ initialBackoffMs: number;
6
+ maxBackoffMs: number;
7
+ now: () => number;
8
+ };
9
+ export type IngestBreakerStatus = {
10
+ windowRows: number;
11
+ billableRows: number;
12
+ logicalRows: number;
13
+ budget: number;
14
+ windowMs: number;
15
+ tripped: boolean;
16
+ reason: IngestBreakerReason | null;
17
+ retryAt: number | null;
18
+ retryAfterMs: number;
19
+ consecutiveTrips: number;
20
+ };
21
+ export declare class IngestBreakerError extends Error {
22
+ readonly error: IngestBreakerReason;
23
+ readonly windowRows: number;
24
+ readonly budget: number;
25
+ readonly retryAfterMs: number;
26
+ readonly status = 429;
27
+ constructor(error: IngestBreakerReason, windowRows: number, budget: number, retryAfterMs: number);
28
+ }
29
+ export declare class IngestCircuitBreaker {
30
+ #private;
31
+ constructor(options: IngestBreakerOptions);
32
+ status(): IngestBreakerStatus;
33
+ assertReady(): void;
34
+ record(rows: number): void;
35
+ recordLogical(rows: number): void;
36
+ recordBillable(rows: number): void;
37
+ trip(reason: IngestBreakerReason): never;
38
+ recovered(): void;
39
+ restore(reason: IngestBreakerReason, retryAt: number, consecutiveTrips: number): void;
40
+ reopen(): void;
41
+ }
42
+ type BillableCursor = {
43
+ rowsWritten?: number;
44
+ next?: (...args: unknown[]) => unknown;
45
+ one?: (...args: unknown[]) => unknown;
46
+ toArray?: (...args: unknown[]) => unknown;
47
+ raw?: (...args: unknown[]) => unknown;
48
+ [Symbol.iterator]?: () => Iterator<unknown>;
49
+ };
50
+ export declare function trackBillableCursorRows<Cursor extends BillableCursor>(cursor: Cursor, record: (rows: number) => void): Cursor;
51
+ export declare function retryDelayMs(attempt: number, initialBackoffMs: number, maxBackoffMs: number): number;
52
+ export declare function shouldRetryDelegatedPush(responseStatus: number | null, attempt: number, maxAttempts: number): boolean;
53
+ export {};