orez-sync-cf-host 0.11.7 → 0.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/README.md CHANGED
@@ -5,19 +5,27 @@ engine. The package exposes a consumer integration surface (`createSyncWorker`,
5
5
  `createSyncDurableObject`) and a harness deployment built
6
6
  from `src/harness-worker.ts`. Consumers provide a JSON Zero schema, application
7
7
  DDL/seed initializer, normalized-claims authenticator, an
8
- `orez-sync-executor` mutator registry, and an optional row-visibility hook.
9
-
10
- The Worker authenticates and authorizes at the consumer edge, then forwards normalized claims
11
- inside the binding request body, where request-header observability cannot record
12
- them. It never logs tokens, mutation arguments, or row contents. Pull runs
13
- `engine_handle_pull` inside `transactionSync`; local push delegates validation,
14
- replay, LMID advancement, CRUD, and committed effects to `orez-sync-executor`
15
- inside `ctx.storage.transaction`. Every SQL cursor is materialized before an
16
- await. Mutators may await only their application transaction operations;
17
- timers, fetches, and other external work belong in
8
+ `orez-sync-executor` mutator registry or delegated push route, and the app's
9
+ ordinary Zero `defineQueries` registry.
10
+
11
+ The Worker authenticates and authorizes at the consumer edge, then forwards
12
+ normalized claims inside the binding request body, where request-header
13
+ observability cannot record them. It never logs tokens, mutation arguments, or
14
+ row contents. Pull runs `engine_handle_query_pull` inside `transactionSync`;
15
+ local push delegates validation, replay, LMID advancement, CRUD, and committed
16
+ effects to `orez-sync-executor` inside `ctx.storage.transaction`. Every SQL
17
+ cursor is materialized before an await. Mutators may await only their
18
+ application transaction operations; timers, fetches, and other external work
19
+ belong in
18
20
  `ctx.defer`, which runs only after commit. Application failures use the
19
21
  required second transaction to advance the LMID marker.
20
22
 
23
+ Every pull is query-driven. Set `config.queries` to the same registry used by
24
+ the client. The host resolves desired named queries in-process and passes
25
+ authenticated claims as their context, so permission scoping remains inside the
26
+ query definitions. Membership-driven puts and dels keep pull work proportional
27
+ to query results without projecting the full namespace.
28
+
21
29
  Transaction-query `ILIKE` folding is ASCII-only on Durable Object SQLite;
22
30
  non-ASCII case pairs can diverge from PostgreSQL.
23
31
 
@@ -154,7 +162,7 @@ convergence.
154
162
  sockets, durable database size, engine watermark/floor, and aggregate counters.
155
163
  After the configured idle gap (5 seconds in the harness deployment), the local
156
164
  deterministic model resets in-memory state and changes boot ID while retaining
157
- SQLite state and sockets. This models the harness/cf idle-teardown pattern; it
165
+ SQLite state and sockets. This models the harness idle-teardown pattern; it
158
166
  does not claim that a real platform eviction happens on a 5-second schedule.
159
167
 
160
168
  ## Authenticated operator controls
@@ -224,8 +232,8 @@ A successful delegated mutation response has a hard causality contract: its
224
232
  committed application effects must already be readable from the configured
225
233
  upstream `/changes` feed before the application worker returns. The sync host
226
234
  then ingests through those effects, journals the acknowledged LMID after them,
227
- and only then returns the push response. This keeps every capped change-log
228
- prefix ordered so an acknowledgement cannot reach a client before its effects.
235
+ and only then returns the push response. This keeps change-log order intact so
236
+ an acknowledgement cannot reach a client before its effects.
229
237
  Chat and Soot use an application-to-data service path that provides this
230
238
  ordering today. A future topology that cannot provide it must extend the
231
239
  delegated response with an upstream watermark receipt and make the host ingest
package/dist/config.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { PullCaps, SyncHostConfig, SyncHostEnv } from './types.js';
1
+ import type { SyncHostConfig, SyncHostEnv } from './types.js';
2
2
  import type { Schema } from '@rocicorp/zero';
3
- export declare function validatePullCaps(caps: PullCaps): PullCaps;
4
3
  export declare function validateSyncHostConfig<Env extends SyncHostEnv, S extends Schema = Schema>(config: SyncHostConfig<Env, S>): SyncHostConfig<Env, S>;
