orez-sync-cf-host 0.12.14 → 0.12.15

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
@@ -168,7 +168,12 @@ channel disconnects, it retries in the background while the safety poll
168
168
  continues to provide convergence.
169
169
 
170
170
  `/admin/status` reports a boot ID, hibernation simulation count, connected wake
171
- sockets, durable database size, engine watermark/floor, and aggregate counters.
171
+ sockets, durable database size, engine watermark/floor, aggregate counters, and
172
+ the most recent coalesced wake batch. That wake record includes the sorted
173
+ upstream table names which caused the batch plus its socket, recipient, timing,
174
+ and frame counts. Local-only pushes have no upstream table attribution and
175
+ therefore report an empty table list. The same fields are emitted in the
176
+ structured `sync_wake` log.
172
177
  After the configured idle gap (5 seconds in the harness deployment), the local
173
178
  deterministic model resets in-memory state and changes boot ID while retaining
174
179
  SQLite state and sockets. This models the harness idle-teardown pattern; it
Binary file
package/dist/host.d.ts CHANGED
@@ -6,7 +6,9 @@ import type { Schema } from '@rocicorp/zero';
6
6
  * Durable Object receives normalized claims inside the binding request body so
7
7
  * observability systems cannot record them as request-header metadata.
8
8
  */
9
- export declare function createSyncWorker<Env extends SyncHostEnv, S extends Schema = Schema>(config: SyncHostConfig<Env, S>): ExportedHandler<Env>;
9
+ export declare function createSyncWorker<Env extends SyncHostEnv, S extends Schema = Schema>(config: SyncHostConfig<Env, S>): ExportedHandler<Env> & {
10
+ notify(env: Env, namespace: string): Promise<Response>;
11
+ };
10
12
  export interface SyncDurableObjectConstructor<Env extends SyncHostEnv> {
11
13
  new (ctx: DurableObjectState, env: Env): DurableObject<Env>;
12
14
  }
