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