@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.
@@ -3,8 +3,9 @@ import {
3
3
  SQLITE_INITIALIZATION_BUSY_TIMEOUT_MS,
4
4
  assertEdgeUpgradeCandidate,
5
5
  installEdgeMarkers,
6
- retrySqliteBusy
7
- } from "./chunk-IVG4XWWS.js";
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
- if (tables.length !== 4) throw new EdgeConflictError("edge database schema is incomplete");
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 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`);
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
- 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)
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
- this.db.prepare(`INSERT INTO edge_migration_gates
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
- 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;
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.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
- }
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.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
- }
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.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);
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
- const leaseToken = randomUUID();
438
- const leaseExpiresAt = new Date(now.getTime() + duration).toISOString();
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
- 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
- }
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.db.exec("BEGIN IMMEDIATE");
475
- try {
476
- this.assertDrainLeaseInTransaction(lease, now);
477
- const counts = this.db.prepare(`SELECT
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
- 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");
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
- this.db.exec("COMMIT");
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.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
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
- 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
- }
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.db.exec("BEGIN IMMEDIATE");
530
- try {
531
- this.assertDrainLeaseInTransaction(lease, now);
532
- const stats = this.db.prepare(`SELECT count(*) AS count FROM edge_outbox
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
- 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
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
- 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
- }
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.db.exec("BEGIN IMMEDIATE");
557
- try {
558
- this.assertScopeActiveInTransaction();
559
- const existing = this.db.prepare(`SELECT * FROM edge_outbox
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
- if (existing) {
562
- if (String(existing.payload_hash) !== payloadHash) {
563
- throw new EdgeConflictError("message ID or idempotency key is already bound to different content");
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.exec("COMMIT");
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
- this.db.exec("COMMIT");
572
- return { draft, created: true };
573
- } catch (error) {
574
- this.db.exec("ROLLBACK");
575
- throw error;
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.db.exec("BEGIN IMMEDIATE");
582
- try {
583
- this.assertClaimAuthorityInTransaction(now, migrationLease);
584
- const head = this.db.prepare(`SELECT * FROM edge_outbox
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
- 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
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
- if (!updated.changes) {
688
+ if (!updated.changes) {
689
+ this.db.exec("COMMIT");
690
+ return void 0;
691
+ }
598
692
  this.db.exec("COMMIT");
599
- return void 0;
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
- 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
- }
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.db.exec("BEGIN IMMEDIATE");
632
- try {
633
- this.cacheOne(message);
634
- const deleted = this.db.prepare(`DELETE FROM edge_outbox
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
- 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
- }
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.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
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
- 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
- }
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.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
- }
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-ZUZ7TCIH.js.map
1516
+ //# sourceMappingURL=chunk-HIYXPNBE.js.map