orez-sync-cf-host 0.12.1 → 0.12.3
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/dist/generated/sync_wasm.d.ts +7 -0
- package/dist/generated/sync_wasm.js +18 -0
- package/dist/generated/sync_wasm_bg.js +18 -0
- package/dist/generated/sync_wasm_bg.wasm +0 -0
- package/dist/host.js +80 -20
- package/dist/sql-storage-adapter.d.ts +4 -2
- package/dist/sql-storage-adapter.js +11 -7
- package/dist/wasm.d.ts +1 -0
- package/dist/wasm.js +1 -0
- package/dist/write-safeguards.d.ts +2 -1
- package/dist/write-safeguards.js +13 -8
- package/package.json +2 -2
|
@@ -105,6 +105,12 @@ export function engine_read_snapshot_progress(db: any): any;
|
|
|
105
105
|
|
|
106
106
|
export function engine_record_app_error(db: any, client_group_id: string, client_id: string, mutation_id: string, user_id: string): void;
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Identity of the durable DDL this engine build installs. Hosts fold it into
|
|
110
|
+
* the fingerprint that lets an up-to-date namespace skip the schema pass.
|
|
111
|
+
*/
|
|
112
|
+
export function engine_schema_revision(): string;
|
|
113
|
+
|
|
108
114
|
export function engine_state(db: any): any;
|
|
109
115
|
|
|
110
116
|
export function engine_version(): string;
|
|
@@ -133,6 +139,7 @@ export interface InitOutput {
|
|
|
133
139
|
readonly engine_push_validate: (a: any) => [number, number, number];
|
|
134
140
|
readonly engine_read_snapshot_progress: (a: any) => [number, number, number];
|
|
135
141
|
readonly engine_record_app_error: (a: any, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number];
|
|
142
|
+
readonly engine_schema_revision: () => [number, number];
|
|
136
143
|
readonly engine_state: (a: any) => [number, number, number];
|
|
137
144
|
readonly engine_version: () => [number, number];
|
|
138
145
|
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
|
@@ -352,6 +352,24 @@ export function engine_record_app_error(db, client_group_id, client_id, mutation
|
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Identity of the durable DDL this engine build installs. Hosts fold it into
|
|
357
|
+
* the fingerprint that lets an up-to-date namespace skip the schema pass.
|
|
358
|
+
* @returns {string}
|
|
359
|
+
*/
|
|
360
|
+
export function engine_schema_revision() {
|
|
361
|
+
let deferred1_0;
|
|
362
|
+
let deferred1_1;
|
|
363
|
+
try {
|
|
364
|
+
const ret = wasm.engine_schema_revision();
|
|
365
|
+
deferred1_0 = ret[0];
|
|
366
|
+
deferred1_1 = ret[1];
|
|
367
|
+
return getStringFromWasm0(ret[0], ret[1]);
|
|
368
|
+
} finally {
|
|
369
|
+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
355
373
|
/**
|
|
356
374
|
* @param {any} db
|
|
357
375
|
* @returns {any}
|
|
@@ -350,6 +350,24 @@ export function engine_record_app_error(db, client_group_id, client_id, mutation
|
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Identity of the durable DDL this engine build installs. Hosts fold it into
|
|
355
|
+
* the fingerprint that lets an up-to-date namespace skip the schema pass.
|
|
356
|
+
* @returns {string}
|
|
357
|
+
*/
|
|
358
|
+
export function engine_schema_revision() {
|
|
359
|
+
let deferred1_0;
|
|
360
|
+
let deferred1_1;
|
|
361
|
+
try {
|
|
362
|
+
const ret = wasm.engine_schema_revision();
|
|
363
|
+
deferred1_0 = ret[0];
|
|
364
|
+
deferred1_1 = ret[1];
|
|
365
|
+
return getStringFromWasm0(ret[0], ret[1]);
|
|
366
|
+
} finally {
|
|
367
|
+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
353
371
|
/**
|
|
354
372
|
* @param {any} db
|
|
355
373
|
* @returns {any}
|
|
Binary file
|
package/dist/host.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
-
import { createSyncExecutor } from 'orez-sync-executor/core';
|
|
2
|
+
import { createSyncExecutor, isMutationRetryError } from 'orez-sync-executor/core';
|
|
3
3
|
import { createSocketHost } from 'orez-sync-executor/realtime';
|
|
4
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_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_schema_revision, 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';
|
|
@@ -31,10 +31,10 @@ function freshCounters() {
|
|
|
31
31
|
externalEffectFailures: 0,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
function json(value, status = 200) {
|
|
34
|
+
function json(value, status = 200, headers) {
|
|
35
35
|
return Response.json(value, {
|
|
36
36
|
status,
|
|
37
|
-
headers: { 'cache-control': 'no-store' },
|
|
37
|
+
headers: { 'cache-control': 'no-store', ...headers },
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
function isStructuredPushFailed(value) {
|
|
@@ -67,8 +67,29 @@ function errorBody(error) {
|
|
|
67
67
|
retryAfterMs: error.retryAfterMs,
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
+
if (isMutationRetryError(error)) {
|
|
71
|
+
// the reason travels in `details` so a caller can pace on why it was
|
|
72
|
+
// refused. it used to arrive as an acknowledged app error, which cost a
|
|
73
|
+
// ledger write per refusal and consumed the mutation.
|
|
74
|
+
return {
|
|
75
|
+
error: errorMessage(error),
|
|
76
|
+
...(error.details === undefined ? {} : { details: error.details }),
|
|
77
|
+
retryAfterMs: error.retryAfterMs,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
70
80
|
return { error: errorMessage(error) };
|
|
71
81
|
}
|
|
82
|
+
// one response builder for every refusal, so a rejection that tells the caller
|
|
83
|
+
// when to come back always says so in the header an HTTP client already reads.
|
|
84
|
+
function errorResponse(error) {
|
|
85
|
+
const body = errorBody(error);
|
|
86
|
+
const retryAfterMs = body.retryAfterMs;
|
|
87
|
+
// whole seconds on the wire, rounded up: rounding down invites the caller
|
|
88
|
+
// back while the window it has to wait out is still open.
|
|
89
|
+
return json(body, statusOf(error), typeof retryAfterMs === 'number'
|
|
90
|
+
? { 'retry-after': String(Math.ceil(retryAfterMs / 1000)) }
|
|
91
|
+
: undefined);
|
|
92
|
+
}
|
|
72
93
|
function requestError(message, status = 400) {
|
|
73
94
|
return Object.assign(new Error(message), { status });
|
|
74
95
|
}
|
|
@@ -209,7 +230,7 @@ export function createSyncWorker(config) {
|
|
|
209
230
|
forwardedBody = { claims, body };
|
|
210
231
|
}
|
|
211
232
|
catch (error) {
|
|
212
|
-
return
|
|
233
|
+
return errorResponse(error);
|
|
213
234
|
}
|
|
214
235
|
}
|
|
215
236
|
}
|
|
@@ -264,10 +285,12 @@ export function createSyncDurableObject(config) {
|
|
|
264
285
|
#executor;
|
|
265
286
|
#executorBeforeCommitFault = null;
|
|
266
287
|
#bootID = crypto.randomUUID();
|
|
288
|
+
#initSkipped = false;
|
|
267
289
|
#lastRequestAt = 0;
|
|
268
290
|
#hibernations = 0;
|
|
269
291
|
#dropNextPushResponse = false;
|
|
270
292
|
#counters = freshCounters();
|
|
293
|
+
#sqlBilling = { rowsRead: 0, rowsWritten: 0 };
|
|
271
294
|
#pulling = new Set();
|
|
272
295
|
#wakeOrigins = new Set();
|
|
273
296
|
#wakeRecipients = new Set();
|
|
@@ -292,11 +315,15 @@ export function createSyncDurableObject(config) {
|
|
|
292
315
|
constructor(ctx, env) {
|
|
293
316
|
super(ctx, env);
|
|
294
317
|
const recordRowsWritten = (rows) => {
|
|
318
|
+
this.#sqlBilling.rowsWritten += rows;
|
|
295
319
|
if (this.#recordingIngestBillable)
|
|
296
320
|
this.#ingestBreaker.recordBillable(rows);
|
|
297
321
|
};
|
|
298
|
-
|
|
299
|
-
|
|
322
|
+
const recordRowsRead = (rows) => {
|
|
323
|
+
this.#sqlBilling.rowsRead += rows;
|
|
324
|
+
};
|
|
325
|
+
this.#engineDb = new SqlStorageSyncDb(ctx.storage.sql, recordRowsWritten, recordRowsRead);
|
|
326
|
+
this.#directSql = new SqlStorageDirect(ctx.storage.sql, recordRowsWritten, recordRowsRead);
|
|
300
327
|
this.#mutatorSql = new SqlStorageMutatorTransaction(this.#directSql, (ast, format) => this.#wasm(() => compileQuery(ast, format)), config.transactionQueryBudget);
|
|
301
328
|
const database = {
|
|
302
329
|
dialect: 'sqlite',
|
|
@@ -311,7 +338,16 @@ export function createSyncDurableObject(config) {
|
|
|
311
338
|
}
|
|
312
339
|
return this.#mutatorSql.exec(sql, params, metadata);
|
|
313
340
|
},
|
|
314
|
-
query: (sql, params) =>
|
|
341
|
+
query: (sql, params) => {
|
|
342
|
+
// exact helper statements use sqlite returning so the executor
|
|
343
|
+
// can prove the physical primary keys that changed. Keep the
|
|
344
|
+
// host's pre-commit fault boundary after those writes too.
|
|
345
|
+
if (/\b(?:INSERT|UPDATE|DELETE|REPLACE)\b/i.test(sql) &&
|
|
346
|
+
/\bRETURNING\b/i.test(sql)) {
|
|
347
|
+
applicationWrite = true;
|
|
348
|
+
}
|
|
349
|
+
return this.#mutatorSql.query(sql, params);
|
|
350
|
+
},
|
|
315
351
|
queryAst: (ast, format, queryName) => this.#mutatorSql.queryAst(ast, format, queryName),
|
|
316
352
|
};
|
|
317
353
|
const value = await work(tx);
|
|
@@ -344,19 +380,35 @@ export function createSyncDurableObject(config) {
|
|
|
344
380
|
: null;
|
|
345
381
|
ctx.blockConcurrencyWhile(async () => {
|
|
346
382
|
ctx.storage.transactionSync(() => {
|
|
347
|
-
config.initialize(this.#directSql);
|
|
348
383
|
this.#directSql.exec(`CREATE TABLE IF NOT EXISTS _zsync_host_control (
|
|
349
384
|
key TEXT PRIMARY KEY,
|
|
350
385
|
value TEXT NOT NULL
|
|
351
386
|
)`);
|
|
352
|
-
|
|
387
|
+
// one durable fingerprint gates the whole schema pass. hibernation
|
|
388
|
+
// reconstructs this object every few idle minutes, and re-running
|
|
389
|
+
// consumer DDL plus both engine inits against an already-current
|
|
390
|
+
// database costs ~1.5k billable reads per wake. everything the pass
|
|
391
|
+
// depends on is in the fingerprint, so a build that changes any DDL
|
|
392
|
+
// surface re-runs it exactly once per namespace.
|
|
393
|
+
const initFingerprint = JSON.stringify([
|
|
394
|
+
config.hostVersion,
|
|
395
|
+
this.#wasm(() => engine_schema_revision()),
|
|
396
|
+
config.schema,
|
|
397
|
+
config.initialize.toString(),
|
|
398
|
+
]);
|
|
399
|
+
this.#initSkipped = this.#controlGet('initFingerprint') === initFingerprint;
|
|
400
|
+
if (!this.#initSkipped) {
|
|
401
|
+
config.initialize(this.#directSql);
|
|
402
|
+
this.#directSql.exec("INSERT OR IGNORE INTO _zsync_host_control (key, value) VALUES ('writerEnabled', '1')");
|
|
403
|
+
this.#wasm(() => engine_init_schema(this.#engineDb, config.schema));
|
|
404
|
+
this.#wasm(() => engine_init_query_schema(this.#engineDb));
|
|
405
|
+
this.#controlSet('initFingerprint', initFingerprint);
|
|
406
|
+
}
|
|
353
407
|
const ingestBreakerReason = this.#controlGet('ingestBreakerReason');
|
|
354
408
|
if (ingestBreakerReason === 'ingestBudgetExceeded' ||
|
|
355
409
|
ingestBreakerReason === 'ingestCursorStalled') {
|
|
356
410
|
this.#ingestBreaker.restore(ingestBreakerReason, Number(this.#controlGet('ingestBreakerRetryAt')), Number(this.#controlGet('ingestBreakerTrips')));
|
|
357
411
|
}
|
|
358
|
-
this.#wasm(() => engine_init_schema(this.#engineDb, config.schema));
|
|
359
|
-
this.#wasm(() => engine_init_query_schema(this.#engineDb));
|
|
360
412
|
});
|
|
361
413
|
});
|
|
362
414
|
}
|
|
@@ -378,6 +430,7 @@ export function createSyncDurableObject(config) {
|
|
|
378
430
|
this.#bootID = crypto.randomUUID();
|
|
379
431
|
this.#hibernations++;
|
|
380
432
|
this.#counters = freshCounters();
|
|
433
|
+
this.#sqlBilling = { rowsRead: 0, rowsWritten: 0 };
|
|
381
434
|
this.#pulling.clear();
|
|
382
435
|
this.#wakeOrigins.clear();
|
|
383
436
|
this.#wakeRecipients.clear();
|
|
@@ -402,8 +455,12 @@ export function createSyncDurableObject(config) {
|
|
|
402
455
|
return row?.value ?? null;
|
|
403
456
|
}
|
|
404
457
|
#controlSet(key, value) {
|
|
458
|
+
// the WHERE guard makes a same-value set free: upstreamPath arrives on
|
|
459
|
+
// every forwarded request, and an unguarded upsert billed one row
|
|
460
|
+
// written per request for a value that almost never changes.
|
|
405
461
|
this.#directSql.exec('INSERT INTO _zsync_host_control (key, value) VALUES (?, ?) ' +
|
|
406
|
-
'ON CONFLICT(key) DO UPDATE SET value = excluded.value'
|
|
462
|
+
'ON CONFLICT(key) DO UPDATE SET value = excluded.value ' +
|
|
463
|
+
'WHERE value <> excluded.value', [key, value]);
|
|
407
464
|
}
|
|
408
465
|
#controlDelete(...keys) {
|
|
409
466
|
if (keys.length === 0)
|
|
@@ -1106,8 +1163,7 @@ export function createSyncDurableObject(config) {
|
|
|
1106
1163
|
return json({ pushResponse: delegatedResponse });
|
|
1107
1164
|
}
|
|
1108
1165
|
catch (error) {
|
|
1109
|
-
|
|
1110
|
-
return json(errorBody(error), status);
|
|
1166
|
+
return errorResponse(error);
|
|
1111
1167
|
}
|
|
1112
1168
|
}
|
|
1113
1169
|
try {
|
|
@@ -1203,7 +1259,7 @@ export function createSyncDurableObject(config) {
|
|
|
1203
1259
|
totalMs: performance.now() - started,
|
|
1204
1260
|
resetReason: null,
|
|
1205
1261
|
});
|
|
1206
|
-
return
|
|
1262
|
+
return errorResponse(error);
|
|
1207
1263
|
}
|
|
1208
1264
|
}
|
|
1209
1265
|
// The hub for this incarnation, built on first use. Null unless the
|
|
@@ -1328,12 +1384,15 @@ export function createSyncDurableObject(config) {
|
|
|
1328
1384
|
#admin(route, request, upstreamPath) {
|
|
1329
1385
|
if (route === '/admin/health')
|
|
1330
1386
|
return json({ ok: true });
|
|
1387
|
+
if (route === '/admin/sql-billing')
|
|
1388
|
+
return json({ ...this.#sqlBilling });
|
|
1331
1389
|
if (route === '/admin/upstream-write-budget' && request.method === 'GET')
|
|
1332
1390
|
return this.#upstreamWriteBudgetStatus();
|
|
1333
1391
|
if (route === '/admin/status') {
|
|
1334
1392
|
const heap = performance.memory;
|
|
1335
1393
|
return this.ctx.storage.getAlarm().then((upstreamAlarmAt) => json({
|
|
1336
1394
|
bootID: this.#bootID,
|
|
1395
|
+
initSkipped: this.#initSkipped,
|
|
1337
1396
|
idleTeardownMs,
|
|
1338
1397
|
hibernations: this.#hibernations,
|
|
1339
1398
|
databaseSizeBytes: this.ctx.storage.sql.databaseSize,
|
|
@@ -1345,6 +1404,7 @@ export function createSyncDurableObject(config) {
|
|
|
1345
1404
|
heapTotalBytes: heap?.totalJSHeapSize ?? null,
|
|
1346
1405
|
heapLimitBytes: heap?.jsHeapSizeLimit ?? null,
|
|
1347
1406
|
engine: this.#engineStateBestEffort(),
|
|
1407
|
+
sqlBillingSinceBoot: this.#sqlBilling,
|
|
1348
1408
|
counters: this.#counters,
|
|
1349
1409
|
ingestBreaker: this.#ingestBreaker.status(),
|
|
1350
1410
|
}));
|
|
@@ -1391,7 +1451,7 @@ export function createSyncDurableObject(config) {
|
|
|
1391
1451
|
});
|
|
1392
1452
|
}
|
|
1393
1453
|
catch (error) {
|
|
1394
|
-
return
|
|
1454
|
+
return errorResponse(error);
|
|
1395
1455
|
}
|
|
1396
1456
|
})();
|
|
1397
1457
|
}
|
|
@@ -1486,7 +1546,7 @@ export function createSyncDurableObject(config) {
|
|
|
1486
1546
|
return json({ ok: true, applied });
|
|
1487
1547
|
}
|
|
1488
1548
|
catch (error) {
|
|
1489
|
-
return
|
|
1549
|
+
return errorResponse(error);
|
|
1490
1550
|
}
|
|
1491
1551
|
}
|
|
1492
1552
|
if ((route === '/pull' || route === '/push') && request.method === 'POST') {
|
|
@@ -1495,14 +1555,14 @@ export function createSyncDurableObject(config) {
|
|
|
1495
1555
|
forwarded = await forwardedSyncRequest(request);
|
|
1496
1556
|
}
|
|
1497
1557
|
catch (error) {
|
|
1498
|
-
return
|
|
1558
|
+
return errorResponse(error);
|
|
1499
1559
|
}
|
|
1500
1560
|
// pull and push both establish the upstream schema and snapshot barrier.
|
|
1501
1561
|
try {
|
|
1502
1562
|
await this.#ingest(upstreamPath);
|
|
1503
1563
|
}
|
|
1504
1564
|
catch (error) {
|
|
1505
|
-
return
|
|
1565
|
+
return errorResponse(error);
|
|
1506
1566
|
}
|
|
1507
1567
|
if (route === '/pull') {
|
|
1508
1568
|
return this.#pull(forwarded.request, forwarded.claims, namespace);
|
|
@@ -27,8 +27,9 @@ export declare function decodeSqlParams(value: unknown): unknown[];
|
|
|
27
27
|
export declare class SqlStorageSyncDb implements JsSyncDb {
|
|
28
28
|
private readonly sql;
|
|
29
29
|
private readonly recordRowsWritten;
|
|
30
|
+
private readonly recordRowsRead;
|
|
30
31
|
readonly stats: AdapterStats;
|
|
31
|
-
constructor(sql: SqlStorage, recordRowsWritten?: (rows: number) => void);
|
|
32
|
+
constructor(sql: SqlStorage, recordRowsWritten?: (rows: number) => void, recordRowsRead?: (rows: number) => void);
|
|
32
33
|
resetStats(): void;
|
|
33
34
|
exec(sql: string, params: WireValue[]): void;
|
|
34
35
|
query(sql: string, params: WireValue[]): WireRow[];
|
|
@@ -37,7 +38,8 @@ export declare class SqlStorageSyncDb implements JsSyncDb {
|
|
|
37
38
|
export declare class SqlStorageDirect implements SyncSql {
|
|
38
39
|
private readonly sql;
|
|
39
40
|
private readonly recordRowsWritten;
|
|
40
|
-
|
|
41
|
+
private readonly recordRowsRead;
|
|
42
|
+
constructor(sql: SqlStorage, recordRowsWritten?: (rows: number) => void, recordRowsRead?: (rows: number) => void);
|
|
41
43
|
exec(sql: string, params?: readonly unknown[], _metadata?: SqlStatementMetadata): ExecResult;
|
|
42
44
|
query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Row[];
|
|
43
45
|
}
|
|
@@ -98,10 +98,12 @@ function encodeResult(value) {
|
|
|
98
98
|
export class SqlStorageSyncDb {
|
|
99
99
|
sql;
|
|
100
100
|
recordRowsWritten;
|
|
101
|
+
recordRowsRead;
|
|
101
102
|
stats = { execCalls: 0, queryCalls: 0, sqlMs: 0 };
|
|
102
|
-
constructor(sql, recordRowsWritten = () => { }) {
|
|
103
|
+
constructor(sql, recordRowsWritten = () => { }, recordRowsRead = () => { }) {
|
|
103
104
|
this.sql = sql;
|
|
104
105
|
this.recordRowsWritten = recordRowsWritten;
|
|
106
|
+
this.recordRowsRead = recordRowsRead;
|
|
105
107
|
}
|
|
106
108
|
resetStats() {
|
|
107
109
|
this.stats.execCalls = 0;
|
|
@@ -111,14 +113,14 @@ export class SqlStorageSyncDb {
|
|
|
111
113
|
exec(sql, params) {
|
|
112
114
|
assertHostSql(sql);
|
|
113
115
|
const start = performance.now();
|
|
114
|
-
trackBillableCursorRows(this.sql.exec(sql, ...params.map((value) => decodeBinding(value))), this.recordRowsWritten);
|
|
116
|
+
trackBillableCursorRows(this.sql.exec(sql, ...params.map((value) => decodeBinding(value))), this.recordRowsWritten, this.recordRowsRead);
|
|
115
117
|
this.stats.execCalls++;
|
|
116
118
|
this.stats.sqlMs += performance.now() - start;
|
|
117
119
|
}
|
|
118
120
|
query(sql, params) {
|
|
119
121
|
assertHostSql(sql);
|
|
120
122
|
const start = performance.now();
|
|
121
|
-
const cursor = trackBillableCursorRows(this.sql.exec(sql, ...params.map((value) => decodeBinding(value))), this.recordRowsWritten);
|
|
123
|
+
const cursor = trackBillableCursorRows(this.sql.exec(sql, ...params.map((value) => decodeBinding(value))), this.recordRowsWritten, this.recordRowsRead);
|
|
122
124
|
const columns = [...cursor.columnNames];
|
|
123
125
|
// A SqlStorage cursor must be fully consumed before any await. Returning a
|
|
124
126
|
// materialized array here makes that property structural.
|
|
@@ -135,19 +137,21 @@ export class SqlStorageSyncDb {
|
|
|
135
137
|
export class SqlStorageDirect {
|
|
136
138
|
sql;
|
|
137
139
|
recordRowsWritten;
|
|
138
|
-
|
|
140
|
+
recordRowsRead;
|
|
141
|
+
constructor(sql, recordRowsWritten = () => { }, recordRowsRead = () => { }) {
|
|
139
142
|
this.sql = sql;
|
|
140
143
|
this.recordRowsWritten = recordRowsWritten;
|
|
144
|
+
this.recordRowsRead = recordRowsRead;
|
|
141
145
|
}
|
|
142
146
|
exec(sql, params = [], _metadata) {
|
|
143
147
|
assertHostSql(sql);
|
|
144
|
-
trackBillableCursorRows(this.sql.exec(sql, ...params), this.recordRowsWritten).toArray();
|
|
145
|
-
const changes = Number(this.sql.exec('SELECT changes() AS changes').one()?.changes ?? 0);
|
|
148
|
+
trackBillableCursorRows(this.sql.exec(sql, ...params), this.recordRowsWritten, this.recordRowsRead).toArray();
|
|
149
|
+
const changes = Number(trackBillableCursorRows(this.sql.exec('SELECT changes() AS changes'), this.recordRowsWritten, this.recordRowsRead).one()?.changes ?? 0);
|
|
146
150
|
return { changes };
|
|
147
151
|
}
|
|
148
152
|
query(sql, params = []) {
|
|
149
153
|
assertHostSql(sql);
|
|
150
|
-
return trackBillableCursorRows(this.sql.exec(sql, ...params), this.recordRowsWritten).toArray();
|
|
154
|
+
return trackBillableCursorRows(this.sql.exec(sql, ...params), this.recordRowsWritten, this.recordRowsRead).toArray();
|
|
151
155
|
}
|
|
152
156
|
}
|
|
153
157
|
/**
|
package/dist/wasm.d.ts
CHANGED
|
@@ -18,5 +18,6 @@ export declare const engine_prune: (db: any, retain_changes: string) => void;
|
|
|
18
18
|
export declare const engine_push_validate: (body: any) => any;
|
|
19
19
|
export declare const engine_read_snapshot_progress: (db: any) => any;
|
|
20
20
|
export declare const engine_record_app_error: (db: any, client_group_id: string, client_id: string, mutation_id: string, user_id: string) => void;
|
|
21
|
+
export declare const engine_schema_revision: () => string;
|
|
21
22
|
export declare const engine_state: (db: any) => any;
|
|
22
23
|
export declare const engine_version: () => string;
|
package/dist/wasm.js
CHANGED
|
@@ -35,5 +35,6 @@ export const engine_prune = withWasm(generated.engine_prune);
|
|
|
35
35
|
export const engine_push_validate = withWasm(generated.engine_push_validate);
|
|
36
36
|
export const engine_read_snapshot_progress = withWasm(generated.engine_read_snapshot_progress);
|
|
37
37
|
export const engine_record_app_error = withWasm(generated.engine_record_app_error);
|
|
38
|
+
export const engine_schema_revision = withWasm(generated.engine_schema_revision);
|
|
38
39
|
export const engine_state = withWasm(generated.engine_state);
|
|
39
40
|
export const engine_version = withWasm(generated.engine_version);
|
|
@@ -40,6 +40,7 @@ export declare class IngestCircuitBreaker {
|
|
|
40
40
|
reopen(): void;
|
|
41
41
|
}
|
|
42
42
|
type BillableCursor = {
|
|
43
|
+
rowsRead?: number;
|
|
43
44
|
rowsWritten?: number;
|
|
44
45
|
next?: (...args: unknown[]) => unknown;
|
|
45
46
|
one?: (...args: unknown[]) => unknown;
|
|
@@ -47,7 +48,7 @@ type BillableCursor = {
|
|
|
47
48
|
raw?: (...args: unknown[]) => unknown;
|
|
48
49
|
[Symbol.iterator]?: () => Iterator<unknown>;
|
|
49
50
|
};
|
|
50
|
-
export declare function trackBillableCursorRows<Cursor extends BillableCursor>(cursor: Cursor,
|
|
51
|
+
export declare function trackBillableCursorRows<Cursor extends BillableCursor>(cursor: Cursor, recordWritten: (rows: number) => void, recordRead?: (rows: number) => void): Cursor;
|
|
51
52
|
export declare function retryDelayMs(attempt: number, initialBackoffMs: number, maxBackoffMs: number): number;
|
|
52
53
|
export declare function shouldRetryDelegatedPush(responseStatus: number | null, attempt: number, maxAttempts: number): boolean;
|
|
53
54
|
export {};
|
package/dist/write-safeguards.js
CHANGED
|
@@ -122,17 +122,22 @@ export class IngestCircuitBreaker {
|
|
|
122
122
|
this.recovered();
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
export function trackBillableCursorRows(cursor,
|
|
125
|
+
export function trackBillableCursorRows(cursor, recordWritten, recordRead = () => { }) {
|
|
126
126
|
if (!cursor || typeof cursor !== 'object')
|
|
127
127
|
return cursor;
|
|
128
|
-
let
|
|
128
|
+
let accountedWritten = 0;
|
|
129
|
+
let accountedRead = 0;
|
|
129
130
|
const account = () => {
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|
|
136
141
|
};
|
|
137
142
|
const wrapIterator = (iterator) => {
|
|
138
143
|
const next = iterator.next.bind(iterator);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orez-sync-cf-host",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
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.
|
|
61
|
+
"orez-sync-executor": "0.12.3"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@cloudflare/workers-types": "4.20260617.1",
|