package/dist/host.js CHANGED
@@ -9,6 +9,8 @@ import { engine_authorize_realtime_subscription, engine_apply_snapshot_changes,
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';
12
+ const NOTIFY_IF_SUBSCRIBED_HEADER = 'x-orez-notify-if-subscribed';
13
+ const WAKE_SUBSCRIBER_TAG = 'orez:wake-subscriber';
12
14
  // A websocket upgrade is a GET, so the authenticated identity cannot ride the
13
15
  // body the way /pull and /push carry their claims. It rides a private header
14
16
  // the worker always deletes from the incoming request before setting its own,
@@ -16,6 +18,15 @@ const UPSTREAM_PATH_HEADER = 'x-orez-sync-upstream-path';
16
18
  const IDENTITY_HEADER = 'x-orez-sync-identity';
17
19
  const DEFAULT_SNAPSHOT_PAGE_ROWS = 2_000;
18
20
  const MIN_SNAPSHOT_PAGE_ROWS = 100;
21
+ function upstreamBatchTables(batch) {
22
+ const tables = new Set();
23
+ for (const change of batch.changes) {
24
+ if (typeof change?.tableName === 'string' && change.tableName.length > 0) {
25
+ tables.add(change.tableName);
26
+ }
27
+ }
28
+ return tables;
29
+ }
19
30
  function freshCounters() {
20
31
  return {
21
32
  pulls: 0,
@@ -171,6 +182,17 @@ function socketCloseQuietly(socket, code, reason) {
171
182
  export function createSyncWorker(config) {
172
183
  validateSyncHostConfig(config);
173
184
  const allowedOrigins = new Set(config.allowedOrigins ?? []);
185
+ const upstreamPath = (namespace) => {
186
+ if (!config.upstream)
187
+ return null;
188
+ const path = typeof config.upstream.namespacePath === 'function'
189
+ ? config.upstream.namespacePath(namespace)
190
+ : config.upstream.namespacePath;
191
+ if (!path.startsWith('/')) {
192
+ throw new Error('upstream namespacePath must be an absolute path');
193
+ }
194
+ return path.replace(/\/$/, '');
195
+ };
174
196
  const corsHeaders = (origin) => ({
175
197
  'access-control-allow-origin': origin,
176
198
  vary: 'origin',
@@ -266,6 +288,7 @@ export function createSyncWorker(config) {
266
288
  headers.delete(NAMESPACE_HEADER);
267
289
  headers.delete(UPSTREAM_PATH_HEADER);
268
290
  headers.delete(IDENTITY_HEADER);
291
+ headers.delete(NOTIFY_IF_SUBSCRIBED_HEADER);
269
292
  let forwardedBody = null;
270
293
  // /wake and /realtime/produce are both websocket upgrades and have no
271
294
  // body to put claims in. wake authentication is normalized from its
@@ -302,14 +325,13 @@ export function createSyncWorker(config) {
302
325
  if (wakeUserID)
303
326
  headers.set(IDENTITY_HEADER, encodeURIComponent(wakeUserID));
304
327
  headers.set(NAMESPACE_HEADER, await namespaceHash(namespace));
305
- if (config.upstream) {
306
- const namespacePath = typeof config.upstream.namespacePath === 'function'
307
- ? config.upstream.namespacePath(namespace)
308
- : config.upstream.namespacePath;
309
- if (!namespacePath.startsWith('/')) {
310
- return json({ error: 'upstream namespacePath must be an absolute path' }, 500);
311
- }
312
- headers.set(UPSTREAM_PATH_HEADER, namespacePath.replace(/\/$/, ''));
328
+ try {
329
+ const path = upstreamPath(namespace);
330
+ if (path !== null)
331
+ headers.set(UPSTREAM_PATH_HEADER, path);
332
+ }
333
+ catch (error) {
334
+ return errorResponse(error);
313
335
  }
314
336
  const forwarded = forwardedBody
315
337
  ? jsonBodyRequest(request, headers, forwardedBody)
@@ -319,6 +341,23 @@ export function createSyncWorker(config) {
319
341
  };
320
342
  return withCors(await handle());
321
343
  },
344
+ async notify(env, namespace) {
345
+ if (!namespace)
346
+ throw new TypeError('sync notification namespace is required');
347
+ const path = upstreamPath(namespace);
348
+ if (path === null)
349
+ throw new Error('sync notifications require an upstream feed');
350
+ const headers = new Headers({
351
+ [NAMESPACE_HEADER]: await namespaceHash(namespace),
352
+ [UPSTREAM_PATH_HEADER]: path,
353
+ [NOTIFY_IF_SUBSCRIBED_HEADER]: '1',
354
+ });
355
+ const request = new Request('https://orez-sync.internal/namespace/notify', {
356
+ method: 'POST',
357
+ headers,
358
+ });
359
+ return env.SYNC_DO.get(env.SYNC_DO.idFromName(namespace)).fetch(request);
360
+ },
322
361
  };
323
362
  }
324
363
  /** Create the namespace Durable Object class for one bundled consumer config. */
@@ -352,6 +391,7 @@ export function createSyncDurableObject(config) {
352
391
  #executor;
353
392
  #executorBeforeCommitFault = null;
354
393
  #bootID = crypto.randomUUID();
394
+ #initialized = false;
355
395
  #initSkipped = false;
356
396
  #lastRequestAt = 0;
357
397
  #hibernations = 0;
@@ -361,7 +401,9 @@ export function createSyncDurableObject(config) {
361
401
  #pulling = new Set();
362
402
  #wakeOrigins = new Set();
363
403
  #wakeRecipients = new Set();
404
+ #wakeTables = new Set();
364
405
  #wakePromise = null;
406
+ #lastWake = null;
365
407
  // Streaming fields. Null when the namespace configures no manifest, which
366
408
  // is every wake-only deployment: no hub is built and nothing below runs.
367
409
  #realtime = null;
@@ -445,39 +487,42 @@ export function createSyncDurableObject(config) {
445
487
  schema: config.schema,
446
488
  })
447
489
  : null;
448
- ctx.blockConcurrencyWhile(async () => {
449
- ctx.storage.transactionSync(() => {
450
- this.#directSql.exec(`CREATE TABLE IF NOT EXISTS _zsync_host_control (
451
- key TEXT PRIMARY KEY,
452
- value TEXT NOT NULL
453
- )`);
454
- // one durable fingerprint gates the whole schema pass. hibernation
455
- // reconstructs this object every few idle minutes, and re-running
456
- // consumer DDL plus both engine inits against an already-current
457
- // database costs ~1.5k billable reads per wake. everything the pass
458
- // depends on is in the fingerprint, so a build that changes any DDL
459
- // surface re-runs it exactly once per namespace.
460
- const initFingerprint = JSON.stringify([
461
- config.hostVersion,
462
- this.#wasm(() => engine_schema_revision()),
463
- config.schema,
464
- config.initialize.toString(),
465
- ]);
466
- this.#initSkipped = this.#controlGet('initFingerprint') === initFingerprint;
467
- if (!this.#initSkipped) {
468
- config.initialize(this.#directSql);
469
- this.#directSql.exec("INSERT OR IGNORE INTO _zsync_host_control (key, value) VALUES ('writerEnabled', '1')");
470
- this.#wasm(() => engine_init_schema(this.#engineDb, config.schema));
471
- this.#wasm(() => engine_init_query_schema(this.#engineDb));
472
- this.#controlSet('initFingerprint', initFingerprint);
473
- }
474
- const ingestBreakerReason = this.#controlGet('ingestBreakerReason');
475
- if (ingestBreakerReason === 'ingestBudgetExceeded' ||
476
- ingestBreakerReason === 'ingestCursorStalled') {
477
- this.#ingestBreaker.restore(ingestBreakerReason, Number(this.#controlGet('ingestBreakerRetryAt')), Number(this.#controlGet('ingestBreakerTrips')));
478
- }
479
- });
490
+ }
491
+ #initialize() {
492
+ if (this.#initialized)
493
+ return;
494
+ this.ctx.storage.transactionSync(() => {
495
+ this.#directSql.exec(`CREATE TABLE IF NOT EXISTS _zsync_host_control (
496
+ key TEXT PRIMARY KEY,
497
+ value TEXT NOT NULL
498
+ )`);
499
+ // one durable fingerprint gates the whole schema pass. hibernation
500
+ // reconstructs this object every few idle minutes, and re-running
501
+ // consumer DDL plus both engine inits against an already-current
502
+ // database costs ~1.5k billable reads per wake. everything the pass
503
+ // depends on is in the fingerprint, so a build that changes any DDL
504
+ // surface re-runs it exactly once per namespace.
505
+ const initFingerprint = JSON.stringify([
506
+ config.hostVersion,
507
+ this.#wasm(() => engine_schema_revision()),
508
+ config.schema,
509
+ config.initialize.toString(),
510
+ ]);
511
+ this.#initSkipped = this.#controlGet('initFingerprint') === initFingerprint;
512
+ if (!this.#initSkipped) {
513
+ config.initialize(this.#directSql);
514
+ this.#directSql.exec("INSERT OR IGNORE INTO _zsync_host_control (key, value) VALUES ('writerEnabled', '1')");
515
+ this.#wasm(() => engine_init_schema(this.#engineDb, config.schema));
516
+ this.#wasm(() => engine_init_query_schema(this.#engineDb));
517
+ this.#controlSet('initFingerprint', initFingerprint);
518
+ }
519
+ const ingestBreakerReason = this.#controlGet('ingestBreakerReason');
520
+ if (ingestBreakerReason === 'ingestBudgetExceeded' ||
521
+ ingestBreakerReason === 'ingestCursorStalled') {
522
+ this.#ingestBreaker.restore(ingestBreakerReason, Number(this.#controlGet('ingestBreakerRetryAt')), Number(this.#controlGet('ingestBreakerTrips')));
523
+ }
480
524
  });
525
+ this.#initialized = true;
481
526
  }
482
527
  #armUpstreamAlarm() {
483
528
  if (!config.upstream)
@@ -501,7 +546,9 @@ export function createSyncDurableObject(config) {
501
546
  this.#pulling.clear();
502
547
  this.#wakeOrigins.clear();
503
548
  this.#wakeRecipients.clear();
549
+ this.#wakeTables.clear();
504
550
  this.#wakePromise = null;
551
+ this.#lastWake = null;
505
552
  // Real hibernation reconstructs the object, so the hub and every
506
553
  // connection handle are gone while the sockets stay open. Modelling
507
554
  // that here is what makes rehydration reachable from a test: leaving
@@ -759,6 +806,7 @@ export function createSyncDurableObject(config) {
759
806
  let pendingPage = null;
760
807
  let snapshotPageLimit = DEFAULT_SNAPSHOT_PAGE_ROWS;
761
808
  let snapshotCompleted = false;
809
+ const changedTables = new Set();
762
810
  for (;;) {
763
811
  if (progress?.state === 'paging') {
764
812
  const activeProgress = progress;
@@ -781,6 +829,7 @@ export function createSyncDurableObject(config) {
781
829
  }, () => this.ctx.storage.transactionSync(() => this.#wasm(() => engine_apply_snapshot_page(this.#engineDb, config.schema, activeProgress.generation, table, pageToApply.rows, pageToApply.nextCursor))));
782
830
  total += pageToApply.rows.length;
783
831
  this.#recordIngestLogicalRows(pageToApply.rows.length);
832
+ changedTables.add(table);
784
833
  progress = nextProgress;
785
834
  pendingPage = null;
786
835
  }
@@ -818,6 +867,8 @@ export function createSyncDurableObject(config) {
818
867
  if (!Number.isSafeInteger(batch.watermark) || !Array.isArray(batch.changes)) {
819
868
  throw new Error('invalid upstream changes response');
820
869
  }
870
+ for (const table of upstreamBatchTables(batch))
871
+ changedTables.add(table);
821
872
  this.#resetSnapshotBillingWindow();
822
873
  const result = this.#withIngestBilling({
823
874
  phase: 'snapshot_catchup',
@@ -884,6 +935,8 @@ export function createSyncDurableObject(config) {
884
935
  if (!Number.isSafeInteger(batch.watermark) || !Array.isArray(batch.changes)) {
885
936
  throw new Error('invalid upstream changes response');
886
937
  }
938
+ for (const table of upstreamBatchTables(batch))
939
+ changedTables.add(table);
887
940
  const result = this.#withIngestBilling({
888
941
  phase: 'changes',
889
942
  cursor,
@@ -921,7 +974,7 @@ export function createSyncDurableObject(config) {
921
974
  this.#recoverIngestBreaker();
922
975
  const endingWatermark = this.#engineState().watermark;
923
976
  if (snapshotCompleted || total > 0 || endingWatermark !== startingWatermark) {
924
- await this.#enqueueWake('__upstream__');
977
+ await this.#enqueueWake('__upstream__', changedTables);
925
978
  }
926
979
  return total;
927
980
  })().finally(() => {
@@ -978,9 +1031,11 @@ export function createSyncDurableObject(config) {
978
1031
  ...fields,
979
1032
  }));
980
1033
  }
981
- #enqueueWake(originClientID) {
1034
+ #enqueueWake(originClientID, tables = []) {
982
1035
  this.#wakeOrigins.add(originClientID);
983
- for (const socket of this.ctx.getWebSockets()) {
1036
+ for (const table of tables)
1037
+ this.#wakeTables.add(table);
1038
+ for (const socket of this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG)) {
984
1039
  const attachment = socketAttachment(socket);
985
1040
  if (!attachment ||
986
1041
  attachment.clientID === originClientID ||
@@ -1001,9 +1056,11 @@ export function createSyncDurableObject(config) {
1001
1056
  this.#wakeOrigins = new Set();
1002
1057
  const recipients = this.#wakeRecipients;
1003
1058
  this.#wakeRecipients = new Set();
1059
+ const tables = [...this.#wakeTables].sort();
1060
+ this.#wakeTables = new Set();
1004
1061
  this.#counters.wakeBatches++;
1005
1062
  let sent = 0;
1006
- const sockets = this.ctx.getWebSockets();
1063
+ const sockets = this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG);
1007
1064
  for (const socket of recipients) {
1008
1065
  try {
1009
1066
  socket.send('wake');
@@ -1015,15 +1072,21 @@ export function createSyncDurableObject(config) {
1015
1072
  // a race here is advisory and carries no correctness weight.
1016
1073
  }
1017
1074
  }
1018
- console.log(JSON.stringify({
1019
- event: 'sync_wake',
1020
- hostVersion: config.hostVersion,
1075
+ const wakeStatus = {
1076
+ at: Date.now(),
1077
+ tables,
1021
1078
  socketCount: sockets.length,
1022
1079
  originCount: origins.size,
1023
1080
  sent,
1024
1081
  eligibleRecipients: recipients.size,
1025
1082
  coalesceMs: fanoutStarted - queuedAt,
1026
1083
  fanoutMs: performance.now() - fanoutStarted,
1084
+ };
1085
+ this.#lastWake = wakeStatus;
1086
+ console.log(JSON.stringify({
1087
+ event: 'sync_wake',
1088
+ hostVersion: config.hostVersion,
1089
+ ...wakeStatus,
1027
1090
  }));
1028
1091
  })().finally(() => {
1029
1092
  this.#wakePromise = null;
@@ -1439,7 +1502,7 @@ export function createSyncDurableObject(config) {
1439
1502
  clientID,
1440
1503
  identity,
1441
1504
  });
1442
- this.ctx.acceptWebSocket(server, [`client:${clientID}`]);
1505
+ this.ctx.acceptWebSocket(server, [WAKE_SUBSCRIBER_TAG, `client:${clientID}`]);
1443
1506
  if (identity) {
1444
1507
  const host = this.#realtimeHost();
1445
1508
  if (host) {
@@ -1474,7 +1537,7 @@ export function createSyncDurableObject(config) {
1474
1537
  idleTeardownMs,
1475
1538
  hibernations: this.#hibernations,
1476
1539
  databaseSizeBytes: this.ctx.storage.sql.databaseSize,
1477
- connectedWakeSockets: this.ctx.getWebSockets().length,
1540
+ connectedWakeSockets: this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG).length,
1478
1541
  upstreamAlarmAt,
1479
1542
  writerEnabled: this.#writerEnabled(),
1480
1543
  wasmMemoryBytes: engine_memory_bytes(),
@@ -1485,6 +1548,7 @@ export function createSyncDurableObject(config) {
1485
1548
  sqlBillingSinceBoot: this.#sqlBilling,
1486
1549
  counters: this.#counters,
1487
1550
  ingestBreaker: this.#ingestBreaker.status(),
1551
+ lastWake: this.#lastWake,
1488
1552
  }));
1489
1553
  }
1490
1554
  if (route === '/admin/sql') {
@@ -1609,8 +1673,20 @@ export function createSyncDurableObject(config) {
1609
1673
  return json({ error: 'not found' }, 404);
1610
1674
  }
1611
1675
  async fetch(request) {
1612
- this.#simulateIdleTeardown(Date.now());
1613
1676
  const route = routeAfterNamespace(new URL(request.url).pathname);
1677
+ if (route === '/notify' &&
1678
+ request.method === 'POST' &&
1679
+ request.headers.get(NOTIFY_IF_SUBSCRIBED_HEADER) === '1' &&
1680
+ this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG).length === 0) {
1681
+ return json({
1682
+ ok: true,
1683
+ applied: 0,
1684
+ skipped: 'no-wake-subscribers',
1685
+ rowsWritten: this.#sqlBilling.rowsWritten,
1686
+ });
1687
+ }
1688
+ this.#initialize();
1689
+ this.#simulateIdleTeardown(Date.now());
1614
1690
  const namespace = request.headers.get(NAMESPACE_HEADER) ?? 'unknown';
1615
1691
  const upstreamPath = this.#rememberUpstreamPath(request);
1616
1692
  if (route.startsWith('/admin/'))
@@ -1622,7 +1698,7 @@ export function createSyncDurableObject(config) {
1622
1698
  }
1623
1699
  if (route === '/notify' && request.method === 'POST') {
1624
1700
  try {
1625
- const applied = await this.#ingest(upstreamPath);
1701
+ const applied = await this.#ingestAfterCurrent(upstreamPath);
1626
1702
  return json({ ok: true, applied });
1627
1703
  }
1628
1704
  catch (error) {
@@ -1652,8 +1728,9 @@ export function createSyncDurableObject(config) {
1652
1728
  return json({ error: 'not found' }, 404);
1653
1729
  }
1654
1730
  async alarm() {
1655
- if (this.ctx.getWebSockets().length === 0)
1731
+ if (this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG).length === 0)
1656
1732
  return;
1733
+ this.#initialize();
1657
1734
  try {
1658
1735
  await this.#ingest();
1659
1736
  }
@@ -1665,7 +1742,7 @@ export function createSyncDurableObject(config) {
1665
1742
  }));
1666
1743
  }
1667
1744
  finally {
1668
- if (this.ctx.getWebSockets().length > 0) {
1745
+ if (this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG).length > 0) {
1669
1746
  const retryAfterMs = this.#ingestBreaker.status().retryAfterMs;
1670
1747
  await this.ctx.storage.setAlarm(Date.now() + Math.max(upstreamIntervalMs, retryAfterMs));
1671
1748
  }
@@ -1700,6 +1777,7 @@ export function createSyncDurableObject(config) {
1700
1777
  socket.send('pong');
1701
1778
  return;
1702
1779
  }
1780
+ this.#initialize();
1703
1781
  const host = this.#realtimeHost();
1704
1782
  if (!host || typeof message !== 'string')
1705
1783
  return;
@@ -1,6 +1,6 @@
1
+ import { trackBillableCursorRows } from 'orez-sync-executor/sql-billing';
1
2
  import { encodeSqlValue } from 'orez-sync-executor/sqlite';
2
3
  import { executeTransactionQueryPlan } from './transaction-query.js';
3
- import { trackBillableCursorRows } from './write-safeguards.js';
4
4
  // Match transaction control only as the statement itself. CREATE TRIGGER uses
5
5
  // `... BEGIN ... END` for its body, which is valid DO SQL and not a transaction.
6
6
  const TX_SQL = /^\s*(BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE)(?=\s|;|$)/i;
@@ -39,16 +39,5 @@ export declare class IngestCircuitBreaker {
39
39
  restore(reason: IngestBreakerReason, retryAt: number, consecutiveTrips: number): void;
40
40
  reopen(): void;
41
41
  }
42
- type BillableCursor = {
43
- rowsRead?: number;
44
- rowsWritten?: number;
45
- next?: (...args: unknown[]) => unknown;
46
- one?: (...args: unknown[]) => unknown;
47
- toArray?: (...args: unknown[]) => unknown;
48
- raw?: (...args: unknown[]) => unknown;
49
- [Symbol.iterator]?: () => Iterator<unknown>;
50
- };
51
- export declare function trackBillableCursorRows<Cursor extends BillableCursor>(cursor: Cursor, recordWritten: (rows: number) => void, recordRead?: (rows: number) => void): Cursor;
52
42
  export declare function retryDelayMs(attempt: number, initialBackoffMs: number, maxBackoffMs: number): number;
53
43
  export declare function shouldRetryDelegatedPush(responseStatus: number | null, attempt: number, maxAttempts: number): boolean;
54
- export {};
@@ -122,65 +122,6 @@ export class IngestCircuitBreaker {
122
122
  this.recovered();
123
123
  }
124
124
  }
125
- export function trackBillableCursorRows(cursor, recordWritten, recordRead = () => { }) {
126
- if (!cursor || typeof cursor !== 'object')
127
- return cursor;
128
- let accountedWritten = 0;
129
- let accountedRead = 0;
130
- const account = () => {
131
- const written = Number(cursor.rowsWritten ?? 0);
132
- if (Number.isSafeInteger(written) && written > accountedWritten) {
133
- recordWritten(written - accountedWritten);
134
- accountedWritten = written;
135
- }
136
- const read = Number(cursor.rowsRead ?? 0);
137
- if (Number.isSafeInteger(read) && read > accountedRead) {
138
- recordRead(read - accountedRead);
139
- accountedRead = read;
140
- }
141
- };
142
- const wrapIterator = (iterator) => {
143
- const next = iterator.next.bind(iterator);
144
- return {
145
- next(...args) {
146
- try {
147
- return next(...args);
148
- }
149
- finally {
150
- account();
151
- }
152
- },
153
- [Symbol.iterator]() {
154
- return this;
155
- },
156
- };
157
- };
158
- let proxy;
159
- proxy = new Proxy(cursor, {
160
- get(target, property) {
161
- const value = Reflect.get(target, property, target);
162
- if (property === Symbol.iterator)
163
- return () => proxy;
164
- if ((property === 'next' || property === 'one' || property === 'toArray') &&
165
- typeof value === 'function') {
166
- return (...args) => {
167
- try {
168
- return Reflect.apply(value, target, args);
169
- }
170
- finally {
171
- account();
172
- }
173
- };
174
- }
175
- if (property === 'raw' && typeof value === 'function') {
176
- return (...args) => wrapIterator(Reflect.apply(value, target, args));
177
- }
178
- return typeof value === 'function' ? value.bind(target) : value;
179
- },
180
- });
181
- account();
182
- return proxy;
183
- }
184
125
  export function retryDelayMs(attempt, initialBackoffMs, maxBackoffMs) {
185
126
  return Math.min(maxBackoffMs, initialBackoffMs * 2 ** Math.max(0, attempt - 1));
186
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orez-sync-cf-host",
3
- "version": "0.12.14",
3
+ "version": "0.12.15",
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": {
@@ -58,7 +58,7 @@
58
58
  "test:large-data": "bun run build:dist && bun large-data-test.mjs"
59
59
  },
60
60
  "dependencies": {
61
- "orez-sync-executor": "0.12.14"
61
+ "orez-sync-executor": "0.12.15"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@cloudflare/workers-types": "4.20260617.1",