@creatornader/agent-bridge 0.6.1 → 0.6.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/CHANGELOG.md +27 -1
- package/README.md +6 -3
- package/ROADMAP.md +16 -2
- package/dist/bridge.d.ts +1 -1
- package/dist/bridge.js +2 -2
- package/dist/{chunk-IVG4XWWS.js → chunk-7PYY3S27.js} +281 -189
- package/dist/chunk-7PYY3S27.js.map +1 -0
- package/dist/{chunk-IF5OKPBK.js → chunk-7Q5THTJH.js} +2 -2
- package/dist/{chunk-JIRNR75D.js → chunk-DHGJN6NA.js} +2 -2
- package/dist/{chunk-IXNQG4SB.js → chunk-TZHXTMPJ.js} +3 -3
- package/dist/{chunk-ZUZ7TCIH.js → chunk-U4M67GFU.js} +323 -226
- package/dist/chunk-U4M67GFU.js.map +1 -0
- package/dist/cli.js +55 -13
- package/dist/cli.js.map +1 -1
- package/dist/client-runtime.d.ts +1 -1
- package/dist/client-runtime.js +3 -3
- package/dist/dr.d.ts +18 -0
- package/dist/dr.js +2 -2
- package/dist/index.js +4 -4
- package/dist/server.js +4 -4
- package/dist/{sqlite-edge-store-BAFUeysk.d.ts → sqlite-edge-store-C-pVhdgx.d.ts} +5 -1
- package/dist/sqlite.d.ts +4 -2
- package/dist/sqlite.js +2 -2
- package/docs/architecture-v2.md +6 -0
- package/docs/deployment.md +6 -3
- package/docs/troubleshooting.md +17 -0
- package/package.json +5 -2
- package/dist/chunk-IVG4XWWS.js.map +0 -1
- package/dist/chunk-ZUZ7TCIH.js.map +0 -1
- /package/dist/{chunk-IF5OKPBK.js.map → chunk-7Q5THTJH.js.map} +0 -0
- /package/dist/{chunk-JIRNR75D.js.map → chunk-DHGJN6NA.js.map} +0 -0
- /package/dist/{chunk-IXNQG4SB.js.map → chunk-TZHXTMPJ.js.map} +0 -0
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
assertEdgeUpgradeCandidate,
|
|
5
5
|
installEdgeMarkers,
|
|
6
6
|
retrySqliteBusy
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7PYY3S27.js";
|
|
8
8
|
import {
|
|
9
9
|
preparePrivateSqliteLocation,
|
|
10
10
|
securePrivatePath,
|
|
@@ -33,9 +33,6 @@ function openReadOnlyDatabase(path) {
|
|
|
33
33
|
}
|
|
34
34
|
var MAX_SEQUENCE = 9223372036854775807n;
|
|
35
35
|
var schema = `
|
|
36
|
-
PRAGMA journal_mode = WAL;
|
|
37
|
-
PRAGMA synchronous = FULL;
|
|
38
|
-
PRAGMA foreign_keys = ON;
|
|
39
36
|
CREATE TABLE IF NOT EXISTS edge_scopes (
|
|
40
37
|
scope_key TEXT PRIMARY KEY,
|
|
41
38
|
endpoint_hash TEXT NOT NULL,
|
|
@@ -118,6 +115,7 @@ CREATE INDEX IF NOT EXISTS edge_inbox_thread
|
|
|
118
115
|
CREATE INDEX IF NOT EXISTS edge_inbox_created
|
|
119
116
|
ON edge_inbox(scope_key, created_at, sequence_key);
|
|
120
117
|
`;
|
|
118
|
+
var writeGateSchema = "CREATE TABLE IF NOT EXISTS edge_write_gates (gate_key TEXT PRIMARY KEY, lease_token TEXT, lease_expires_at TEXT)";
|
|
121
119
|
function inspectEdgeScopeReadOnly(path, scope, now = /* @__PURE__ */ new Date()) {
|
|
122
120
|
if (!existsSync(path)) {
|
|
123
121
|
return { exists: false, gate: { scopeKey: edgeScopeKey(scope), state: "active", operationId: void 0, leaseExpiresAt: void 0, updatedAt: (/* @__PURE__ */ new Date(0)).toISOString() }, pending: 0, due: 0, scheduled: 0, leased: 0, blocked: 0, cached: 0 };
|
|
@@ -130,13 +128,16 @@ function inspectEdgeScopeReadOnly(path, scope, now = /* @__PURE__ */ new Date())
|
|
|
130
128
|
}
|
|
131
129
|
const db = openReadOnlyDatabase(path);
|
|
132
130
|
try {
|
|
133
|
-
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('edge_scopes','edge_migration_gates','edge_outbox','edge_inbox') ORDER BY name").all();
|
|
131
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('edge_scopes','edge_migration_gates','edge_outbox','edge_inbox','edge_write_gates') ORDER BY name").all();
|
|
134
132
|
if (tables.length === 0) {
|
|
135
133
|
const userTables = db.prepare("SELECT count(*) AS count FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").get();
|
|
136
134
|
if (Number(userTables.count) !== 0) throw new EdgeConflictError("edge database is not an Agent Bridge edge store");
|
|
137
135
|
return { exists: true, gate: { scopeKey: edgeScopeKey(scope), state: "active", operationId: void 0, leaseExpiresAt: void 0, updatedAt: (/* @__PURE__ */ new Date(0)).toISOString() }, pending: 0, due: 0, scheduled: 0, leased: 0, blocked: 0, cached: 0 };
|
|
138
136
|
}
|
|
139
|
-
|
|
137
|
+
const names = new Set(tables.map((table) => String(table.name)));
|
|
138
|
+
for (const name of ["edge_scopes", "edge_migration_gates", "edge_outbox", "edge_inbox"]) {
|
|
139
|
+
if (!names.has(name)) throw new EdgeConflictError("edge database schema is incomplete");
|
|
140
|
+
}
|
|
140
141
|
const key = edgeScopeKey(scope);
|
|
141
142
|
const scopeRow = db.prepare("SELECT 1 AS present FROM edge_scopes WHERE scope_key=?").get(key);
|
|
142
143
|
if (!scopeRow) {
|
|
@@ -250,10 +251,12 @@ function safeOperationId(value) {
|
|
|
250
251
|
return value.toLowerCase();
|
|
251
252
|
}
|
|
252
253
|
var SQLiteEdgeStore = class {
|
|
253
|
-
constructor(path, scope, busyTimeoutMs = 2e3) {
|
|
254
|
+
constructor(path, scope, busyTimeoutMs = 2e3, writeGateLeaseMs = 3e4, writeGateWaitMs = process.platform === "win32" ? 6e4 : SQLITE_INITIALIZATION_BUSY_TIMEOUT_MS) {
|
|
254
255
|
this.path = path;
|
|
255
256
|
this.scope = scope;
|
|
256
257
|
this.busyTimeoutMs = busyTimeoutMs;
|
|
258
|
+
this.writeGateLeaseMs = writeGateLeaseMs;
|
|
259
|
+
this.writeGateWaitMs = writeGateWaitMs;
|
|
257
260
|
this.initialized = false;
|
|
258
261
|
const selected = preparePrivateSqliteLocation(path, true, "defer");
|
|
259
262
|
this.databasePath = selected;
|
|
@@ -273,6 +276,74 @@ var SQLiteEdgeStore = class {
|
|
|
273
276
|
}
|
|
274
277
|
this.key = edgeScopeKey(scope);
|
|
275
278
|
}
|
|
279
|
+
async write(work) {
|
|
280
|
+
if (this.databasePath === ":memory:") return work();
|
|
281
|
+
const token = randomUUID();
|
|
282
|
+
const deadline = Date.now() + Math.max(1, Math.trunc(this.writeGateWaitMs));
|
|
283
|
+
let pause = 2;
|
|
284
|
+
while (true) {
|
|
285
|
+
const now = /* @__PURE__ */ new Date();
|
|
286
|
+
const claimed = await retrySqliteBusy(() => {
|
|
287
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
288
|
+
try {
|
|
289
|
+
this.db.prepare("INSERT INTO edge_write_gates (gate_key,lease_token,lease_expires_at) VALUES ('edge',NULL,NULL) ON CONFLICT(gate_key) DO NOTHING").run();
|
|
290
|
+
const update = this.db.prepare(`UPDATE edge_write_gates SET lease_token=?,lease_expires_at=?
|
|
291
|
+
WHERE gate_key='edge' AND (lease_token IS NULL OR lease_expires_at<=?)`).run(
|
|
292
|
+
token,
|
|
293
|
+
new Date(now.getTime() + Math.max(1, Math.trunc(this.writeGateLeaseMs))).toISOString(),
|
|
294
|
+
now.toISOString()
|
|
295
|
+
);
|
|
296
|
+
this.db.exec("COMMIT");
|
|
297
|
+
return update.changes === 1;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
this.db.exec("ROLLBACK");
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}, Math.max(1, deadline - Date.now()));
|
|
303
|
+
if (claimed) break;
|
|
304
|
+
if (Date.now() >= deadline) throw new EdgeConflictError("edge write coordinator remained busy");
|
|
305
|
+
await new Promise((resolve) => setTimeout(resolve, pause));
|
|
306
|
+
pause = Math.min(Math.ceil(pause * 1.7), 25);
|
|
307
|
+
}
|
|
308
|
+
let completed = false;
|
|
309
|
+
let result;
|
|
310
|
+
try {
|
|
311
|
+
result = await work();
|
|
312
|
+
completed = true;
|
|
313
|
+
} finally {
|
|
314
|
+
try {
|
|
315
|
+
await retrySqliteBusy(() => {
|
|
316
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
317
|
+
try {
|
|
318
|
+
this.db.prepare("UPDATE edge_write_gates SET lease_token=NULL,lease_expires_at=NULL WHERE gate_key='edge' AND lease_token=?").run(token);
|
|
319
|
+
this.db.exec("COMMIT");
|
|
320
|
+
} catch (error) {
|
|
321
|
+
this.db.exec("ROLLBACK");
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}, Math.max(1, Math.trunc(this.writeGateWaitMs)));
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (!completed) throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
async bootstrapWriteGate(timeoutMs) {
|
|
332
|
+
await retrySqliteBusy(() => this.db.exec("PRAGMA journal_mode = WAL"), timeoutMs);
|
|
333
|
+
const exists = () => Boolean(this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='edge_write_gates'").get());
|
|
334
|
+
if (exists()) return;
|
|
335
|
+
await retrySqliteBusy(() => {
|
|
336
|
+
if (exists()) return;
|
|
337
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
338
|
+
try {
|
|
339
|
+
if (!exists()) this.db.exec(writeGateSchema);
|
|
340
|
+
this.db.exec("COMMIT");
|
|
341
|
+
} catch (error) {
|
|
342
|
+
this.db.exec("ROLLBACK");
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}, timeoutMs);
|
|
346
|
+
}
|
|
276
347
|
restrictFiles(includeSidecars = true) {
|
|
277
348
|
if (this.databasePath === ":memory:") return;
|
|
278
349
|
const paths = includeSidecars ? [this.databasePath, `${this.databasePath}-wal`, `${this.databasePath}-shm`] : [this.databasePath];
|
|
@@ -341,52 +412,56 @@ var SQLiteEdgeStore = class {
|
|
|
341
412
|
async initializeOnce() {
|
|
342
413
|
assertEdgeUpgradeCandidate(this.db);
|
|
343
414
|
const initializationTimeoutMs = Math.max(this.busyTimeoutMs, SQLITE_INITIALIZATION_BUSY_TIMEOUT_MS);
|
|
344
|
-
await
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
this.db.exec(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
this.db.exec(`ALTER TABLE edge_scopes ADD COLUMN ${name} TEXT`);
|
|
415
|
+
await this.bootstrapWriteGate(initializationTimeoutMs);
|
|
416
|
+
this.db.exec("PRAGMA synchronous = FULL; PRAGMA foreign_keys = ON");
|
|
417
|
+
await this.write(async () => {
|
|
418
|
+
await retrySqliteBusy(() => this.db.exec("BEGIN IMMEDIATE"), initializationTimeoutMs);
|
|
419
|
+
try {
|
|
420
|
+
this.db.exec(schema);
|
|
421
|
+
const scopeColumns = this.db.prepare("PRAGMA table_info(edge_scopes)").all();
|
|
422
|
+
if (!scopeColumns.some((column) => column.name === "cache_contract")) {
|
|
423
|
+
this.db.exec("ALTER TABLE edge_scopes ADD COLUMN cache_contract INTEGER NOT NULL DEFAULT 0");
|
|
354
424
|
}
|
|
425
|
+
for (const name of ["last_outbound_sync_at", "last_inbound_sync_at", "last_attempt_at"]) {
|
|
426
|
+
if (!scopeColumns.some((column) => column.name === name)) {
|
|
427
|
+
this.db.exec(`ALTER TABLE edge_scopes ADD COLUMN ${name} TEXT`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const columns = this.db.prepare("PRAGMA table_info(edge_inbox)").all();
|
|
431
|
+
if (!columns.some((column) => column.name === "project")) {
|
|
432
|
+
this.db.exec("ALTER TABLE edge_inbox ADD COLUMN project TEXT");
|
|
433
|
+
}
|
|
434
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS edge_inbox_project ON edge_inbox(scope_key, project, sequence_key)");
|
|
435
|
+
const outboxColumns = this.db.prepare("PRAGMA table_info(edge_outbox)").all();
|
|
436
|
+
if (!outboxColumns.some((column) => column.name === "blocked_at")) {
|
|
437
|
+
this.db.exec("ALTER TABLE edge_outbox ADD COLUMN blocked_at TEXT");
|
|
438
|
+
}
|
|
439
|
+
installEdgeMarkers(this.db);
|
|
440
|
+
this.db.exec("COMMIT");
|
|
441
|
+
} catch (error) {
|
|
442
|
+
this.db.exec("ROLLBACK");
|
|
443
|
+
throw error;
|
|
355
444
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
this.db.exec("CREATE INDEX IF NOT EXISTS edge_inbox_project ON edge_inbox(scope_key, project, sequence_key)");
|
|
361
|
-
const outboxColumns = this.db.prepare("PRAGMA table_info(edge_outbox)").all();
|
|
362
|
-
if (!outboxColumns.some((column) => column.name === "blocked_at")) {
|
|
363
|
-
this.db.exec("ALTER TABLE edge_outbox ADD COLUMN blocked_at TEXT");
|
|
364
|
-
}
|
|
365
|
-
installEdgeMarkers(this.db);
|
|
366
|
-
this.db.exec("COMMIT");
|
|
367
|
-
} catch (error) {
|
|
368
|
-
this.db.exec("ROLLBACK");
|
|
369
|
-
throw error;
|
|
370
|
-
}
|
|
371
|
-
this.restrictFiles();
|
|
372
|
-
const endpointHash = createHash("sha256").update(this.scope.endpoint.replace(/\/$/, "")).digest("hex");
|
|
373
|
-
this.db.prepare(`INSERT INTO edge_scopes (scope_key, endpoint_hash, workspace, agent)
|
|
445
|
+
this.restrictFiles();
|
|
446
|
+
const endpointHash = createHash("sha256").update(this.scope.endpoint.replace(/\/$/, "")).digest("hex");
|
|
447
|
+
this.db.prepare(`INSERT INTO edge_scopes (scope_key, endpoint_hash, workspace, agent)
|
|
374
448
|
VALUES (?, ?, ?, ?) ON CONFLICT(scope_key) DO NOTHING`).run(this.key, endpointHash, this.scope.principal.workspace, this.scope.principal.agent);
|
|
375
|
-
|
|
449
|
+
this.db.prepare(`INSERT INTO edge_migration_gates
|
|
376
450
|
(scope_key,state,operation_id,lease_token,lease_expires_at,updated_at)
|
|
377
451
|
SELECT scope_key,'active',NULL,NULL,NULL,? FROM edge_scopes
|
|
378
452
|
WHERE scope_key=? ON CONFLICT(scope_key) DO NOTHING`).run((/* @__PURE__ */ new Date()).toISOString(), this.key);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
453
|
+
const contract = this.db.prepare("SELECT cache_contract FROM edge_scopes WHERE scope_key=?").get(this.key);
|
|
454
|
+
if (Number(contract.cache_contract) < 1) {
|
|
455
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
456
|
+
try {
|
|
457
|
+
this.db.prepare("UPDATE edge_scopes SET pull_cursor=NULL, cache_contract=1 WHERE scope_key=?").run(this.key);
|
|
458
|
+
this.db.exec("COMMIT");
|
|
459
|
+
} catch (error) {
|
|
460
|
+
this.db.exec("ROLLBACK");
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
388
463
|
}
|
|
389
|
-
}
|
|
464
|
+
});
|
|
390
465
|
}
|
|
391
466
|
async ready() {
|
|
392
467
|
await this.initialize();
|
|
@@ -398,60 +473,66 @@ var SQLiteEdgeStore = class {
|
|
|
398
473
|
/** Refuse new normal-client writes before any remote attempt begins. */
|
|
399
474
|
async assertScopeActive() {
|
|
400
475
|
await this.ready();
|
|
401
|
-
this.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
476
|
+
await this.write(() => {
|
|
477
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
478
|
+
try {
|
|
479
|
+
this.assertScopeActiveInTransaction();
|
|
480
|
+
this.db.exec("COMMIT");
|
|
481
|
+
} catch (error) {
|
|
482
|
+
this.db.exec("ROLLBACK");
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
409
486
|
}
|
|
410
487
|
/** Refuse remote publication unless this worker still owns the durable drain lease. */
|
|
411
488
|
async assertDrainLease(lease, now = /* @__PURE__ */ new Date()) {
|
|
412
489
|
await this.ready();
|
|
413
490
|
if (lease.scopeKey !== this.key) throw new EdgeMigrationLeaseError();
|
|
414
|
-
this.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
491
|
+
await this.write(() => {
|
|
492
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
493
|
+
try {
|
|
494
|
+
this.assertDrainLeaseInTransaction(lease, now);
|
|
495
|
+
this.db.exec("COMMIT");
|
|
496
|
+
} catch (error) {
|
|
497
|
+
this.db.exec("ROLLBACK");
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
});
|
|
422
501
|
}
|
|
423
502
|
async claimDrainLease(operationId, now, leaseMs, mode) {
|
|
424
503
|
await this.ready();
|
|
425
504
|
const normalizedOperationId = safeOperationId(operationId);
|
|
426
505
|
const duration = Math.max(1, Math.trunc(leaseMs));
|
|
427
|
-
this.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
|
|
506
|
+
return this.write(() => {
|
|
507
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
508
|
+
try {
|
|
509
|
+
const gate = this.gateInTransaction();
|
|
510
|
+
if (gate.state === "retired") throw new EdgeMigrationRetiredError(gate);
|
|
511
|
+
if (mode === "begin" && gate.state !== "active") throw new EdgeMigrationDrainingError(gate);
|
|
512
|
+
if (mode === "resume") {
|
|
513
|
+
if (gate.state !== "draining" || gate.operationId !== normalizedOperationId || !gate.leaseExpiresAt || Date.parse(gate.leaseExpiresAt) > now.getTime()) {
|
|
514
|
+
throw new EdgeMigrationDrainingError(gate);
|
|
515
|
+
}
|
|
435
516
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const updated = this.db.prepare(`UPDATE edge_migration_gates
|
|
517
|
+
const leaseToken = randomUUID();
|
|
518
|
+
const leaseExpiresAt = new Date(now.getTime() + duration).toISOString();
|
|
519
|
+
const updated = this.db.prepare(`UPDATE edge_migration_gates
|
|
440
520
|
SET state='draining',operation_id=?,lease_token=?,lease_expires_at=?,updated_at=?
|
|
441
521
|
WHERE scope_key=?`).run(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
522
|
+
normalizedOperationId,
|
|
523
|
+
leaseToken,
|
|
524
|
+
leaseExpiresAt,
|
|
525
|
+
now.toISOString(),
|
|
526
|
+
this.key
|
|
527
|
+
);
|
|
528
|
+
if (updated.changes !== 1) throw new EdgeConflictError("edge migration gate could not be acquired");
|
|
529
|
+
this.db.exec("COMMIT");
|
|
530
|
+
return { scopeKey: this.key, operationId: normalizedOperationId, leaseToken, leaseExpiresAt };
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.db.exec("ROLLBACK");
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
});
|
|
455
536
|
}
|
|
456
537
|
/** Start a new durable drain only from an active edge scope. */
|
|
457
538
|
async beginDrain(operationId, now = /* @__PURE__ */ new Date(), leaseMs = 3e4) {
|
|
@@ -471,81 +552,87 @@ var SQLiteEdgeStore = class {
|
|
|
471
552
|
async assertDrainComplete(lease, now = /* @__PURE__ */ new Date()) {
|
|
472
553
|
await this.ready();
|
|
473
554
|
if (lease.scopeKey !== this.key) throw new EdgeMigrationLeaseError();
|
|
474
|
-
this.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
555
|
+
await this.write(() => {
|
|
556
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
557
|
+
try {
|
|
558
|
+
this.assertDrainLeaseInTransaction(lease, now);
|
|
559
|
+
const counts = this.db.prepare(`SELECT
|
|
478
560
|
sum(CASE WHEN state='pending' THEN 1 ELSE 0 END) AS pending,
|
|
479
561
|
sum(CASE WHEN state='pending' AND available_at<=? AND (lease_expires_at IS NULL OR lease_expires_at<=?) THEN 1 ELSE 0 END) AS due,
|
|
480
562
|
sum(CASE WHEN state='pending' AND available_at>? AND (lease_expires_at IS NULL OR lease_expires_at<=?) THEN 1 ELSE 0 END) AS scheduled,
|
|
481
563
|
sum(CASE WHEN state='pending' AND lease_expires_at>? THEN 1 ELSE 0 END) AS leased,
|
|
482
564
|
sum(CASE WHEN state='blocked' THEN 1 ELSE 0 END) AS blocked
|
|
483
565
|
FROM edge_outbox WHERE scope_key=?`).get(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
566
|
+
now.toISOString(),
|
|
567
|
+
now.toISOString(),
|
|
568
|
+
now.toISOString(),
|
|
569
|
+
now.toISOString(),
|
|
570
|
+
now.toISOString(),
|
|
571
|
+
this.key
|
|
572
|
+
);
|
|
573
|
+
if (Number(counts.pending ?? 0) !== 0 || Number(counts.due ?? 0) !== 0 || Number(counts.scheduled ?? 0) !== 0 || Number(counts.leased ?? 0) !== 0 || Number(counts.blocked ?? 0) !== 0) {
|
|
574
|
+
throw new EdgeConflictError("edge scope cannot complete a drain with retained outbox work");
|
|
575
|
+
}
|
|
576
|
+
this.db.exec("COMMIT");
|
|
577
|
+
} catch (error) {
|
|
578
|
+
this.db.exec("ROLLBACK");
|
|
579
|
+
throw error;
|
|
493
580
|
}
|
|
494
|
-
|
|
495
|
-
} catch (error) {
|
|
496
|
-
this.db.exec("ROLLBACK");
|
|
497
|
-
throw error;
|
|
498
|
-
}
|
|
581
|
+
});
|
|
499
582
|
}
|
|
500
583
|
async renewDrainLease(lease, now = /* @__PURE__ */ new Date(), leaseMs = 3e4) {
|
|
501
584
|
await this.ready();
|
|
502
585
|
if (lease.scopeKey !== this.key) throw new EdgeMigrationLeaseError();
|
|
503
586
|
const duration = Math.max(1, Math.trunc(leaseMs));
|
|
504
|
-
this.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
587
|
+
return this.write(() => {
|
|
588
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
589
|
+
try {
|
|
590
|
+
this.assertDrainLeaseInTransaction(lease, now);
|
|
591
|
+
const leaseExpiresAt = new Date(now.getTime() + duration).toISOString();
|
|
592
|
+
const updated = this.db.prepare(`UPDATE edge_migration_gates
|
|
509
593
|
SET lease_expires_at=?,updated_at=? WHERE scope_key=? AND state='draining'
|
|
510
594
|
AND operation_id=? AND lease_token=? AND lease_expires_at=?`).run(
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
595
|
+
leaseExpiresAt,
|
|
596
|
+
now.toISOString(),
|
|
597
|
+
this.key,
|
|
598
|
+
lease.operationId,
|
|
599
|
+
lease.leaseToken,
|
|
600
|
+
lease.leaseExpiresAt
|
|
601
|
+
);
|
|
602
|
+
if (updated.changes !== 1) throw new EdgeMigrationLeaseError();
|
|
603
|
+
this.db.exec("COMMIT");
|
|
604
|
+
return { ...lease, leaseExpiresAt };
|
|
605
|
+
} catch (error) {
|
|
606
|
+
this.db.exec("ROLLBACK");
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
});
|
|
525
610
|
}
|
|
526
611
|
async retireScope(lease, now = /* @__PURE__ */ new Date()) {
|
|
527
612
|
await this.ready();
|
|
528
613
|
if (lease.scopeKey !== this.key) throw new EdgeMigrationLeaseError();
|
|
529
|
-
this.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
614
|
+
await this.write(() => {
|
|
615
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
616
|
+
try {
|
|
617
|
+
this.assertDrainLeaseInTransaction(lease, now);
|
|
618
|
+
const stats = this.db.prepare(`SELECT count(*) AS count FROM edge_outbox
|
|
533
619
|
WHERE scope_key=?`).get(this.key);
|
|
534
|
-
|
|
535
|
-
|
|
620
|
+
if (Number(stats.count) !== 0) throw new EdgeConflictError("edge scope cannot retire with retained outbox work");
|
|
621
|
+
const updated = this.db.prepare(`UPDATE edge_migration_gates
|
|
536
622
|
SET state='retired',lease_token=NULL,lease_expires_at=NULL,updated_at=?
|
|
537
623
|
WHERE scope_key=? AND state='draining' AND operation_id=? AND lease_token=?`).run(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
624
|
+
now.toISOString(),
|
|
625
|
+
this.key,
|
|
626
|
+
lease.operationId,
|
|
627
|
+
lease.leaseToken
|
|
628
|
+
);
|
|
629
|
+
if (updated.changes !== 1) throw new EdgeMigrationLeaseError();
|
|
630
|
+
this.db.exec("COMMIT");
|
|
631
|
+
} catch (error) {
|
|
632
|
+
this.db.exec("ROLLBACK");
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
});
|
|
549
636
|
}
|
|
550
637
|
async enqueue(input, now = /* @__PURE__ */ new Date()) {
|
|
551
638
|
await this.ready();
|
|
@@ -553,94 +640,100 @@ var SQLiteEdgeStore = class {
|
|
|
553
640
|
const payloadHash = edgeMessageFingerprint(draft);
|
|
554
641
|
const serialized = canonical(draft);
|
|
555
642
|
const nowText = now.toISOString();
|
|
556
|
-
this.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
643
|
+
return this.write(() => {
|
|
644
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
645
|
+
try {
|
|
646
|
+
this.assertScopeActiveInTransaction();
|
|
647
|
+
const existing = this.db.prepare(`SELECT * FROM edge_outbox
|
|
560
648
|
WHERE scope_key=? AND (message_id=? OR idempotency_key=?) ORDER BY position LIMIT 1`).get(this.key, draft.id, draft.idempotencyKey);
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
649
|
+
if (existing) {
|
|
650
|
+
if (String(existing.payload_hash) !== payloadHash) {
|
|
651
|
+
throw new EdgeConflictError("message ID or idempotency key is already bound to different content");
|
|
652
|
+
}
|
|
653
|
+
this.db.exec("COMMIT");
|
|
654
|
+
return { draft: parseDraft(existing.draft_json), created: false };
|
|
564
655
|
}
|
|
565
|
-
this.db.
|
|
566
|
-
return { draft: parseDraft(existing.draft_json), created: false };
|
|
567
|
-
}
|
|
568
|
-
this.db.prepare(`INSERT INTO edge_outbox
|
|
656
|
+
this.db.prepare(`INSERT INTO edge_outbox
|
|
569
657
|
(scope_key, message_id, idempotency_key, payload_hash, draft_json, state, available_at, created_at)
|
|
570
658
|
VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)`).run(this.key, draft.id, draft.idempotencyKey, payloadHash, serialized, nowText, nowText);
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
659
|
+
this.db.exec("COMMIT");
|
|
660
|
+
return { draft, created: true };
|
|
661
|
+
} catch (error) {
|
|
662
|
+
this.db.exec("ROLLBACK");
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
});
|
|
577
666
|
}
|
|
578
667
|
async claimNext(now = /* @__PURE__ */ new Date(), leaseMs = 3e4, migrationLease) {
|
|
579
668
|
await this.ready();
|
|
580
669
|
const nowText = now.toISOString();
|
|
581
|
-
this.
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
670
|
+
return this.write(() => {
|
|
671
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
672
|
+
try {
|
|
673
|
+
this.assertClaimAuthorityInTransaction(now, migrationLease);
|
|
674
|
+
const head = this.db.prepare(`SELECT * FROM edge_outbox
|
|
585
675
|
WHERE scope_key=? AND state='pending' AND available_at<=?
|
|
586
676
|
AND (lease_expires_at IS NULL OR lease_expires_at<=?)
|
|
587
677
|
ORDER BY position LIMIT 1`).get(this.key, nowText, nowText);
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
678
|
+
if (!head) {
|
|
679
|
+
this.db.exec("COMMIT");
|
|
680
|
+
return void 0;
|
|
681
|
+
}
|
|
682
|
+
const leaseToken = randomUUID();
|
|
683
|
+
const leaseExpiresAt = new Date(now.getTime() + leaseMs).toISOString();
|
|
684
|
+
const updated = this.db.prepare(`UPDATE edge_outbox
|
|
595
685
|
SET lease_token=?, lease_expires_at=? WHERE scope_key=? AND position=?
|
|
596
686
|
AND (lease_expires_at IS NULL OR lease_expires_at<=?)`).run(leaseToken, leaseExpiresAt, this.key, Number(head.position), nowText);
|
|
597
|
-
|
|
687
|
+
if (!updated.changes) {
|
|
688
|
+
this.db.exec("COMMIT");
|
|
689
|
+
return void 0;
|
|
690
|
+
}
|
|
598
691
|
this.db.exec("COMMIT");
|
|
599
|
-
return
|
|
692
|
+
return {
|
|
693
|
+
position: Number(head.position),
|
|
694
|
+
draft: parseDraft(head.draft_json),
|
|
695
|
+
payloadHash: String(head.payload_hash),
|
|
696
|
+
attempts: Number(head.attempts),
|
|
697
|
+
leaseToken,
|
|
698
|
+
createdAt: String(head.created_at)
|
|
699
|
+
};
|
|
700
|
+
} catch (error) {
|
|
701
|
+
this.db.exec("ROLLBACK");
|
|
702
|
+
throw error;
|
|
600
703
|
}
|
|
601
|
-
|
|
602
|
-
return {
|
|
603
|
-
position: Number(head.position),
|
|
604
|
-
draft: parseDraft(head.draft_json),
|
|
605
|
-
payloadHash: String(head.payload_hash),
|
|
606
|
-
attempts: Number(head.attempts),
|
|
607
|
-
leaseToken,
|
|
608
|
-
createdAt: String(head.created_at)
|
|
609
|
-
};
|
|
610
|
-
} catch (error) {
|
|
611
|
-
this.db.exec("ROLLBACK");
|
|
612
|
-
throw error;
|
|
613
|
-
}
|
|
704
|
+
});
|
|
614
705
|
}
|
|
615
706
|
async retry(record, error, availableAt) {
|
|
616
707
|
await this.ready();
|
|
617
|
-
this.db.prepare(`UPDATE edge_outbox SET attempts=attempts+1, available_at=?,
|
|
708
|
+
await this.write(() => this.db.prepare(`UPDATE edge_outbox SET attempts=attempts+1, available_at=?,
|
|
618
709
|
lease_token=NULL, lease_expires_at=NULL, last_error=?
|
|
619
|
-
WHERE scope_key=? AND position=? AND lease_token=?`).run(availableAt.toISOString(), error.slice(0, 256), this.key, record.position, record.leaseToken);
|
|
710
|
+
WHERE scope_key=? AND position=? AND lease_token=?`).run(availableAt.toISOString(), error.slice(0, 256), this.key, record.position, record.leaseToken));
|
|
620
711
|
await this.noteError(error);
|
|
621
712
|
}
|
|
622
713
|
async block(record, error, now = /* @__PURE__ */ new Date()) {
|
|
623
714
|
await this.ready();
|
|
624
|
-
this.db.prepare(`UPDATE edge_outbox SET state='blocked', attempts=attempts+1,
|
|
715
|
+
await this.write(() => this.db.prepare(`UPDATE edge_outbox SET state='blocked', attempts=attempts+1,
|
|
625
716
|
lease_token=NULL, lease_expires_at=NULL, last_error=?, blocked_at=?
|
|
626
|
-
WHERE scope_key=? AND position=? AND lease_token=?`).run(error.slice(0, 256), now.toISOString(), this.key, record.position, record.leaseToken);
|
|
717
|
+
WHERE scope_key=? AND position=? AND lease_token=?`).run(error.slice(0, 256), now.toISOString(), this.key, record.position, record.leaseToken));
|
|
627
718
|
await this.noteError(error);
|
|
628
719
|
}
|
|
629
720
|
async commit(record, message, now = /* @__PURE__ */ new Date()) {
|
|
630
721
|
await this.ready();
|
|
631
|
-
this.
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
722
|
+
await this.write(() => {
|
|
723
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
724
|
+
try {
|
|
725
|
+
this.cacheOne(message);
|
|
726
|
+
const deleted = this.db.prepare(`DELETE FROM edge_outbox
|
|
635
727
|
WHERE scope_key=? AND position=? AND lease_token=?`).run(this.key, record.position, record.leaseToken);
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
728
|
+
if (deleted.changes !== 1) throw new EdgeConflictError("outbox lease was lost before commit");
|
|
729
|
+
this.db.prepare(`UPDATE edge_scopes SET last_sync_at=?, last_error=NULL WHERE scope_key=?`).run(now.toISOString(), this.key);
|
|
730
|
+
this.db.prepare("UPDATE edge_scopes SET last_outbound_sync_at=? WHERE scope_key=?").run(now.toISOString(), this.key);
|
|
731
|
+
this.db.exec("COMMIT");
|
|
732
|
+
} catch (error) {
|
|
733
|
+
this.db.exec("ROLLBACK");
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
});
|
|
644
737
|
}
|
|
645
738
|
cacheOne(message) {
|
|
646
739
|
const serialized = canonical(message);
|
|
@@ -676,33 +769,37 @@ var SQLiteEdgeStore = class {
|
|
|
676
769
|
async cachePage(messages, cursor, now = /* @__PURE__ */ new Date()) {
|
|
677
770
|
await this.ready();
|
|
678
771
|
const scope = cursorScope(this.scope.principal, { mailbox: "all", includeExpired: true });
|
|
679
|
-
this.
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
772
|
+
await this.write(() => {
|
|
773
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
774
|
+
try {
|
|
775
|
+
for (const message of messages) this.cacheOne(message);
|
|
776
|
+
const current = this.db.prepare("SELECT pull_cursor FROM edge_scopes WHERE scope_key=?").get(this.key);
|
|
777
|
+
const currentCursor = current.pull_cursor ? String(current.pull_cursor) : void 0;
|
|
778
|
+
const nextCursor = !cursor ? currentCursor : !currentCursor || sequenceKey(decodeCursor(cursor, scope)) >= sequenceKey(decodeCursor(currentCursor, scope)) ? cursor : currentCursor;
|
|
779
|
+
this.db.prepare(`UPDATE edge_scopes SET pull_cursor=?, last_sync_at=?, last_error=NULL
|
|
686
780
|
WHERE scope_key=?`).run(nextCursor ?? null, now.toISOString(), this.key);
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
781
|
+
this.db.prepare("UPDATE edge_scopes SET last_inbound_sync_at=? WHERE scope_key=?").run(now.toISOString(), this.key);
|
|
782
|
+
this.db.exec("COMMIT");
|
|
783
|
+
} catch (error) {
|
|
784
|
+
this.db.exec("ROLLBACK");
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
787
|
+
});
|
|
693
788
|
}
|
|
694
789
|
async cacheLatest(messages, now = /* @__PURE__ */ new Date()) {
|
|
695
790
|
await this.ready();
|
|
696
|
-
this.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
791
|
+
await this.write(() => {
|
|
792
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
793
|
+
try {
|
|
794
|
+
for (const message of messages) this.cacheOne(message);
|
|
795
|
+
this.db.prepare(`UPDATE edge_scopes SET last_sync_at=?, last_error=NULL WHERE scope_key=?`).run(now.toISOString(), this.key);
|
|
796
|
+
this.db.prepare("UPDATE edge_scopes SET last_inbound_sync_at=? WHERE scope_key=?").run(now.toISOString(), this.key);
|
|
797
|
+
this.db.exec("COMMIT");
|
|
798
|
+
} catch (error) {
|
|
799
|
+
this.db.exec("ROLLBACK");
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
});
|
|
706
803
|
}
|
|
707
804
|
async list(query = {}) {
|
|
708
805
|
await this.ready();
|
|
@@ -784,11 +881,11 @@ var SQLiteEdgeStore = class {
|
|
|
784
881
|
}
|
|
785
882
|
async noteError(error) {
|
|
786
883
|
await this.ready();
|
|
787
|
-
this.db.prepare("UPDATE edge_scopes SET last_error=? WHERE scope_key=?").run(error.slice(0, 256), this.key);
|
|
884
|
+
await this.write(() => this.db.prepare("UPDATE edge_scopes SET last_error=? WHERE scope_key=?").run(error.slice(0, 256), this.key));
|
|
788
885
|
}
|
|
789
886
|
async noteAttempt(now = /* @__PURE__ */ new Date()) {
|
|
790
887
|
await this.ready();
|
|
791
|
-
this.db.prepare("UPDATE edge_scopes SET last_attempt_at=? WHERE scope_key=?").run(now.toISOString(), this.key);
|
|
888
|
+
await this.write(() => this.db.prepare("UPDATE edge_scopes SET last_attempt_at=? WHERE scope_key=?").run(now.toISOString(), this.key));
|
|
792
889
|
}
|
|
793
890
|
async stats(now = /* @__PURE__ */ new Date()) {
|
|
794
891
|
await this.ready();
|
|
@@ -1415,4 +1512,4 @@ export {
|
|
|
1415
1512
|
isRetryableSyncError,
|
|
1416
1513
|
SyncingBridgeStore
|
|
1417
1514
|
};
|
|
1418
|
-
//# sourceMappingURL=chunk-
|
|
1515
|
+
//# sourceMappingURL=chunk-U4M67GFU.js.map
|