@godspeedai/cognate-store-sqlite 0.3.0 → 0.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godspeedai/cognate-store-sqlite",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Authoritative DurableStore on bun:sqlite (spec §13).",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,6 +22,6 @@
22
22
  ".": "./src/index.ts"
23
23
  },
24
24
  "dependencies": {
25
- "@godspeedai/cognate-events": "^0.3.0"
25
+ "@godspeedai/cognate-events": "^0.5.0"
26
26
  }
27
27
  }
package/src/index.ts CHANGED
@@ -99,7 +99,7 @@ export function buildListRecordsQuery(collection: string, where?: { readonly [fi
99
99
  }
100
100
 
101
101
  function mapOutboxRow(row: Row): OutboxItem {
102
- return {
102
+ const item: OutboxItem = {
103
103
  id: row.id,
104
104
  topic: row.topic,
105
105
  payload: JSON.parse(row.payload as string),
@@ -110,6 +110,8 @@ function mapOutboxRow(row: Row): OutboxItem {
110
110
  receipt: row.receipt != null ? JSON.parse(row.receipt as string) : null,
111
111
  createdAt: row.created_at,
112
112
  };
113
+ if (row.lease_owner != null) return { ...item, lease: { ownerId: row.lease_owner, generation: row.lease_generation, expiresAt: row.claimed_until } };
114
+ return item;
113
115
  }
114
116
 
115
117
  function mapCheckpointRow(row: Row): ProjectionCheckpoint {
@@ -264,23 +266,52 @@ export function openSqliteStore(options: SqliteStoreOptions): DurableStore {
264
266
  });
265
267
 
266
268
  const claimOutboxTxn = db.transaction((topic: string, now: number, limit: number, leaseMs: number): OutboxItem[] => {
267
- const rows = db.query("SELECT * FROM outbox WHERE topic = ? AND status = 'pending' AND claimed_until <= ? ORDER BY rowid LIMIT ?").all(topic, now, limit) as Row[];
269
+ const rows = db.query("SELECT * FROM outbox WHERE topic = ? AND status = 'pending' AND claimed_until <= ? AND lease_generation = 0 ORDER BY rowid LIMIT ?").all(topic, now, limit) as Row[];
268
270
  const claimedUntil = now + leaseMs;
269
271
  const items: OutboxItem[] = [];
270
272
  for (const row of rows) {
271
- db.query("UPDATE outbox SET attempts = attempts + 1, claimed_until = ? WHERE id = ?").run(claimedUntil, row.id);
272
- items.push(mapOutboxRow({ ...row, attempts: row.attempts + 1, claimed_until: claimedUntil }));
273
+ db.query("UPDATE outbox SET attempts = attempts + 1, claimed_until = ?, lease_owner = NULL WHERE id = ? AND lease_generation = 0").run(claimedUntil, row.id);
274
+ items.push(mapOutboxRow({ ...row, attempts: row.attempts + 1, claimed_until: claimedUntil, lease_owner: null }));
273
275
  }
274
276
  return items;
275
277
  });
276
278
 
279
+ const claimOutboxLeasedTxn = db.transaction((topic: string, ownerId: string, now: number, limit: number, leaseMs: number): OutboxItem[] => {
280
+ const rows = db.query("SELECT * FROM outbox WHERE topic = ? AND status = 'pending' AND claimed_until <= ? ORDER BY rowid LIMIT ?").all(topic, now, limit) as Row[];
281
+ const expiresAt = now + leaseMs;
282
+ return rows.map((row) => {
283
+ const generation = row.lease_generation + 1;
284
+ db.query("UPDATE outbox SET attempts = attempts + 1, claimed_until = ?, lease_owner = ?, lease_generation = ? WHERE id = ?").run(expiresAt, ownerId, generation, row.id);
285
+ return mapOutboxRow({ ...row, attempts: row.attempts + 1, claimed_until: expiresAt, lease_owner: ownerId, lease_generation: generation });
286
+ });
287
+ });
288
+
289
+ const renewOutboxLeaseTxn = db.transaction((id: string, ownerId: string, generation: number, now: number, leaseMs: number): boolean => {
290
+ const result = db.query("UPDATE outbox SET claimed_until = ? WHERE id = ? AND status = 'pending' AND lease_owner = ? AND lease_generation = ? AND claimed_until > ?").run(now + leaseMs, id, ownerId, generation, now);
291
+ return result.changes === 1;
292
+ });
293
+
294
+ const settleOutboxLeaseTxn = db.transaction((id: string, ownerId: string, generation: number, now: number, settlement: OutboxSettlement): boolean => {
295
+ let result;
296
+ if (settlement.kind === "delivered") {
297
+ result = db.query("UPDATE outbox SET status = 'delivered', receipt = ?, last_error = NULL, lease_owner = NULL, lease_generation = lease_generation + 1 WHERE id = ? AND status = 'pending' AND lease_owner = ? AND lease_generation = ? AND claimed_until > ?").run(JSON.stringify(settlement.receipt), id, ownerId, generation, now);
298
+ } else if (settlement.kind === "retry") {
299
+ result = db.query("UPDATE outbox SET last_error = ?, claimed_until = ?, lease_owner = NULL, lease_generation = lease_generation + 1 WHERE id = ? AND status = 'pending' AND lease_owner = ? AND lease_generation = ? AND claimed_until > ?").run(settlement.error, settlement.retryAt, id, ownerId, generation, now);
300
+ } else {
301
+ result = db.query("UPDATE outbox SET status = 'poisoned', last_error = ?, lease_owner = NULL, lease_generation = lease_generation + 1 WHERE id = ? AND status = 'pending' AND lease_owner = ? AND lease_generation = ? AND claimed_until > ?").run(settlement.error, id, ownerId, generation, now);
302
+ }
303
+ return result.changes === 1;
304
+ });
305
+
277
306
  const settleOutboxTxn = db.transaction((id: string, settlement: OutboxSettlement): void => {
307
+ const row = db.query("SELECT lease_owner, lease_generation FROM outbox WHERE id = ?").get(id) as { lease_owner: string | null; lease_generation: number } | null;
308
+ if (row?.lease_owner != null || (row?.lease_generation ?? 0) > 0) throw new Error(`outbox item ${id} uses fenced leases`);
278
309
  if (settlement.kind === "delivered") {
279
- db.query("UPDATE outbox SET status = 'delivered', receipt = ? WHERE id = ?").run(JSON.stringify(settlement.receipt), id);
310
+ db.query("UPDATE outbox SET status = 'delivered', receipt = ? WHERE id = ? AND lease_owner IS NULL AND lease_generation = 0").run(JSON.stringify(settlement.receipt), id);
280
311
  } else if (settlement.kind === "retry") {
281
- db.query("UPDATE outbox SET last_error = ?, claimed_until = ? WHERE id = ?").run(settlement.error, settlement.retryAt, id);
312
+ db.query("UPDATE outbox SET last_error = ?, claimed_until = ? WHERE id = ? AND lease_owner IS NULL AND lease_generation = 0").run(settlement.error, settlement.retryAt, id);
282
313
  } else {
283
- db.query("UPDATE outbox SET status = 'poisoned', last_error = ? WHERE id = ?").run(settlement.error, id);
314
+ db.query("UPDATE outbox SET status = 'poisoned', last_error = ? WHERE id = ? AND lease_owner IS NULL AND lease_generation = 0").run(settlement.error, id);
284
315
  }
285
316
  });
286
317
 
@@ -423,6 +454,21 @@ export function openSqliteStore(options: SqliteStoreOptions): DurableStore {
423
454
  return claimOutboxTxn.immediate(options.topic, options.now, options.limit, options.leaseMs);
424
455
  },
425
456
 
457
+ async claimOutboxLeased(options) {
458
+ assertOpen();
459
+ return claimOutboxLeasedTxn.immediate(options.topic, options.ownerId, options.now, options.limit, options.leaseMs);
460
+ },
461
+
462
+ async renewOutboxLease(lease) {
463
+ assertOpen();
464
+ return renewOutboxLeaseTxn.immediate(lease.id, lease.ownerId, lease.generation, lease.now, lease.leaseMs);
465
+ },
466
+
467
+ async settleOutboxLease(lease, settlement) {
468
+ assertOpen();
469
+ return settleOutboxLeaseTxn.immediate(lease.id, lease.ownerId, lease.generation, lease.now, settlement);
470
+ },
471
+
426
472
  async settleOutbox(id: string, settlement: OutboxSettlement): Promise<void> {
427
473
  assertOpen();
428
474
  settleOutboxTxn.immediate(id, settlement);
@@ -430,7 +476,7 @@ export function openSqliteStore(options: SqliteStoreOptions): DurableStore {
430
476
 
431
477
  async requeueOutbox(id: string, now: number): Promise<void> {
432
478
  assertOpen();
433
- db.query("UPDATE outbox SET status = 'pending', claimed_until = ? WHERE id = ?").run(now, id);
479
+ db.query("UPDATE outbox SET status = 'pending', claimed_until = ?, lease_owner = NULL, lease_generation = CASE WHEN lease_generation > 0 THEN lease_generation + 1 ELSE 0 END WHERE id = ?").run(now, id);
434
480
  },
435
481
 
436
482
  async listOutbox(options?: { readonly topic?: string; readonly status?: OutboxStatus }): Promise<OutboxItem[]> {
package/src/schema.ts CHANGED
@@ -125,6 +125,13 @@ export const MIGRATIONS: readonly Migration[] = [
125
125
  CREATE INDEX records_runs_agent_idx ON records (json_extract(value, '$.agent')) WHERE collection = 'runs';
126
126
  `,
127
127
  },
128
+ {
129
+ version: 4,
130
+ sql: `
131
+ ALTER TABLE outbox ADD COLUMN lease_owner TEXT;
132
+ ALTER TABLE outbox ADD COLUMN lease_generation INTEGER NOT NULL DEFAULT 0;
133
+ `,
134
+ },
128
135
  ];
129
136
 
130
137
  /** Apply any migration not yet recorded in `schema_versions`, inside one transaction. */