package/dist/config.js CHANGED
@@ -1,9 +1,3 @@
1
- export function validatePullCaps(caps) {
2
- if (!Number.isSafeInteger(caps.maxChangeRows) || caps.maxChangeRows < 1) {
3
- throw new TypeError('caps.maxChangeRows must be a positive safe integer');
4
- }
5
- return caps;
6
- }
7
1
  export function validateSyncHostConfig(config) {
8
2
  if (typeof config.authorizeWake !== 'function') {
9
3
  throw new TypeError('sync host config authorizeWake is required');
@@ -14,6 +8,9 @@ export function validateSyncHostConfig(config) {
14
8
  if (typeof config.authorize !== 'function') {
15
9
  throw new TypeError('sync host config authorize is required');
16
10
  }
11
+ if (!config.queries || typeof config.queries !== 'object') {
12
+ throw new TypeError("sync host config queries is required: pass the app's Zero query registry (defineQueries result)");
13
+ }
17
14
  const hasMutators = config.mutators !== undefined;
18
15
  const hasDelegate = config.mutateUrl !== undefined;
19
16
  if (hasMutators === hasDelegate) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sync-wasm",
3
3
  "type": "module",
4
- "version": "0.1.1",
4
+ "version": "0.1.2",
5
5
  "license": "MIT",
6
6
  "files": [
7
7
  "sync_wasm_bg.wasm",
@@ -63,12 +63,7 @@ export function engine_finalize(db: any, client_group_id: string, client_id: str
63
63
  export function engine_finalize_snapshot_generation(db: any, schema: any, generation: string, watermark: string): any;
64
64
 
65
65
  /**
66
- * Production pull entry. The TypeScript host owns `transactionSync`.
67
- */
68
- export function engine_handle_pull(db: any, schema: any, visibility: any, caps: any, retain_changes: string, body: any, user_id: string): any;
69
-
70
- /**
71
- * Query-aware pull entry point. Desired-query ASTs are already resolved and
66
+ * Production pull entry point. Desired-query ASTs are already resolved and
72
67
  * validated by the consumer host before crossing this boundary.
73
68
  */
74
69
  export function engine_handle_query_pull(db: any, schema: any, retain_changes: string, body: any, user_id: string): any;
@@ -128,7 +123,6 @@ export interface InitOutput {
128
123
  readonly engine_compile_query: (a: any, b: any, c: any) => [number, number, number];
129
124
  readonly engine_finalize: (a: any, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number];
130
125
  readonly engine_finalize_snapshot_generation: (a: any, b: any, c: number, d: number, e: number, f: number) => [number, number, number];
131
- readonly engine_handle_pull: (a: any, b: any, c: any, d: any, e: number, f: number, g: any, h: number, i: number) => [number, number, number];
132
126
  readonly engine_handle_query_pull: (a: any, b: any, c: number, d: number, e: any, f: number, g: number) => [number, number, number];
133
127
  readonly engine_init_query_schema: (a: any) => [number, number];
134
128
  readonly engine_init_schema: (a: any, b: any) => [number, number];
@@ -197,30 +197,7 @@ export function engine_finalize_snapshot_generation(db, schema, generation, wate
197
197
  }
198
198
 
199
199
  /**
200
- * Production pull entry. The TypeScript host owns `transactionSync`.
201
- * @param {any} db
202
- * @param {any} schema
203
- * @param {any} visibility
204
- * @param {any} caps
205
- * @param {string} retain_changes
206
- * @param {any} body
207
- * @param {string} user_id
208
- * @returns {any}
209
- */
210
- export function engine_handle_pull(db, schema, visibility, caps, retain_changes, body, user_id) {
211
- const ptr0 = passStringToWasm0(retain_changes, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
212
- const len0 = WASM_VECTOR_LEN;
213
- const ptr1 = passStringToWasm0(user_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
214
- const len1 = WASM_VECTOR_LEN;
215
- const ret = wasm.engine_handle_pull(db, schema, visibility, caps, ptr0, len0, body, ptr1, len1);
216
- if (ret[2]) {
217
- throw takeFromExternrefTable0(ret[1]);
218
- }
219
- return takeFromExternrefTable0(ret[0]);
220
- }
221
-
222
- /**
223
- * Query-aware pull entry point. Desired-query ASTs are already resolved and
200
+ * Production pull entry point. Desired-query ASTs are already resolved and
224
201
  * validated by the consumer host before crossing this boundary.
225
202
  * @param {any} db
226
203
  * @param {any} schema
@@ -512,7 +489,7 @@ function __wbg_get_imports() {
512
489
  const ret = Object.entries(arg0);
513
490
  return ret;
514
491
  },
515
- __wbg_exec_0b4e8f9e7a8c0e02: function() { return handleError(function (arg0, arg1, arg2, arg3) {
492
+ __wbg_exec_58f139058b7f1498: function() { return handleError(function (arg0, arg1, arg2, arg3) {
516
493
  arg0.exec(getStringFromWasm0(arg1, arg2), arg3);
517
494
  }, arguments); },
518
495
  __wbg_get_507a50627bffa49b: function(arg0, arg1) {
@@ -622,7 +599,7 @@ function __wbg_get_imports() {
622
599
  __wbg_prototypesetcall_4770620bbe4688a0: function(arg0, arg1, arg2) {
623
600
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
624
601
  },
625
- __wbg_query_61b60d4aa0a8eccc: function() { return handleError(function (arg0, arg1, arg2, arg3) {
602
+ __wbg_query_03a8dbd1d5cf6385: function() { return handleError(function (arg0, arg1, arg2, arg3) {
626
603
  const ret = arg0.query(getStringFromWasm0(arg1, arg2), arg3);
627
604
  return ret;
628
605
  }, arguments); },
@@ -195,30 +195,7 @@ export function engine_finalize_snapshot_generation(db, schema, generation, wate
195
195
  }
196
196
 
197
197
  /**
198
- * Production pull entry. The TypeScript host owns `transactionSync`.
199
- * @param {any} db
200
- * @param {any} schema
201
- * @param {any} visibility
202
- * @param {any} caps
203
- * @param {string} retain_changes
204
- * @param {any} body
205
- * @param {string} user_id
206
- * @returns {any}
207
- */
208
- export function engine_handle_pull(db, schema, visibility, caps, retain_changes, body, user_id) {
209
- const ptr0 = passStringToWasm0(retain_changes, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
210
- const len0 = WASM_VECTOR_LEN;
211
- const ptr1 = passStringToWasm0(user_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
212
- const len1 = WASM_VECTOR_LEN;
213
- const ret = wasm.engine_handle_pull(db, schema, visibility, caps, ptr0, len0, body, ptr1, len1);
214
- if (ret[2]) {
215
- throw takeFromExternrefTable0(ret[1]);
216
- }
217
- return takeFromExternrefTable0(ret[0]);
218
- }
219
-
220
- /**
221
- * Query-aware pull entry point. Desired-query ASTs are already resolved and
198
+ * Production pull entry point. Desired-query ASTs are already resolved and
222
199
  * validated by the consumer host before crossing this boundary.
223
200
  * @param {any} db
224
201
  * @param {any} schema
@@ -507,7 +484,7 @@ export function __wbg_entries_015dc610cd81ede0(arg0) {
507
484
  const ret = Object.entries(arg0);
508
485
  return ret;
509
486
  }
510
- export function __wbg_exec_0b4e8f9e7a8c0e02() { return handleError(function (arg0, arg1, arg2, arg3) {
487
+ export function __wbg_exec_58f139058b7f1498() { return handleError(function (arg0, arg1, arg2, arg3) {
511
488
  arg0.exec(getStringFromWasm0(arg1, arg2), arg3);
512
489
  }, arguments); }
513
490
  export function __wbg_get_507a50627bffa49b(arg0, arg1) {
@@ -617,7 +594,7 @@ export function __wbg_next_71f2aa1cb3d1e37e() { return handleError(function (arg
617
594
  export function __wbg_prototypesetcall_4770620bbe4688a0(arg0, arg1, arg2) {
618
595
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
619
596
  }
620
- export function __wbg_query_61b60d4aa0a8eccc() { return handleError(function (arg0, arg1, arg2, arg3) {
597
+ export function __wbg_query_03a8dbd1d5cf6385() { return handleError(function (arg0, arg1, arg2, arg3) {
621
598
  const ret = arg0.query(getStringFromWasm0(arg1, arg2), arg3);
622
599
  return ret;
623
600
  }, arguments); }
Binary file
package/dist/host.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { DurableObject } from 'cloudflare:workers';
2
2
  import { createSyncExecutor } from 'orez-sync-executor/core';
3
3
  import { createSocketHost } from 'orez-sync-executor/realtime';
4
- import { validatePullCaps, validateSyncHostConfig } from './config.js';
4
+ import { validateSyncHostConfig } from './config.js';
5
5
  import { createQueryCompiler } from './query-compiler.js';
6
6
  import { resolveQueryPatch } from './query-patch.js';
7
7
  import { decodeSqlParams, SqlStorageDirect, SqlStorageMutatorTransaction, SqlStorageSyncDb, } from './sql-storage-adapter.js';
8
- import { engine_authorize_realtime_subscription, engine_apply_snapshot_changes, engine_apply_snapshot_page, engine_apply_upstream, engine_begin_snapshot_generation, 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_state, engine_version, } from './wasm.js';
8
+ import { engine_authorize_realtime_subscription, engine_apply_snapshot_changes, engine_apply_snapshot_page, engine_apply_upstream, engine_begin_snapshot_generation, engine_finalize, engine_finalize_snapshot_generation, 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_state, engine_version, } from './wasm.js';
9
9
  import { IngestBreakerError, IngestCircuitBreaker, retryDelayMs, shouldRetryDelegatedPush, } from './write-safeguards.js';
10
10
  const NAMESPACE_HEADER = 'x-orez-sync-namespace';
11
11
  const UPSTREAM_PATH_HEADER = 'x-orez-sync-upstream-path';
@@ -16,10 +16,6 @@ const UPSTREAM_PATH_HEADER = 'x-orez-sync-upstream-path';
16
16
  const IDENTITY_HEADER = 'x-orez-sync-identity';
17
17
  const DEFAULT_SNAPSHOT_PAGE_ROWS = 2_000;
18
18
  const MIN_SNAPSHOT_PAGE_ROWS = 100;
19
- const DEFAULT_CAPS = {
20
- maxChangeRows: 10_000,
21
- maxChangeBytes: 2_000_000,
22
- };
23
19
  function freshCounters() {
24
20
  return {
25
21
  pulls: 0,
@@ -90,28 +86,6 @@ async function requestObject(request) {
90
86
  throw requestError('request body must be a JSON object');
91
87
  return value;
92
88
  }
93
- // clients always ship their desired queries and treat the server's got ack as
94
- // authoritative. the query-aware engine acks through its own tracking; a
95
- // non-query-aware host syncs every visible row, so its ack is an echo of the
96
- // hash-level desired delta in the same response that carries the rows.
97
- function withGotQueriesAck(body, queryAware, response) {
98
- if (queryAware)
99
- return response;
100
- const queries = body.queries;
101
- if (!queries || typeof queries.version !== 'number' || !Array.isArray(queries.patch)) {
102
- return response;
103
- }
104
- return {
105
- ...response,
106
- gotQueries: {
107
- version: queries.version,
108
- patch: queries.patch.map((op) => {
109
- const entry = op;
110
- return entry.op === 'put' ? { op: 'put', hash: entry.hash } : op;
111
- }),
112
- },
113
- };
114
- }
115
89
  function routeAfterNamespace(pathname) {
116
90
  const [, , ...parts] = pathname.split('/');
117
91
  return `/${parts.join('/')}`;
@@ -269,7 +243,6 @@ export function createSyncDurableObject(config) {
269
243
  validateSyncHostConfig(config);
270
244
  const compileQuery = createQueryCompiler(config.schema);
271
245
  const defaultRetainChanges = String(config.retainChanges ?? 4_096);
272
- const caps = validatePullCaps({ ...DEFAULT_CAPS, ...config.caps });
273
246
  const idleTeardownMs = config.idleTeardownMs ?? 5_000;
274
247
  // A CF fan-out wakes every client into an HTTP pull. Give concurrent writer
275
248
  // requests a real batching window so a storm burst creates one pull wave.
@@ -308,7 +281,6 @@ export function createSyncDurableObject(config) {
308
281
  // during the replay wait for it instead of racing past into an empty hub.
309
282
  #realtimeReady = null;
310
283
  #ingestPromise = null;
311
- #queryPullLocks = new Map();
312
284
  #recordingIngestBillable = false;
313
285
  #ingestBreaker = new IngestCircuitBreaker({
314
286
  budgetRows: ingestBudgetRows,
@@ -384,8 +356,7 @@ export function createSyncDurableObject(config) {
384
356
  this.#ingestBreaker.restore(ingestBreakerReason, Number(this.#controlGet('ingestBreakerRetryAt')), Number(this.#controlGet('ingestBreakerTrips')));
385
357
  }
386
358
  this.#wasm(() => engine_init_schema(this.#engineDb, config.schema));
387
- if (config.queryAware || config.resolveQueries)
388
- this.#wasm(() => engine_init_query_schema(this.#engineDb));
359
+ this.#wasm(() => engine_init_query_schema(this.#engineDb));
389
360
  });
390
361
  });
391
362
  }
@@ -402,22 +373,6 @@ export function createSyncDurableObject(config) {
402
373
  this.#counters.wasmBoundaryCalls++;
403
374
  return call();
404
375
  }
405
- async #acquireQueryPullLock(clientGroupID) {
406
- const previous = this.#queryPullLocks.get(clientGroupID);
407
- let release;
408
- const current = new Promise((resolve) => {
409
- release = resolve;
410
- });
411
- this.#queryPullLocks.set(clientGroupID, current);
412
- if (previous)
413
- await previous;
414
- return () => {
415
- release();
416
- if (this.#queryPullLocks.get(clientGroupID) === current) {
417
- this.#queryPullLocks.delete(clientGroupID);
418
- }
419
- };
420
- }
421
376
  #simulateIdleTeardown(now) {
422
377
  if (this.#lastRequestAt > 0 && now - this.#lastRequestAt >= idleTeardownMs) {
423
378
  this.#bootID = crypto.randomUUID();
@@ -438,37 +393,10 @@ export function createSyncDurableObject(config) {
438
393
  }
439
394
  this.#lastRequestAt = now;
440
395
  }
441
- #visibility(claims) {
442
- if (!config.visibility || !this.#visibilityEnabled())
443
- return null;
444
- return {
445
- rowLocal: typeof config.visibility.rowLocal === 'function'
446
- ? config.visibility.rowLocal(claims)
447
- : config.visibility.rowLocal,
448
- filters: Object.keys(config.schema.tables).flatMap((table) => {
449
- const filter = config.visibility?.filter(table, claims);
450
- return filter
451
- ? [
452
- filter.kind === 'expression'
453
- ? { kind: 'expression', table, expression: filter.expression }
454
- : {
455
- kind: 'raw',
456
- table,
457
- sql: filter.sql,
458
- params: [...(filter.params ?? [])],
459
- },
460
- ]
461
- : [];
462
- }),
463
- };
464
- }
465
- // the admin-set namespace knobs (writer, visibility, query-aware,
466
- // retention) live in _zsync_host_control, NOT in instance fields: a real
467
- // eviction recreates this instance, and an in-memory override silently
468
- // reverting to the config default mid-run turns a query-aware namespace
469
- // back into a baseline one — the client keeps re-sending its desired-query
470
- // patch, the host ignores it, and every pull answers {unchanged:true}
471
- // forever.
396
+ // the admin-set namespace knobs (writer, retention) live in
397
+ // _zsync_host_control, NOT in instance fields: a real eviction recreates
398
+ // this instance, and an in-memory override silently reverting to the
399
+ // config default mid-run changes namespace behavior under the client.
472
400
  #controlGet(key) {
473
401
  const row = this.#directSql.query('SELECT value FROM _zsync_host_control WHERE key = ?', [key])[0];
474
402
  return row?.value ?? null;
@@ -500,14 +428,6 @@ export function createSyncDurableObject(config) {
500
428
  #writerEnabled() {
501
429
  return this.#controlGet('writerEnabled') === '1';
502
430
  }
503
- #visibilityEnabled() {
504
- const value = this.#controlGet('visibilityEnabled');
505
- return value === null ? (config.visibilityEnabled ?? false) : value === '1';
506
- }
507
- #queryAwareOverride() {
508
- const value = this.#controlGet('queryAwareOverride');
509
- return value === null ? null : value === '1';
510
- }
511
431
  #retainChanges() {
512
432
  return this.#controlGet('retainChanges') ?? defaultRetainChanges;
513
433
  }
@@ -997,83 +917,47 @@ export function createSyncDurableObject(config) {
997
917
  let body;
998
918
  try {
999
919
  body = await requestObject(request);
1000
- const queryAware = this.#queryAwareOverride() ??
1001
- (typeof config.queryAware === 'function'
1002
- ? config.queryAware(claims)
1003
- : (config.queryAware ?? Boolean(config.resolveQueries)));
1004
- const transformVersion = queryAware
1005
- ? typeof config.queryTransformVersion === 'function'
1006
- ? config.queryTransformVersion(claims)
1007
- : (config.queryTransformVersion ?? 0)
1008
- : 0;
920
+ const transformVersion = typeof config.queryTransformVersion === 'function'
921
+ ? config.queryTransformVersion(claims)
922
+ : (config.queryTransformVersion ?? 0);
1009
923
  if (!Number.isSafeInteger(transformVersion) || transformVersion < 0) {
1010
924
  throw new TypeError('queryTransformVersion must be a non-negative safe integer');
1011
925
  }
1012
- // The group lock exists to keep two desired-query patches from being
1013
- // applied out of arrival order when one of them is slow to resolve.
1014
- // A pull carrying no patch applies nothing, and it reads membership
1015
- // inside transactionSync, which is already atomic against whichever
1016
- // patch commits around it. Holding the lock for those pulls only makes
1017
- // an unchanged warm pull wait out somebody else's query-resolution round
1018
- // trip to the application, which is the head-of-line blocking behind
1019
- // the pull tail: a client polls far more often than it changes what it
1020
- // wants, so almost every pull in flight is patch-free.
1021
- const releaseQueryPull = queryAware && config.resolveQueries && body.queries
1022
- ? await this.#acquireQueryPullLock(typeof body.clientGroupID === 'string' ? body.clientGroupID : '')
1023
- : null;
1024
926
  let response;
1025
- try {
1026
- if (queryAware && body.queries) {
1027
- const queries = body.queries;
1028
- if (Array.isArray(queries.patch)) {
1029
- // matches the browser host: a query-aware namespace with no
1030
- // resolver refuses the patch (400) instead of crashing (500).
1031
- // #queryAwareOverride can force queryAware on, so this is
1032
- // reachable even though validateSyncHostConfig passes.
1033
- if (!config.resolveQueries) {
1034
- throw requestError('query put requires a server-resolved named query');
1035
- }
1036
- // resolveQueries may be async and needs `env` (a consumer can
1037
- // delegate the transform to its app's real synced-queries
1038
- // endpoint over an app service binding — authenticate runs in the
1039
- // worker isolate, but the query loop runs here in the DO, so the
1040
- // binding must come from the DO's own env, not a shared global).
1041
- const patch = await resolveQueryPatch(queries.patch, (requests) => config.resolveQueries(requests, claims, this.env), transformVersion, requestError);
1042
- body = { ...body, queries: { ...queries, patch } };
1043
- }
1044
- }
1045
- if (queryAware) {
1046
- body = { ...body, _serverQueryTransformVersion: transformVersion };
927
+ {
928
+ const queries = body.queries;
929
+ if (queries && Array.isArray(queries.patch)) {
930
+ // named queries resolve in-process against the app's ordinary
931
+ // Zero registry, synchronously: patch application order is
932
+ // arrival order by construction, with no cross-request await
933
+ // between resolution and the engine transaction.
934
+ const patch = resolveQueryPatch(queries.patch, config.queries, claims, transformVersion, requestError);
935
+ body = { ...body, queries: { ...queries, patch } };
1047
936
  }
937
+ body = { ...body, _serverQueryTransformVersion: transformVersion };
1048
938
  const clientID = typeof body.clientID === 'string' ? body.clientID : '';
1049
939
  this.#pulling.add(clientID);
1050
940
  try {
1051
941
  const txStarted = performance.now();
1052
942
  const duringFault = this.#takeFault('pull_during_tx');
1053
943
  response = this.ctx.storage.transactionSync(() => {
1054
- const result = this.#wasm(() => queryAware
1055
- ? engine_handle_query_pull(this.#engineDb, config.schema, this.#retainChanges(), body, claims.userID)
1056
- : engine_handle_pull(this.#engineDb, config.schema, this.#visibility(claims), caps, this.#retainChanges(), body, claims.userID));
944
+ const result = this.#wasm(() => engine_handle_query_pull(this.#engineDb, config.schema, this.#retainChanges(), body, claims.userID));
1057
945
  if (duringFault)
1058
946
  throw this.#faultError(duringFault, 'pull_during_tx');
1059
947
  return result;
1060
948
  });
1061
949
  transactionMs = performance.now() - txStarted;
1062
- response = withGotQueriesAck(body, queryAware, response);
1063
950
  }
1064
951
  finally {
1065
952
  this.#pulling.delete(clientID);
1066
953
  }
1067
954
  }
1068
- finally {
1069
- releaseQueryPull?.();
1070
- }
1071
955
  const afterPullFault = this.#takeFault('pull_after_commit');
1072
956
  if (afterPullFault)
1073
957
  throw this.#faultError(afterPullFault, 'pull_after_commit');
1074
958
  const patch = Array.isArray(response.rowsPatch) ? response.rowsPatch : [];
1075
959
  const queriesBody = body.queries;
1076
- const queryPuts = queryAware && Array.isArray(queriesBody?.patch)
960
+ const queryPuts = Array.isArray(queriesBody?.patch)
1077
961
  ? queriesBody.patch.filter((entry) => entry?.op === 'put').length
1078
962
  : 0;
1079
963
  this.#counters.queryRecompilations += queryPuts;
@@ -1519,20 +1403,6 @@ export function createSyncDurableObject(config) {
1519
1403
  this.ctx.abort('admin requested durable object restart');
1520
1404
  return json({ ok: true, bootID: this.#bootID });
1521
1405
  }
1522
- if (route === '/admin/visibility') {
1523
- return request.json().then((body) => {
1524
- const enabled = Boolean(body.enabled);
1525
- this.#controlSet('visibilityEnabled', enabled ? '1' : '0');
1526
- return json({ ok: true, enabled });
1527
- });
1528
- }
1529
- if (route === '/admin/query-aware') {
1530
- return request.json().then((body) => {
1531
- const enabled = Boolean(body.enabled);
1532
- this.#controlSet('queryAwareOverride', enabled ? '1' : '0');
1533
- return json({ ok: true, enabled });
1534
- });
1535
- }
1536
1406
  if (route === '/admin/retention') {
1537
1407
  return request
1538
1408
  .json()
package/dist/index.d.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  export { createSyncDurableObject, createSyncWorker } from './host.js';
2
2
  export { createQueryCompiler } from './query-compiler.js';
3
3
  export { resolveQueryPatch } from './query-patch.js';
4
- export { visibility } from './types.js';
5
4
  export { DEFAULT_TRANSACTION_QUERY_BUDGET, TransactionQueryBudgetError, executeTransactionQueryPlan, executeTransactionQueryPlanAsync, } from './transaction-query.js';
6
- export type { PullCaps, QueryResolution, QueryResolutionRequest, QueryResolver, SyncHostConfig, SyncHostEnv, ServiceBinding, SyncSql, UpstreamConfig, VisibilityConfig, VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue, } from './types.js';
5
+ export type { SyncHostConfig, SyncHostEnv, ServiceBinding, SyncSql, UpstreamConfig, } from './types.js';
7
6
  export type { SyncDurableObjectConstructor } from './host.js';
8
7
  export type { CompiledTransactionQueryNode, CompiledTransactionQueryPlan, CompiledTransactionQueryRelationship, TransactionQueryBinding, TransactionQueryBudget, TransactionQueryColumn, TransactionQueryColumnType, TransactionQueryExecutionOptions, TransactionQueryWireValue, } from './transaction-query.js';
9
8
  export type { TransactionQueryCompiler } from './query-compiler.js';
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  export { createSyncDurableObject, createSyncWorker } from './host.js';
2
2
  export { createQueryCompiler } from './query-compiler.js';
3
3
  export { resolveQueryPatch } from './query-patch.js';
4
- export { visibility } from './types.js';
5
4
  export { DEFAULT_TRANSACTION_QUERY_BUDGET, TransactionQueryBudgetError, executeTransactionQueryPlan, executeTransactionQueryPlanAsync, } from './transaction-query.js';
@@ -1,30 +1,21 @@
1
- import type { JsonValue } from 'orez-sync-executor';
2
- export type QueryResolutionRequest = {
3
- readonly name: string;
4
- readonly args: readonly JsonValue[];
5
- };
6
- /**
7
- * One resolved query, positionally matched to the request at the same index.
8
- *
9
- * An `error` fails the whole patch (the pull is refused as one unit), but the
10
- * refusal names the query it came from and carries its resolver error, so a
11
- * client that registers one unknown query alongside nine good ones is told
12
- * which one is unknown.
13
- */
14
- export type QueryResolution = {
15
- readonly ast: JsonValue;
16
- } | {
17
- readonly error: string;
18
- };
1
+ import type { AnyQueryRegistry } from '@rocicorp/zero';
19
2
  /**
20
3
  * Turn a client's desired-query patch into the resolved patch sync-core takes.
21
4
  *
22
- * Both hosts share this because both must agree on exactly one thing: how many
23
- * times a consumer's resolver is called for one patch. Resolving per query made
24
- * a pull cost one application round trip per registered query, each one
25
- * re-authenticating, so a screen mounting eleven views paid eleven of them in
26
- * series. Everything else here exists to make the single call safe: the patch
27
- * keeps its order, duplicate (name, args) pairs collapse to one request and fan
28
- * back out, and a resolver error names the query it came from.
5
+ * Every named query resolves in-process against the app's ordinary Zero query
6
+ * registry — the same object the client was built with. `CustomQuery.fn`
7
+ * validates the arguments and applies the query context, so this is exactly
8
+ * the transform zero-cache delegates to an app endpoint, minus the network.
9
+ * The context is the host's authenticated identity (claims on the CF host,
10
+ * authData on the browser host); a query definition scopes its rows from it.
11
+ *
12
+ * Resolution is structural on purpose: the registry is walked as a plain
13
+ * object and the built query's `ast` is read directly, never through Zero's
14
+ * `asQueryInternals`, whose instance-tag check fails whenever the app's
15
+ * registry and this host resolve different copies of the zero module.
16
+ *
17
+ * A resolution error fails the whole patch (the pull is refused as one unit),
18
+ * but the refusal names the query it came from, so a client that registers
19
+ * one unknown query alongside nine good ones is told which one is unknown.
29
20
  */
30
- export declare function resolveQueryPatch(patch: readonly unknown[], resolve: (requests: readonly QueryResolutionRequest[]) => readonly QueryResolution[] | Promise<readonly QueryResolution[]>, transformVersion: number, fail: (message: string) => Error): Promise<unknown[]>;
21
+ export declare function resolveQueryPatch(patch: readonly unknown[], queries: AnyQueryRegistry, context: unknown, transformVersion: number, fail: (message: string) => Error): unknown[];
@@ -1,18 +1,26 @@
1
+ // Zero separates registry path segments with '.' or '|' (its getValueAtPath).
2
+ const NAME_SEPARATOR = /[.|]/;
1
3
  /**
2
4
  * Turn a client's desired-query patch into the resolved patch sync-core takes.
3
5
  *
4
- * Both hosts share this because both must agree on exactly one thing: how many
5
- * times a consumer's resolver is called for one patch. Resolving per query made
6
- * a pull cost one application round trip per registered query, each one
7
- * re-authenticating, so a screen mounting eleven views paid eleven of them in
8
- * series. Everything else here exists to make the single call safe: the patch
9
- * keeps its order, duplicate (name, args) pairs collapse to one request and fan
10
- * back out, and a resolver error names the query it came from.
6
+ * Every named query resolves in-process against the app's ordinary Zero query
7
+ * registry — the same object the client was built with. `CustomQuery.fn`
8
+ * validates the arguments and applies the query context, so this is exactly
9
+ * the transform zero-cache delegates to an app endpoint, minus the network.
10
+ * The context is the host's authenticated identity (claims on the CF host,
11
+ * authData on the browser host); a query definition scopes its rows from it.
12
+ *
13
+ * Resolution is structural on purpose: the registry is walked as a plain
14
+ * object and the built query's `ast` is read directly, never through Zero's
15
+ * `asQueryInternals`, whose instance-tag check fails whenever the app's
16
+ * registry and this host resolve different copies of the zero module.
17
+ *
18
+ * A resolution error fails the whole patch (the pull is refused as one unit),
19
+ * but the refusal names the query it came from, so a client that registers
20
+ * one unknown query alongside nine good ones is told which one is unknown.
11
21
  */
12
- export async function resolveQueryPatch(patch, resolve, transformVersion, fail) {
13
- const puts = [];
14
- const requests = [];
15
- const requestByKey = new Map();
22
+ export function resolveQueryPatch(patch, queries, context, transformVersion, fail) {
23
+ const result = [...patch];
16
24
  patch.forEach((operation, index) => {
17
25
  if (!operation || typeof operation !== 'object')
18
26
  return;
@@ -24,34 +32,38 @@ export async function resolveQueryPatch(patch, resolve, transformVersion, fail)
24
32
  }
25
33
  if (!Array.isArray(op.args))
26
34
  throw fail('named query args must be an array');
27
- const key = JSON.stringify([op.name, op.args]);
28
- if (!requestByKey.has(key)) {
29
- requestByKey.set(key, requests.length);
30
- requests.push({ name: op.name, args: op.args });
35
+ let node = queries;
36
+ for (const segment of op.name.split(NAME_SEPARATOR)) {
37
+ node =
38
+ node && typeof node === 'object'
39
+ ? node[segment]
40
+ : undefined;
31
41
  }
32
- puts.push({ index, key, name: op.name, hash: op.hash });
33
- });
34
- if (puts.length === 0)
35
- return [...patch];
36
- const resolved = await resolve(requests);
37
- // broken positional arity is a host-side resolver bug, not a client error,
38
- // so it deliberately bypasses fail() and surfaces as a 500
39
- if (resolved.length !== requests.length) {
40
- throw new Error(`query resolver returned ${resolved.length} results for ${requests.length} queries`);
41
- }
42
- const result = [...patch];
43
- for (const put of puts) {
44
- const resolution = resolved[requestByKey.get(put.key)];
45
- if (!resolution || 'error' in resolution) {
46
- const detail = resolution && 'error' in resolution ? `: ${resolution.error}` : '';
47
- throw fail(`unknown or unsupported named query: ${put.name}${detail}`);
42
+ const custom = node;
43
+ if (!custom || typeof custom.fn !== 'function') {
44
+ throw fail(`unknown or unsupported named query: ${op.name}`);
45
+ }
46
+ let ast;
47
+ try {
48
+ const built = custom.fn({
49
+ args: op.args[0],
50
+ ctx: context,
51
+ });
52
+ if (!built || typeof built !== 'object' || built.ast === undefined) {
53
+ throw new Error('query did not build to an AST-bearing Zero query');
54
+ }
55
+ ast = built.ast;
48
56
  }
49
- result[put.index] = {
57
+ catch (error) {
58
+ const detail = error instanceof Error ? error.message : String(error);
59
+ throw fail(`unknown or unsupported named query: ${op.name}: ${detail}`);
60
+ }
61
+ result[index] = {
50
62
  op: 'put',
51
- hash: put.hash,
52
- ast: resolution.ast,
63
+ hash: op.hash,
64
+ ast,
53
65
  transformVersion,
54
66
  };
55
- }
67
+ });
56
68
  return result;
57
69
  }
package/dist/types.d.ts CHANGED
@@ -1,33 +1,11 @@
1
- import type { QueryResolution, QueryResolutionRequest } from './query-patch.js';
2
1
  import type { TransactionQueryBudget } from './transaction-query.js';
3
- import type { Schema } from '@rocicorp/zero';
4
- import type { ExecResult, MutatorRegistry, NormalizedClaims, SqlStatementMetadata, VisibilityConfig } from 'orez-sync-executor';
2
+ import type { AnyQueryRegistry, Schema } from '@rocicorp/zero';
3
+ import type { ExecResult, MutatorRegistry, NormalizedClaims, SqlStatementMetadata } from 'orez-sync-executor';
5
4
  import type { StreamingManifest } from 'orez-sync-executor/realtime';
6
- export { visibility, type VisibilityExpression, type VisibilityFilter, type VisibilityOperand, type VisibilityValue, } from './visibility.js';
7
- export type { VisibilityConfig } from 'orez-sync-executor';
8
- export type { QueryResolution, QueryResolutionRequest } from './query-patch.js';
9
5
  export interface SyncSql {
10
6
  exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): ExecResult;
11
7
  query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Row[];
12
8
  }
13
- /**
14
- * Resolve a whole desired-query patch in one call.
15
- *
16
- * This is a batch contract because it is nearly always a network call: a
17
- * consumer delegates the transform to its application, which authenticates and
18
- * answers over a service binding. Resolving one query per call made a pull's
19
- * cost linear in the number of queries a screen registers, and each round trip
20
- * re-authenticated. Measured against production with a captured mobile client's
21
- * desired-query set: an 11-query pull took 23.3 s against 2.0 s for the same
22
- * pull carrying no queries.
23
- *
24
- * Implementations must return exactly one entry per request, in request order.
25
- */
26
- export type QueryResolver = (requests: readonly QueryResolutionRequest[], claims: NormalizedClaims, env: SyncHostEnv) => readonly QueryResolution[] | Promise<readonly QueryResolution[]>;
27
- export type PullCaps = {
28
- maxChangeRows: number;
29
- maxChangeBytes: number;
30
- };
31
9
  export interface SyncHostEnv {
32
10
  SYNC_DO: DurableObjectNamespace;
33
11
  ADMIN_KEY?: string;
@@ -109,12 +87,14 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv, S extends Sche
109
87
  authorizeNotify(request: Request, env: Env): boolean | Promise<boolean>;
110
88
  /** Resolve the first path component or another consumer-defined namespace. */
111
89
  namespace(request: Request): string | null;
112
- visibility?: VisibilityConfig;
113
- /** Enable desired-query pulls for this namespace and resolve named queries
114
- * into validated Zero ASTs before they reach sync-core. */
115
- queryAware?: boolean | ((claims: NormalizedClaims) => boolean);
116
- /** Resolve every named query in one desired-query patch, in one call. */
117
- resolveQueries?: QueryResolver;
90
+ /**
91
+ * The app's ordinary Zero query registry (the `defineQueries` result the
92
+ * client is built with). The host resolves every desired named query
93
+ * in-process against it — argument validation and context scoping run in
94
+ * the query definitions themselves, with the authenticated claims as the
95
+ * query context. There is no app endpoint to call and nothing else to wire.
96
+ */
97
+ queries: AnyQueryRegistry;
118
98
  /**
119
99
  * Streaming fields for this namespace: which columns may carry a live,
120
100
  * uncommitted value, and their publish mode and rate bounds.
@@ -138,10 +118,7 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv, S extends Sche
138
118
  authorizeProduce?: (request: Request, env: Env) => boolean | Promise<boolean>;
139
119
  /** Server-owned invalidation epoch for permission/schema transforms. */
140
120
  queryTransformVersion?: number | ((claims: NormalizedClaims) => number);
141
- /** Enable consumer visibility from the first request. Defaults to false for harnesses. */
142
- visibilityEnabled?: boolean;
143
121
  retainChanges?: number;
144
- caps?: Partial<PullCaps>;
145
122
  idleTeardownMs?: number;
146
123
  wakeCoalesceMs?: number;
147
124
  /** per-query guard for recursive transaction query materialization. */
package/dist/types.js CHANGED
@@ -1 +1 @@
1
- export { visibility, } from './visibility.js';
1
+ export {};
package/dist/wasm.d.ts CHANGED
@@ -8,7 +8,6 @@ export declare const engine_begin_snapshot_generation: (db: any, schema: any, st
8
8
  export declare const engine_compile_query: (schema: any, ast: any, format: any) => any;
9
9
  export declare const engine_finalize: (db: any, client_group_id: string, client_id: string, mutation_id: string) => void;
10
10
  export declare const engine_finalize_snapshot_generation: (db: any, schema: any, generation: string, watermark: string) => any;
11
- export declare const engine_handle_pull: (db: any, schema: any, visibility: any, caps: any, retain_changes: string, body: any, user_id: string) => any;
12
11
  export declare const engine_handle_query_pull: (db: any, schema: any, retain_changes: string, body: any, user_id: string) => any;
13
12
  export declare const engine_init_query_schema: (db: any) => void;
14
13
  export declare const engine_init_schema: (db: any, schema: any) => void;
package/dist/wasm.js CHANGED
@@ -25,7 +25,6 @@ export const engine_begin_snapshot_generation = withWasm(generated.engine_begin_
25
25
  export const engine_compile_query = withWasm(generated.engine_compile_query);
26
26
  export const engine_finalize = withWasm(generated.engine_finalize);
27
27
  export const engine_finalize_snapshot_generation = withWasm(generated.engine_finalize_snapshot_generation);
28
- export const engine_handle_pull = withWasm(generated.engine_handle_pull);
29
28
  export const engine_handle_query_pull = withWasm(generated.engine_handle_query_pull);
30
29
  export const engine_init_query_schema = withWasm(generated.engine_init_query_schema);
31
30
  export const engine_init_schema = withWasm(generated.engine_init_schema);
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "orez-sync-cf-host",
3
- "version": "0.11.7",
3
+ "version": "0.12.0",
4
+ "description": "Internal orez Cloudflare sync host. Do not import directly: consumers use orez-lite/cloudflare/sync.",
4
5
  "type": "module",
5
6
  "exports": {
6
7
  ".": {
@@ -27,10 +28,6 @@
27
28
  "types": "./dist/transaction-query.d.ts",
28
29
  "import": "./dist/transaction-query.js"
29
30
  },
30
- "./visibility": {
31
- "types": "./dist/visibility.d.ts",
32
- "import": "./dist/visibility.js"
33
- },
34
31
  "./vite-wasm-loader": {
35
32
  "types": "./dist/vite-wasm-loader.d.ts",
36
33
  "import": "./dist/vite-wasm-loader.js"
@@ -49,7 +46,7 @@
49
46
  "build:dist:platform": "bun run build:dependencies && bun run build:wasm:platform && bun run build:runtime",
50
47
  "bundle": "bun run build:dist && wrangler deploy --dry-run --outdir .wrangler/bundle",
51
48
  "measure": "bun run build:dist && bun measure.mjs",
52
- "test": "bun run test:config && bun run test:query-compiler && bun run test:package && bun run test:platform && bun run test:integration && bun run test:ingest && bun run test:restart",
49
+ "test": "bun run test:config && bun run test:query-compiler && bun run test:package && bun run test:platform && bun run test:integration && bun run test:large-data && bun run test:ingest && bun run test:restart",
53
50
  "test:config": "bun test config.test.mjs write-safeguards.test.mjs transaction-query.test.mjs",
54
51
  "test:integration": "bun run build:dist && bun integration-test.mjs",
55
52
  "test:ingest": "bun run build:dist && bun ingest-test.mjs",
@@ -57,10 +54,11 @@
57
54
  "test:query-compiler": "bun run build:dist && bun test --preload orez-sync-cf-host/bun-wasm-loader query-compiler.test.mjs && node vite-ssr-query-compiler-test.mjs",
58
55
  "test:restart": "bun run build:dist && bun restart-test.mjs",
59
56
  "test:platform": "bun run build:dist:platform && bun platform-test.mjs",
60
- "typecheck": "bun run build:dist:platform && tsc --noEmit"
57
+ "typecheck": "bun run build:dist:platform && tsc --noEmit",
58
+ "test:large-data": "bun run build:dist && bun large-data-test.mjs"
61
59
  },
62
60
  "dependencies": {
63
- "orez-sync-executor": "0.11.7"
61
+ "orez-sync-executor": "0.12.0"
64
62
  },
65
63
  "devDependencies": {
66
64
  "@cloudflare/workers-types": "4.20260617.1",
@@ -1,12 +0,0 @@
1
- import type { VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue } from 'orez-sync-executor';
2
- export type { VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue, } from 'orez-sync-executor';
3
- export declare const visibility: {
4
- readonly column: (table: string, column: string, qualifier?: string) => VisibilityOperand;
5
- readonly value: (value: VisibilityValue) => VisibilityOperand;
6
- readonly comparison: (left: VisibilityOperand, operator: "=" | "!=" | "<" | ">" | "<=" | ">=" | "IS" | "IS NOT", right: VisibilityOperand) => VisibilityExpression;
7
- readonly and: (...conditions: VisibilityExpression[]) => VisibilityExpression;
8
- readonly or: (...conditions: VisibilityExpression[]) => VisibilityExpression;
9
- readonly exists: (table: string, where: VisibilityExpression, qualifier?: string) => VisibilityExpression;
10
- readonly filter: (expression: VisibilityExpression) => VisibilityFilter;
11
- readonly raw: (sql: string, params?: readonly VisibilityValue[]) => VisibilityFilter;
12
- };
@@ -1,30 +0,0 @@
1
- export const visibility = {
2
- column(table, column, qualifier) {
3
- return qualifier
4
- ? { type: 'column', table, column, qualifier }
5
- : { type: 'column', table, column };
6
- },
7
- value(value) {
8
- return { type: 'value', value };
9
- },
10
- comparison(left, operator, right) {
11
- return { type: 'comparison', operator, left, right };
12
- },
13
- and(...conditions) {
14
- return { type: 'and', conditions };
15
- },
16
- or(...conditions) {
17
- return { type: 'or', conditions };
18
- },
19
- exists(table, where, qualifier) {
20
- return qualifier
21
- ? { type: 'exists', table, qualifier, where }
22
- : { type: 'exists', table, where };
23
- },
24
- filter(expression) {
25
- return { kind: 'expression', expression };
26
- },
27
- raw(sql, params = []) {
28
- return { kind: 'raw', sql, params };
29
- },
30
- };