@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 +2 -2
- package/src/index.ts +54 -8
- package/src/schema.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godspeedai/cognate-store-sqlite",
|
|
3
|
-
"version": "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.
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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. */
|