orez-sync-cf-host 0.16.19 → 0.16.20

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
@@ -144,6 +144,12 @@ Pass a mint callback to the low-level HTTP transport. It calls `getToken()` for
144
144
  every socket attempt, including reconnects, so short-lived tokens are never
145
145
  reused after the wake connection drops:
146
146
 
147
+ Calling `ensureHttpPullTransport` again with a custom token provider replaces
148
+ that provider for future wake attempts while retaining the transport and
149
+ WebSocket shim. Module reloads can recreate the callback without chaining
150
+ transports. Switching between custom minting, auth-token wake and disabled wake,
151
+ or changing transport routing or timing, remains a configuration conflict.
152
+
147
153
  ```ts
148
154
  import { ensureHttpPullTransport } from 'orez-lite/client'
149
155
 
@@ -115,6 +115,14 @@ export function engine_state(db: any): any;
115
115
 
116
116
  export function engine_version(): string;
117
117
 
118
+ /**
119
+ * The clients a wake after an ingest should reach: every client whose group
120
+ * can see a row committed after `since`. `{ all: true }` means wake every
121
+ * socket; otherwise `clientIDs` lists the clients to wake. Call it with a
122
+ * transaction open, after the ingest that advanced past `since`.
123
+ */
124
+ export function engine_wake_targets(db: any, schema: any, since: string): any;
125
+
118
126
  export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
119
127
 
120
128
  export interface InitOutput {
@@ -142,6 +150,7 @@ export interface InitOutput {
142
150
  readonly engine_schema_revision: () => [number, number];
143
151
  readonly engine_state: (a: any) => [number, number, number];
144
152
  readonly engine_version: () => [number, number];
153
+ readonly engine_wake_targets: (a: any, b: any, c: number, d: number) => [number, number, number];
145
154
  readonly __wbindgen_malloc: (a: number, b: number) => number;
146
155
  readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
147
156
  readonly __wbindgen_exn_store: (a: number) => void;
@@ -397,6 +397,26 @@ export function engine_version() {
397
397
  wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
398
398
  }
399
399
  }
400
+
401
+ /**
402
+ * The clients a wake after an ingest should reach: every client whose group
403
+ * can see a row committed after `since`. `{ all: true }` means wake every
404
+ * socket; otherwise `clientIDs` lists the clients to wake. Call it with a
405
+ * transaction open, after the ingest that advanced past `since`.
406
+ * @param {any} db
407
+ * @param {any} schema
408
+ * @param {string} since
409
+ * @returns {any}
410
+ */
411
+ export function engine_wake_targets(db, schema, since) {
412
+ const ptr0 = passStringToWasm0(since, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
413
+ const len0 = WASM_VECTOR_LEN;
414
+ const ret = wasm.engine_wake_targets(db, schema, ptr0, len0);
415
+ if (ret[2]) {
416
+ throw takeFromExternrefTable0(ret[1]);
417
+ }
418
+ return takeFromExternrefTable0(ret[0]);
419
+ }
400
420
  function __wbg_get_imports() {
401
421
  const import0 = {
402
422
  __proto__: null,
@@ -395,6 +395,26 @@ export function engine_version() {
395
395
  wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
396
396
  }
397
397
  }
398
+
399
+ /**
400
+ * The clients a wake after an ingest should reach: every client whose group
401
+ * can see a row committed after `since`. `{ all: true }` means wake every
402
+ * socket; otherwise `clientIDs` lists the clients to wake. Call it with a
403
+ * transaction open, after the ingest that advanced past `since`.
404
+ * @param {any} db
405
+ * @param {any} schema
406
+ * @param {string} since
407
+ * @returns {any}
408
+ */
409
+ export function engine_wake_targets(db, schema, since) {
410
+ const ptr0 = passStringToWasm0(since, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
411
+ const len0 = WASM_VECTOR_LEN;
412
+ const ret = wasm.engine_wake_targets(db, schema, ptr0, len0);
413
+ if (ret[2]) {
414
+ throw takeFromExternrefTable0(ret[1]);
415
+ }
416
+ return takeFromExternrefTable0(ret[0]);
417
+ }
398
418
  export function __wbg_Error_92b29b0548f8b746(arg0, arg1) {
399
419
  const ret = Error(getStringFromWasm0(arg0, arg1));
400
420
  return ret;
Binary file
package/dist/host.js CHANGED
@@ -7,7 +7,7 @@ import { resolveQueryPatch } from './query-patch.js';
7
7
  import { ServingLagTracker } from './serving-lag.js';
8
8
  import { decodeSqlParams, SqlStorageDirect, SqlStorageMutatorTransaction, SqlStorageSyncDb, } from './sql-storage-adapter.js';
9
9
  import { DEFAULT_UPSTREAM_MAX_RESPONSE_BYTES, DEFAULT_UPSTREAM_MAX_REQUEST_BYTES, DEFAULT_UPSTREAM_REQUEST_TIMEOUT_MS, fetchBoundedUpstreamJson, readBoundedJsonResponse, readBoundedStream, UpstreamResponseLimitError, } from './upstream-response.js';
10
- 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_schema_revision, engine_state, engine_version, } from './wasm.js';
10
+ 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_schema_revision, engine_state, engine_version, engine_wake_targets, } from './wasm.js';
11
11
  import { IngestBreakerError, IngestCircuitBreaker, retryDelayMs, shouldRetryDelegatedPush, } from './write-safeguards.js';
12
12
  const NAMESPACE_HEADER = 'x-orez-sync-namespace';
13
13
  const UPSTREAM_PATH_HEADER = 'x-orez-sync-upstream-path';
@@ -18,6 +18,7 @@ const WAKE_SUBSCRIBER_TAG = 'orez:wake-subscriber';
18
18
  // the worker always deletes from the incoming request before setting its own,
19
19
  // so a client cannot present one.
20
20
  const IDENTITY_HEADER = 'x-orez-sync-identity';
21
+ const WAKE_ISSUED_AT_HEADER = 'x-orez-wake-issued-at';
21
22
  const DEFAULT_SNAPSHOT_PAGE_ROWS = 2_000;
22
23
  const MIN_SNAPSHOT_PAGE_ROWS = 100;
23
24
  const WORKER_STAGE_TELEMETRY_SAMPLE_RATE = 0.01;
@@ -51,6 +52,14 @@ function upstreamBatchTables(batch) {
51
52
  }
52
53
  return tables;
53
54
  }
55
+ function isRealtimeIdentity(identity) {
56
+ return (typeof identity === 'object' &&
57
+ identity !== null &&
58
+ typeof identity.userID === 'string' &&
59
+ typeof identity.clientID === 'string' &&
60
+ typeof identity.clientGroupID === 'string' &&
61
+ identity.clientGroupID.length > 0);
62
+ }
54
63
  function freshCounters() {
55
64
  return {
56
65
  pulls: 0,
@@ -167,6 +176,14 @@ async function boundedRequestObject(request, maxBytes, timeoutMs) {
167
176
  }
168
177
  }
169
178
  function routeAfterNamespace(pathname) {
179
+ if (pathname.startsWith('/admin/') ||
180
+ pathname === '/wake' ||
181
+ pathname === '/notify' ||
182
+ pathname === '/pull' ||
183
+ pathname === '/push' ||
184
+ pathname.startsWith('/realtime/')) {
185
+ return pathname;
186
+ }
170
187
  const [, , ...parts] = pathname.split('/');
171
188
  return `/${parts.join('/')}`;
172
189
  }
@@ -324,6 +341,7 @@ export function createSyncWorker(config) {
324
341
  const sampled = Math.random() < WORKER_STAGE_TELEMETRY_SAMPLE_RATE;
325
342
  const isAdmin = route.startsWith('/admin/');
326
343
  let wakeUserID = null;
344
+ let wakeIssuedAt = null;
327
345
  if (route === '/wake') {
328
346
  let wakeRequest = request;
329
347
  const protocol = request.headers.get('sec-websocket-protocol')?.trim();
@@ -350,8 +368,13 @@ export function createSyncWorker(config) {
350
368
  const wake = await config.authorizeWake(wakeRequest, env);
351
369
  if (!wake)
352
370
  return json({ error: 'missing wake capability' }, 401);
353
- if (typeof wake === 'object')
371
+ if (typeof wake === 'object') {
354
372
  wakeUserID = wake.userID;
373
+ if (typeof wake.tokenIssuedAt === 'number' &&
374
+ Number.isFinite(wake.tokenIssuedAt)) {
375
+ wakeIssuedAt = wake.tokenIssuedAt;
376
+ }
377
+ }
355
378
  // A namespace that streams fields authorizes every subscription against
356
379
  // this userID, so a capability that does not carry one cannot open the
357
380
  // socket. Failing here names the cause; accepting it would produce a
@@ -377,6 +400,7 @@ export function createSyncWorker(config) {
377
400
  headers.delete(NAMESPACE_HEADER);
378
401
  headers.delete(UPSTREAM_PATH_HEADER);
379
402
  headers.delete(IDENTITY_HEADER);
403
+ headers.delete(WAKE_ISSUED_AT_HEADER);
380
404
  headers.delete(NOTIFY_IF_SUBSCRIBED_HEADER);
381
405
  let forwardedBody = null;
382
406
  // /wake and /realtime/produce are both websocket upgrades and have no
@@ -432,6 +456,8 @@ export function createSyncWorker(config) {
432
456
  // nothing gained by moving them here.
433
457
  if (wakeUserID)
434
458
  headers.set(IDENTITY_HEADER, encodeURIComponent(wakeUserID));
459
+ if (wakeIssuedAt !== null)
460
+ headers.set(WAKE_ISSUED_AT_HEADER, String(wakeIssuedAt));
435
461
  const hashedNamespace = await namespaceHash(namespace);
436
462
  headers.set(NAMESPACE_HEADER, hashedNamespace);
437
463
  try {
@@ -480,7 +506,7 @@ export function createSyncDurableObject(config) {
480
506
  validateSyncHostConfig(config);
481
507
  const compileQuery = createQueryCompiler(config.schema);
482
508
  const defaultRetainChanges = String(config.retainChanges ?? 4_096);
483
- const idleTeardownMs = config.idleTeardownMs ?? 5_000;
509
+ const idleTeardownMs = config.idleTeardownMs ?? null;
484
510
  // A CF fan-out wakes every client into an HTTP pull. Give concurrent writer
485
511
  // requests a real batching window so a storm burst creates one pull wave.
486
512
  const wakeCoalesceMs = config.wakeCoalesceMs ?? 25;
@@ -522,6 +548,7 @@ export function createSyncDurableObject(config) {
522
548
  #wakeOrigins = new Set();
523
549
  #wakeRecipients = new Set();
524
550
  #wakeTables = new Set();
551
+ #wakeUntargeted = false;
525
552
  #wakePromise = null;
526
553
  #lastWake = null;
527
554
  // Streaming fields. Null when the namespace configures no manifest, which
@@ -543,6 +570,7 @@ export function createSyncDurableObject(config) {
543
570
  });
544
571
  constructor(ctx, env) {
545
572
  super(ctx, env);
573
+ ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair('ping', 'pong'));
546
574
  const recordRowsWritten = (rows) => {
547
575
  this.#sqlBilling.rowsWritten += rows;
548
576
  if (this.#recordingIngestBillable)
@@ -644,6 +672,13 @@ export function createSyncDurableObject(config) {
644
672
  ]);
645
673
  this.#initSkipped = this.#controlGet('initFingerprint') === initFingerprint;
646
674
  if (!this.#initSkipped) {
675
+ this.#directSql.exec(`CREATE TABLE IF NOT EXISTS orez_revocations (
676
+ user_id TEXT NOT NULL,
677
+ revoked_at INTEGER NOT NULL
678
+ )`);
679
+ this.#directSql.exec('CREATE INDEX IF NOT EXISTS idx_orez_revocations_user_id ON orez_revocations(user_id, revoked_at)');
680
+ this.#directSql.exec('CREATE VIEW IF NOT EXISTS revocations AS SELECT user_id, revoked_at FROM orez_revocations');
681
+ this.#directSql.exec('CREATE TRIGGER IF NOT EXISTS trg_revocations_insert INSTEAD OF INSERT ON revocations BEGIN INSERT INTO orez_revocations(user_id, revoked_at) VALUES (NEW.user_id, NEW.revoked_at); END;');
647
682
  config.initialize(this.#directSql);
648
683
  this.#directSql.exec("INSERT OR IGNORE INTO _zsync_host_control (key, value) VALUES ('writerEnabled', '1')");
649
684
  this.#wasm(() => engine_init_schema(this.#engineDb, config.schema));
@@ -673,6 +708,8 @@ export function createSyncDurableObject(config) {
673
708
  return call();
674
709
  }
675
710
  #simulateIdleTeardown(now) {
711
+ if (idleTeardownMs === null)
712
+ return;
676
713
  if (this.#lastRequestAt > 0 && now - this.#lastRequestAt >= idleTeardownMs) {
677
714
  this.#bootID = crypto.randomUUID();
678
715
  this.#hibernations++;
@@ -682,6 +719,7 @@ export function createSyncDurableObject(config) {
682
719
  this.#wakeOrigins.clear();
683
720
  this.#wakeRecipients.clear();
684
721
  this.#wakeTables.clear();
722
+ this.#wakeUntargeted = false;
685
723
  this.#wakePromise = null;
686
724
  this.#lastWake = null;
687
725
  // Real hibernation reconstructs the object, so the hub and every
@@ -1226,7 +1264,10 @@ export function createSyncDurableObject(config) {
1226
1264
  }
1227
1265
  }
1228
1266
  if (snapshotCompleted || total > 0 || endingWatermark !== startingWatermark) {
1229
- await this.#enqueueWake('__upstream__', changedTables);
1267
+ // a completed snapshot replaced the replica wholesale: every client
1268
+ // re-pulls. otherwise wake only the clients these rows can reach.
1269
+ const targets = snapshotCompleted ? null : this.#wakeTargets(startingWatermark);
1270
+ await this.#enqueueWake('__upstream__', changedTables, targets);
1230
1271
  }
1231
1272
  return total;
1232
1273
  })().finally(() => {
@@ -1302,11 +1343,33 @@ export function createSyncDurableObject(config) {
1302
1343
  ...fields,
1303
1344
  }));
1304
1345
  }
1305
- #enqueueWake(originClientID, tables = []) {
1346
+ // the clients a batch committed after `since` can reach, or null for every
1347
+ // socket. a wake is advisory, so a targeting failure wakes everyone rather
1348
+ // than failing the ingest that serves the pull waiting on it.
1349
+ #wakeTargets(since) {
1350
+ try {
1351
+ const targets = this.ctx.storage.transactionSync(() => this.#wasm(() => engine_wake_targets(this.#engineDb, config.schema, since)));
1352
+ return targets.all ? null : new Set(targets.clientIDs);
1353
+ }
1354
+ catch (error) {
1355
+ console.error(JSON.stringify({
1356
+ event: 'sync_wake_target_error',
1357
+ hostVersion: config.hostVersion,
1358
+ error: errorMessage(error),
1359
+ }));
1360
+ return null;
1361
+ }
1362
+ }
1363
+ #enqueueWake(originClientID, tables = [], targets = null) {
1306
1364
  this.#wakeOrigins.add(originClientID);
1307
1365
  for (const table of tables)
1308
1366
  this.#wakeTables.add(table);
1309
- for (const socket of this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG)) {
1367
+ if (targets === null)
1368
+ this.#wakeUntargeted = true;
1369
+ const sockets = targets === null
1370
+ ? this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG)
1371
+ : [...targets].flatMap((clientID) => this.ctx.getWebSockets(`client:${clientID}`));
1372
+ for (const socket of sockets) {
1310
1373
  const attachment = socketAttachment(socket);
1311
1374
  if (!attachment ||
1312
1375
  attachment.clientID === originClientID ||
@@ -1329,6 +1392,8 @@ export function createSyncDurableObject(config) {
1329
1392
  this.#wakeRecipients = new Set();
1330
1393
  const tables = [...this.#wakeTables].sort();
1331
1394
  this.#wakeTables = new Set();
1395
+ const targeted = !this.#wakeUntargeted;
1396
+ this.#wakeUntargeted = false;
1332
1397
  this.#counters.wakeBatches++;
1333
1398
  let sent = 0;
1334
1399
  const sockets = this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG);
@@ -1346,6 +1411,7 @@ export function createSyncDurableObject(config) {
1346
1411
  const wakeStatus = {
1347
1412
  at: Date.now(),
1348
1413
  tables,
1414
+ targeted,
1349
1415
  socketCount: sockets.length,
1350
1416
  originCount: origins.size,
1351
1417
  sent,
@@ -1724,7 +1790,7 @@ export function createSyncDurableObject(config) {
1724
1790
  this.#realtimeConnections.set(socket, host.acceptProducer(socket, attachment.producerID));
1725
1791
  continue;
1726
1792
  }
1727
- if (!attachment.identity)
1793
+ if (!isRealtimeIdentity(attachment.identity))
1728
1794
  continue;
1729
1795
  subscribers.push({
1730
1796
  socket,
@@ -1760,19 +1826,31 @@ export function createSyncDurableObject(config) {
1760
1826
  const clientID = params.get('clientID');
1761
1827
  if (!clientID)
1762
1828
  return json({ error: 'clientID is required' }, 400);
1829
+ const encodedUserID = request.headers.get(IDENTITY_HEADER);
1830
+ const rawIssuedAt = request.headers.get(WAKE_ISSUED_AT_HEADER);
1831
+ const userID = encodedUserID ? decodeURIComponent(encodedUserID) : null;
1832
+ if (userID) {
1833
+ const rows = this.#directSql.query('SELECT MAX(revoked_at) AS revoked_at FROM orez_revocations WHERE user_id = ?', [userID]);
1834
+ const latestRevocation = rows[0]?.revoked_at;
1835
+ if (typeof latestRevocation === 'number' && Number.isFinite(latestRevocation)) {
1836
+ const tokenIssuedAt = rawIssuedAt !== null ? Number(rawIssuedAt) : -Infinity;
1837
+ if (Number.isNaN(tokenIssuedAt) || tokenIssuedAt < latestRevocation) {
1838
+ return json({ error: 'revoked wake capability' }, 401);
1839
+ }
1840
+ }
1841
+ }
1763
1842
  const pair = new WebSocketPair();
1764
1843
  const [client, server] = Object.values(pair);
1765
1844
  // The userID is the worker's, taken from the authenticated request. The
1766
1845
  // group is the client's own claim, and stays a claim: every subscription
1767
1846
  // is checked against this userID before a row is read, so asserting
1768
1847
  // someone else's group buys nothing.
1769
- const encodedUserID = request.headers.get(IDENTITY_HEADER);
1770
1848
  const clientGroupID = params.get('clientGroupID');
1771
- const identity = encodedUserID && clientGroupID
1849
+ const identity = encodedUserID
1772
1850
  ? {
1773
1851
  userID: decodeURIComponent(encodedUserID),
1774
1852
  clientID,
1775
- clientGroupID,
1853
+ ...(clientGroupID ? { clientGroupID } : {}),
1776
1854
  }
1777
1855
  : undefined;
1778
1856
  server.serializeAttachment({
@@ -1780,7 +1858,7 @@ export function createSyncDurableObject(config) {
1780
1858
  identity,
1781
1859
  });
1782
1860
  this.ctx.acceptWebSocket(server, [WAKE_SUBSCRIBER_TAG, `client:${clientID}`]);
1783
- if (identity) {
1861
+ if (isRealtimeIdentity(identity)) {
1784
1862
  const host = this.#realtimeHost();
1785
1863
  if (host) {
1786
1864
  this.#realtimeConnections.set(server, host.acceptSubscriber(server, identity, clientID));
@@ -1802,6 +1880,47 @@ export function createSyncDurableObject(config) {
1802
1880
  #admin(route, request, upstreamPath) {
1803
1881
  if (route === '/admin/health')
1804
1882
  return json({ ok: true });
1883
+ if (route === '/admin/revoke') {
1884
+ if (request.method !== 'POST') {
1885
+ return json({ error: 'method not allowed' }, 405);
1886
+ }
1887
+ return request
1888
+ .json()
1889
+ .catch(() => ({}))
1890
+ .then((body) => {
1891
+ const parsed = typeof body === 'object' && body !== null ? body : {};
1892
+ const targetUserID = 'userID' in parsed && typeof parsed.userID === 'string'
1893
+ ? parsed.userID
1894
+ : 'userId' in parsed && typeof parsed.userId === 'string'
1895
+ ? parsed.userId
1896
+ : null;
1897
+ if (!targetUserID) {
1898
+ return json({ error: 'userID is required' }, 400);
1899
+ }
1900
+ const revokedAt = 'now' in parsed &&
1901
+ typeof parsed.now === 'number' &&
1902
+ Number.isFinite(parsed.now)
1903
+ ? parsed.now
1904
+ : Date.now();
1905
+ this.ctx.storage.transactionSync(() => {
1906
+ this.#directSql.exec(`CREATE TABLE IF NOT EXISTS orez_revocations (
1907
+ user_id TEXT NOT NULL,
1908
+ revoked_at INTEGER NOT NULL
1909
+ )`);
1910
+ this.#directSql.exec('INSERT INTO orez_revocations (user_id, revoked_at) VALUES (?, ?)', [targetUserID, revokedAt]);
1911
+ });
1912
+ let closedSockets = 0;
1913
+ for (const socket of this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG)) {
1914
+ const attachment = socketAttachment(socket);
1915
+ if (attachment?.identity?.userID === targetUserID) {
1916
+ this.#realtimeDrop(socket);
1917
+ socketCloseQuietly(socket, 1008, 'user revoked');
1918
+ closedSockets++;
1919
+ }
1920
+ }
1921
+ return json({ ok: true, userID: targetUserID, revokedAt, closedSockets });
1922
+ });
1923
+ }
1805
1924
  if (route === '/admin/sql-billing')
1806
1925
  return json({ ...this.#sqlBilling });
1807
1926
  if (route === '/admin/upstream-write-budget' && request.method === 'GET')
@@ -2061,10 +2180,6 @@ export function createSyncDurableObject(config) {
2061
2180
  return new Response(null, { status: 101, webSocket: client });
2062
2181
  }
2063
2182
  async webSocketMessage(socket, message) {
2064
- if (message === 'ping') {
2065
- socket.send('pong');
2066
- return;
2067
- }
2068
2183
  this.#initialize();
2069
2184
  const host = this.#realtimeHost();
2070
2185
  if (!host || typeof message !== 'string')
package/dist/types.d.ts CHANGED
@@ -91,8 +91,10 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv, S extends Sche
91
91
  * never arrives. */
92
92
  authorizeWake(request: Request, env: Env): boolean | {
93
93
  userID: string;
94
+ tokenIssuedAt?: number;
94
95
  } | Promise<boolean | {
95
96
  userID: string;
97
+ tokenIssuedAt?: number;
96
98
  }>;
97
99
  /** Authorize upstream service notifications before selecting a namespace DO. */
98
100
  authorizeNotify(request: Request, env: Env): boolean | Promise<boolean>;
@@ -143,6 +145,13 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv, S extends Sche
143
145
  /** Server-owned invalidation epoch for permission/schema transforms. */
144
146
  queryTransformVersion?: number | ((claims: NormalizedClaims) => number);
145
147
  retainChanges?: number;
148
+ /**
149
+ * Test model of a hibernation eviction: after this much inactivity the next
150
+ * request resets the object's in-memory state the way a real eviction would.
151
+ * Leave it unset in production, where the runtime evicts for real; a
152
+ * simulated reset there drops pending wakes whenever one request blocks the
153
+ * object for longer than the window.
154
+ */
146
155
  idleTeardownMs?: number;
147
156
  wakeCoalesceMs?: number;
148
157
  /** per-query guard for recursive transaction query materialization. */
package/dist/wasm.d.ts CHANGED
@@ -21,3 +21,4 @@ export declare const engine_record_app_error: (db: any, client_group_id: string,
21
21
  export declare const engine_schema_revision: () => string;
22
22
  export declare const engine_state: (db: any) => any;
23
23
  export declare const engine_version: () => string;
24
+ export declare const engine_wake_targets: (db: any, schema: any, since: string) => any;
package/dist/wasm.js CHANGED
@@ -38,3 +38,4 @@ export const engine_record_app_error = withWasm(generated.engine_record_app_erro
38
38
  export const engine_schema_revision = withWasm(generated.engine_schema_revision);
39
39
  export const engine_state = withWasm(generated.engine_state);
40
40
  export const engine_version = withWasm(generated.engine_version);
41
+ export const engine_wake_targets = withWasm(generated.engine_wake_targets);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orez-sync-cf-host",
3
- "version": "0.16.19",
3
+ "version": "0.16.20",
4
4
  "description": "Internal orez Cloudflare sync host. Do not import directly: consumers use orez-lite/cloudflare/sync.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "@opentelemetry/api": "^1.9.0",
63
- "orez-sync-executor": "0.16.19"
63
+ "orez-sync-executor": "0.16.20"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@cloudflare/workers-types": "4.20260617.1",
@@ -87,5 +87,6 @@
87
87
  "type": "git",
88
88
  "url": "git+https://github.com/natew/orez.git",
89
89
  "directory": "packages/sync-cf-host"
90
- }
90
+ },
91
+ "releaseSourceCommit": "ea248201d35f2c30b74a6401eda49b9bdd3d2287"
91
92
  }