@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.
@@ -4,7 +4,7 @@ import {
4
4
  assertEdgeUpgradeCandidate,
5
5
  installEdgeMarkers,
6
6
  retrySqliteBusy
7
- } from "./chunk-IVG4XWWS.js";
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
- if (tables.length !== 4) throw new EdgeConflictError("edge database schema is incomplete");
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 retrySqliteBusy(() => this.db.exec(schema), initializationTimeoutMs);
345
- await retrySqliteBusy(() => this.db.exec("BEGIN IMMEDIATE"), initializationTimeoutMs);
346
- try {
347
- const scopeColumns = this.db.prepare("PRAGMA table_info(edge_scopes)").all();
348
- if (!scopeColumns.some((column) => column.name === "cache_contract")) {
349
- this.db.exec("ALTER TABLE edge_scopes ADD COLUMN cache_contract INTEGER NOT NULL DEFAULT 0");
350
- }
351
- for (const name of ["last_outbound_sync_at", "last_inbound_sync_at", "last_attempt_at"]) {
352
- if (!scopeColumns.some((column) => column.name === name)) {
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
- const columns = this.db.prepare("PRAGMA table_info(edge_inbox)").all();
357
- if (!columns.some((column) => column.name === "project")) {
358
- this.db.exec("ALTER TABLE edge_inbox ADD COLUMN project TEXT");
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
- this.db.prepare(`INSERT INTO edge_migration_gates
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
- const contract = this.db.prepare("SELECT cache_contract FROM edge_scopes WHERE scope_key=?").get(this.key);
380
- if (Number(contract.cache_contract) < 1) {
381
- this.db.exec("BEGIN IMMEDIATE");
382
- try {
383
- this.db.prepare("UPDATE edge_scopes SET pull_cursor=NULL, cache_contract=1 WHERE scope_key=?").run(this.key);
384
- this.db.exec("COMMIT");
385
- } catch (error) {
386
- this.db.exec("ROLLBACK");
387
- throw error;
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.db.exec("BEGIN IMMEDIATE");
402
- try {
403
- this.assertScopeActiveInTransaction();
404
- this.db.exec("COMMIT");
405
- } catch (error) {
406
- this.db.exec("ROLLBACK");
407
- throw error;
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.db.exec("BEGIN IMMEDIATE");
415
- try {
416
- this.assertDrainLeaseInTransaction(lease, now);
417
- this.db.exec("COMMIT");
418
- } catch (error) {
419
- this.db.exec("ROLLBACK");
420
- throw error;
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.db.exec("BEGIN IMMEDIATE");
428
- try {
429
- const gate = this.gateInTransaction();
430
- if (gate.state === "retired") throw new EdgeMigrationRetiredError(gate);
431
- if (mode === "begin" && gate.state !== "active") throw new EdgeMigrationDrainingError(gate);
432
- if (mode === "resume") {
433
- if (gate.state !== "draining" || gate.operationId !== normalizedOperationId || !gate.leaseExpiresAt || Date.parse(gate.leaseExpiresAt) > now.getTime()) {
434
- throw new EdgeMigrationDrainingError(gate);
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
- const leaseToken = randomUUID();
438
- const leaseExpiresAt = new Date(now.getTime() + duration).toISOString();
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
- normalizedOperationId,
443
- leaseToken,
444
- leaseExpiresAt,
445
- now.toISOString(),
446
- this.key
447
- );
448
- if (updated.changes !== 1) throw new EdgeConflictError("edge migration gate could not be acquired");
449
- this.db.exec("COMMIT");
450
- return { scopeKey: this.key, operationId: normalizedOperationId, leaseToken, leaseExpiresAt };
451
- } catch (error) {
452
- this.db.exec("ROLLBACK");
453
- throw error;
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.db.exec("BEGIN IMMEDIATE");
475
- try {
476
- this.assertDrainLeaseInTransaction(lease, now);
477
- const counts = this.db.prepare(`SELECT
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
- now.toISOString(),
485
- now.toISOString(),
486
- now.toISOString(),
487
- now.toISOString(),
488
- now.toISOString(),
489
- this.key
490
- );
491
- 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) {
492
- throw new EdgeConflictError("edge scope cannot complete a drain with retained outbox work");
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
- this.db.exec("COMMIT");
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.db.exec("BEGIN IMMEDIATE");
505
- try {
506
- this.assertDrainLeaseInTransaction(lease, now);
507
- const leaseExpiresAt = new Date(now.getTime() + duration).toISOString();
508
- const updated = this.db.prepare(`UPDATE edge_migration_gates
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
- leaseExpiresAt,
512
- now.toISOString(),
513
- this.key,
514
- lease.operationId,
515
- lease.leaseToken,
516
- lease.leaseExpiresAt
517
- );
518
- if (updated.changes !== 1) throw new EdgeMigrationLeaseError();
519
- this.db.exec("COMMIT");
520
- return { ...lease, leaseExpiresAt };
521
- } catch (error) {
522
- this.db.exec("ROLLBACK");
523
- throw error;
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.db.exec("BEGIN IMMEDIATE");
530
- try {
531
- this.assertDrainLeaseInTransaction(lease, now);
532
- const stats = this.db.prepare(`SELECT count(*) AS count FROM edge_outbox
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
- if (Number(stats.count) !== 0) throw new EdgeConflictError("edge scope cannot retire with retained outbox work");
535
- const updated = this.db.prepare(`UPDATE edge_migration_gates
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
- now.toISOString(),
539
- this.key,
540
- lease.operationId,
541
- lease.leaseToken
542
- );
543
- if (updated.changes !== 1) throw new EdgeMigrationLeaseError();
544
- this.db.exec("COMMIT");
545
- } catch (error) {
546
- this.db.exec("ROLLBACK");
547
- throw error;
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.db.exec("BEGIN IMMEDIATE");
557
- try {
558
- this.assertScopeActiveInTransaction();
559
- const existing = this.db.prepare(`SELECT * FROM edge_outbox
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
- if (existing) {
562
- if (String(existing.payload_hash) !== payloadHash) {
563
- throw new EdgeConflictError("message ID or idempotency key is already bound to different content");
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.exec("COMMIT");
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
- this.db.exec("COMMIT");
572
- return { draft, created: true };
573
- } catch (error) {
574
- this.db.exec("ROLLBACK");
575
- throw error;
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.db.exec("BEGIN IMMEDIATE");
582
- try {
583
- this.assertClaimAuthorityInTransaction(now, migrationLease);
584
- const head = this.db.prepare(`SELECT * FROM edge_outbox
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
- if (!head) {
589
- this.db.exec("COMMIT");
590
- return void 0;
591
- }
592
- const leaseToken = randomUUID();
593
- const leaseExpiresAt = new Date(now.getTime() + leaseMs).toISOString();
594
- const updated = this.db.prepare(`UPDATE edge_outbox
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
- if (!updated.changes) {
687
+ if (!updated.changes) {
688
+ this.db.exec("COMMIT");
689
+ return void 0;
690
+ }
598
691
  this.db.exec("COMMIT");
599
- return void 0;
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
- this.db.exec("COMMIT");
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.db.exec("BEGIN IMMEDIATE");
632
- try {
633
- this.cacheOne(message);
634
- const deleted = this.db.prepare(`DELETE FROM edge_outbox
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
- if (deleted.changes !== 1) throw new EdgeConflictError("outbox lease was lost before commit");
637
- this.db.prepare(`UPDATE edge_scopes SET last_sync_at=?, last_error=NULL WHERE scope_key=?`).run(now.toISOString(), this.key);
638
- this.db.prepare("UPDATE edge_scopes SET last_outbound_sync_at=? WHERE scope_key=?").run(now.toISOString(), this.key);
639
- this.db.exec("COMMIT");
640
- } catch (error) {
641
- this.db.exec("ROLLBACK");
642
- throw error;
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.db.exec("BEGIN IMMEDIATE");
680
- try {
681
- for (const message of messages) this.cacheOne(message);
682
- const current = this.db.prepare("SELECT pull_cursor FROM edge_scopes WHERE scope_key=?").get(this.key);
683
- const currentCursor = current.pull_cursor ? String(current.pull_cursor) : void 0;
684
- const nextCursor = !cursor ? currentCursor : !currentCursor || sequenceKey(decodeCursor(cursor, scope)) >= sequenceKey(decodeCursor(currentCursor, scope)) ? cursor : currentCursor;
685
- this.db.prepare(`UPDATE edge_scopes SET pull_cursor=?, last_sync_at=?, last_error=NULL
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
- this.db.prepare("UPDATE edge_scopes SET last_inbound_sync_at=? WHERE scope_key=?").run(now.toISOString(), this.key);
688
- this.db.exec("COMMIT");
689
- } catch (error) {
690
- this.db.exec("ROLLBACK");
691
- throw error;
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.db.exec("BEGIN IMMEDIATE");
697
- try {
698
- for (const message of messages) this.cacheOne(message);
699
- this.db.prepare(`UPDATE edge_scopes SET last_sync_at=?, last_error=NULL WHERE scope_key=?`).run(now.toISOString(), this.key);
700
- this.db.prepare("UPDATE edge_scopes SET last_inbound_sync_at=? WHERE scope_key=?").run(now.toISOString(), this.key);
701
- this.db.exec("COMMIT");
702
- } catch (error) {
703
- this.db.exec("ROLLBACK");
704
- throw error;
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-ZUZ7TCIH.js.map
1515
+ //# sourceMappingURL=chunk-U4M67GFU.js.map