@nimbus-sh/fabric 0.1.0 → 0.3.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/README.md +208 -293
- package/dist/bindings.js +5 -5
- package/dist/budgets.d.ts +132 -0
- package/dist/budgets.d.ts.map +1 -0
- package/dist/budgets.js +248 -0
- package/dist/composition.d.ts +3 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +2 -0
- package/dist/connections.d.ts +81 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +114 -0
- package/dist/derived.d.ts +65 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +95 -0
- package/dist/do-calls.d.ts +94 -0
- package/dist/do-calls.d.ts.map +1 -0
- package/dist/do-calls.js +111 -0
- package/dist/facet-pool.d.ts +90 -0
- package/dist/facet-pool.d.ts.map +1 -0
- package/dist/facet-pool.js +113 -0
- package/dist/{fanout-pool.d.ts → fanout.d.ts} +20 -20
- package/dist/fanout.d.ts.map +1 -0
- package/dist/{fanout-pool.js → fanout.js} +20 -20
- package/dist/{launch-journal.d.ts → fenced-work.d.ts} +58 -17
- package/dist/fenced-work.d.ts.map +1 -0
- package/dist/fenced-work.js +241 -0
- package/dist/generation.d.ts +69 -0
- package/dist/generation.d.ts.map +1 -0
- package/dist/generation.js +118 -0
- package/dist/{facet-image-store.d.ts → image-store.d.ts} +8 -8
- package/dist/image-store.d.ts.map +1 -0
- package/dist/{facet-image-store.js → image-store.js} +4 -4
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -8
- package/dist/{loader-pool.d.ts → isolate-pool.d.ts} +19 -19
- package/dist/isolate-pool.d.ts.map +1 -0
- package/dist/{loader-pool.js → isolate-pool.js} +20 -20
- package/dist/journal.d.ts +111 -0
- package/dist/journal.d.ts.map +1 -0
- package/dist/journal.js +177 -0
- package/dist/outbox.d.ts +249 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +355 -0
- package/dist/process-fabric.d.ts +33 -15
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +25 -15
- package/dist/process-host.d.ts +1 -1
- package/dist/process-host.d.ts.map +1 -1
- package/dist/process-host.js +19 -11
- package/dist/sealed.d.ts +78 -0
- package/dist/sealed.d.ts.map +1 -0
- package/dist/sealed.js +145 -0
- package/dist/timers.d.ts +138 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +231 -0
- package/dist/{launch-pacer.d.ts → turn-budget.d.ts} +24 -21
- package/dist/turn-budget.d.ts.map +1 -0
- package/dist/{launch-pacer.js → turn-budget.js} +24 -12
- package/dist/workerd-facet-host.d.ts +67 -70
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +129 -181
- package/examples/agent-core-adapter.ts +191 -0
- package/package.json +4 -2
- package/src/bindings.ts +6 -6
- package/src/budgets.ts +308 -0
- package/src/composition.ts +16 -0
- package/src/connections.ts +140 -0
- package/src/derived.ts +135 -0
- package/src/do-calls.ts +156 -0
- package/src/facet-pool.ts +157 -0
- package/src/{fanout-pool.ts → fanout.ts} +35 -35
- package/src/{launch-journal.ts → fenced-work.ts} +129 -42
- package/src/generation.ts +144 -0
- package/src/{facet-image-store.ts → image-store.ts} +9 -9
- package/src/index.ts +16 -8
- package/src/{loader-pool.ts → isolate-pool.ts} +34 -34
- package/src/journal.ts +242 -0
- package/src/node-async-hooks.d.ts +14 -0
- package/src/outbox.ts +520 -0
- package/src/process-fabric.ts +43 -34
- package/src/process-host.ts +22 -20
- package/src/sealed.ts +150 -0
- package/src/timers.ts +294 -0
- package/src/{launch-pacer.ts → turn-budget.ts} +34 -27
- package/src/workerd-facet-host.ts +159 -208
- package/dist/alarms.d.ts +0 -134
- package/dist/alarms.d.ts.map +0 -1
- package/dist/alarms.js +0 -214
- package/dist/ctx-exports.d.ts +0 -47
- package/dist/ctx-exports.d.ts.map +0 -1
- package/dist/ctx-exports.js +0 -54
- package/dist/facet-image-store.d.ts.map +0 -1
- package/dist/fanout-pool.d.ts.map +0 -1
- package/dist/launch-journal.d.ts.map +0 -1
- package/dist/launch-journal.js +0 -154
- package/dist/launch-pacer.d.ts.map +0 -1
- package/dist/loader-ledger.d.ts +0 -57
- package/dist/loader-ledger.d.ts.map +0 -1
- package/dist/loader-ledger.js +0 -91
- package/dist/loader-pool.d.ts.map +0 -1
- package/src/alarms.ts +0 -275
- package/src/ctx-exports.ts +0 -77
- package/src/loader-ledger.ts +0 -112
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* journal.ts — an append-only event journal with dedupe and delivery leases,
|
|
3
|
+
* over a Durable Object's own SQLite.
|
|
4
|
+
*
|
|
5
|
+
* Modeled on Proteus's EventLog (`core/src/events/hub/log.ts`), which proved
|
|
6
|
+
* the contract this keeps:
|
|
7
|
+
*
|
|
8
|
+
* - `publish` returns `{ id, admitted }` — a dedupe hit returns the
|
|
9
|
+
* EXISTING id with `admitted: false` (log.ts:53-58), and the dedupe is
|
|
10
|
+
* storage-level: a unique partial index over non-null keys, because its
|
|
11
|
+
* schema comment calls the indexes "mandatory — without them recovery
|
|
12
|
+
* scans regress to table-scans on the hot path" (hub/schema.ts:6-7).
|
|
13
|
+
* - pending reads are priority-ordered: higher priority first, arrival
|
|
14
|
+
* order within a priority.
|
|
15
|
+
*
|
|
16
|
+
* Where this deliberately differs: Proteus binds a delivery by writing
|
|
17
|
+
* `consumed_at` and then needs a cold-start sweep (`unbindStale`, 10 minutes,
|
|
18
|
+
* orchestrator.ts:196) to recover rows a dead activation left bound. Here a
|
|
19
|
+
* claim takes a LEASE that expires on its own — expiry alone re-pends the
|
|
20
|
+
* row, so recovery needs no sweep and no cold-start hook. A lease that
|
|
21
|
+
* expired and was re-claimed is fenced by a per-claim nonce: the dead
|
|
22
|
+
* holder's `done`/`defer`/`dismiss` returns false instead of clobbering the
|
|
23
|
+
* new claimant.
|
|
24
|
+
*
|
|
25
|
+
* Everything here is synchronous — DO SQLite is — so the journal is safe to
|
|
26
|
+
* touch from a constructor without awaiting on the init gate.
|
|
27
|
+
*
|
|
28
|
+
* The index set is derived from this module's own query set, not copied from
|
|
29
|
+
* the consumer's nine: the claim scan gets a partial index over pending rows
|
|
30
|
+
* in claim order, and the dedupe probe gets the unique partial index. No
|
|
31
|
+
* other query here repeats on a hot path.
|
|
32
|
+
*/
|
|
33
|
+
/** Synchronous DO SQLite, as the journal uses it. */
|
|
34
|
+
export interface JournalSqlExec {
|
|
35
|
+
exec(query: string, ...bindings: Array<string | number | null>): Iterable<unknown>;
|
|
36
|
+
}
|
|
37
|
+
export interface JournalContext {
|
|
38
|
+
storage: {
|
|
39
|
+
sql: JournalSqlExec;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface JournalPublishResult {
|
|
43
|
+
/** The event id — the existing one when the publish was a duplicate. */
|
|
44
|
+
id: string;
|
|
45
|
+
/** False when the dedupe key already existed; nothing was written. */
|
|
46
|
+
admitted: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** One claimed delivery: the record, and the lease that settles it. */
|
|
49
|
+
export interface JournalClaim<P> {
|
|
50
|
+
id: string;
|
|
51
|
+
payload: P;
|
|
52
|
+
priority: number;
|
|
53
|
+
receivedAt: number;
|
|
54
|
+
lease: JournalLease;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* One delivery lease. Every settle returns whether THIS lease still held the
|
|
58
|
+
* row — false means the lease expired and another claim owns it now, and
|
|
59
|
+
* nothing was written.
|
|
60
|
+
*/
|
|
61
|
+
export interface JournalLease {
|
|
62
|
+
/** The event is handled; it never delivers again. */
|
|
63
|
+
done(): boolean;
|
|
64
|
+
/** Release the lease and hide the event until `at`. */
|
|
65
|
+
defer(at: number): boolean;
|
|
66
|
+
/** The event is refused for good; it never delivers again. */
|
|
67
|
+
dismiss(reason: string): boolean;
|
|
68
|
+
}
|
|
69
|
+
/** One named journal on one hosting actor. Cheap accessor, like `timers()`. */
|
|
70
|
+
export declare function journal<P>(ctx: JournalContext, name: string): Journal<P>;
|
|
71
|
+
export declare class Journal<P> {
|
|
72
|
+
private readonly ctx;
|
|
73
|
+
private readonly table;
|
|
74
|
+
private schemaReady;
|
|
75
|
+
private lastId;
|
|
76
|
+
private seq;
|
|
77
|
+
private leaseSeq;
|
|
78
|
+
constructor(ctx: JournalContext, name: string);
|
|
79
|
+
private ensureSchema;
|
|
80
|
+
/** Same shape as the outbox's: time-ordered, forced above every stored id. */
|
|
81
|
+
private mintId;
|
|
82
|
+
/**
|
|
83
|
+
* Append one event. A dedupe key that already exists — pending, done, or
|
|
84
|
+
* dismissed — refuses the append and names the existing id.
|
|
85
|
+
*/
|
|
86
|
+
publish(payload: P, opts?: {
|
|
87
|
+
dedupeKey?: string;
|
|
88
|
+
priority?: number;
|
|
89
|
+
now?: number;
|
|
90
|
+
}): JournalPublishResult;
|
|
91
|
+
/**
|
|
92
|
+
* Claim deliverable events under a lease: pending, at or past their
|
|
93
|
+
* revisit time, and not held by a live lease. Higher priority first,
|
|
94
|
+
* arrival order within a priority. Expiry alone re-pends a row a dead
|
|
95
|
+
* holder left leased — that is the whole recovery path.
|
|
96
|
+
*/
|
|
97
|
+
claim(opts: {
|
|
98
|
+
leaseMs: number;
|
|
99
|
+
minPriority?: number;
|
|
100
|
+
limit?: number;
|
|
101
|
+
now?: number;
|
|
102
|
+
}): Array<JournalClaim<P>>;
|
|
103
|
+
/**
|
|
104
|
+
* The settle ops all share one fence: they write only while the row is
|
|
105
|
+
* still pending under THIS lease's nonce. An expired-and-reclaimed row has
|
|
106
|
+
* a different nonce, so the dead holder's settle is a refused no-op —
|
|
107
|
+
* synchronous SQL makes the check-then-write atomic on the actor thread.
|
|
108
|
+
*/
|
|
109
|
+
private lease;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=journal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journal.d.ts","sourceRoot":"","sources":["../src/journal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;CACpF;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE;QAAE,GAAG,EAAE,cAAc,CAAA;KAAE,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,wEAAwE;IACxE,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,IAAI,IAAI,OAAO,CAAC;IAChB,uDAAuD;IACvD,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,8DAA8D;IAC9D,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC;AAWD,+EAA+E;AAC/E,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAExE;AAED,qBAAa,OAAO,CAAC,CAAC;IAOR,OAAO,CAAC,QAAQ,CAAC,GAAG;IANhC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,QAAQ,CAAK;gBAEQ,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM;IAO9D,OAAO,CAAC,YAAY;IA4BpB,8EAA8E;IAC9E,OAAO,CAAC,MAAM;IAOd;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,oBAAoB;IAmB7G;;;;;OAKG;IACH,KAAK,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAgC5G;;;;;OAKG;IACH,OAAO,CAAC,KAAK;CAoCd"}
|
package/dist/journal.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* journal.ts — an append-only event journal with dedupe and delivery leases,
|
|
3
|
+
* over a Durable Object's own SQLite.
|
|
4
|
+
*
|
|
5
|
+
* Modeled on Proteus's EventLog (`core/src/events/hub/log.ts`), which proved
|
|
6
|
+
* the contract this keeps:
|
|
7
|
+
*
|
|
8
|
+
* - `publish` returns `{ id, admitted }` — a dedupe hit returns the
|
|
9
|
+
* EXISTING id with `admitted: false` (log.ts:53-58), and the dedupe is
|
|
10
|
+
* storage-level: a unique partial index over non-null keys, because its
|
|
11
|
+
* schema comment calls the indexes "mandatory — without them recovery
|
|
12
|
+
* scans regress to table-scans on the hot path" (hub/schema.ts:6-7).
|
|
13
|
+
* - pending reads are priority-ordered: higher priority first, arrival
|
|
14
|
+
* order within a priority.
|
|
15
|
+
*
|
|
16
|
+
* Where this deliberately differs: Proteus binds a delivery by writing
|
|
17
|
+
* `consumed_at` and then needs a cold-start sweep (`unbindStale`, 10 minutes,
|
|
18
|
+
* orchestrator.ts:196) to recover rows a dead activation left bound. Here a
|
|
19
|
+
* claim takes a LEASE that expires on its own — expiry alone re-pends the
|
|
20
|
+
* row, so recovery needs no sweep and no cold-start hook. A lease that
|
|
21
|
+
* expired and was re-claimed is fenced by a per-claim nonce: the dead
|
|
22
|
+
* holder's `done`/`defer`/`dismiss` returns false instead of clobbering the
|
|
23
|
+
* new claimant.
|
|
24
|
+
*
|
|
25
|
+
* Everything here is synchronous — DO SQLite is — so the journal is safe to
|
|
26
|
+
* touch from a constructor without awaiting on the init gate.
|
|
27
|
+
*
|
|
28
|
+
* The index set is derived from this module's own query set, not copied from
|
|
29
|
+
* the consumer's nine: the claim scan gets a partial index over pending rows
|
|
30
|
+
* in claim order, and the dedupe probe gets the unique partial index. No
|
|
31
|
+
* other query here repeats on a hot path.
|
|
32
|
+
*/
|
|
33
|
+
import { z } from 'zod/v4';
|
|
34
|
+
const ClaimRowSchema = z.object({
|
|
35
|
+
id: z.string(),
|
|
36
|
+
payload: z.string(),
|
|
37
|
+
priority: z.number(),
|
|
38
|
+
received_at: z.number(),
|
|
39
|
+
});
|
|
40
|
+
const NAME_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
41
|
+
/** One named journal on one hosting actor. Cheap accessor, like `timers()`. */
|
|
42
|
+
export function journal(ctx, name) {
|
|
43
|
+
return new Journal(ctx, name);
|
|
44
|
+
}
|
|
45
|
+
export class Journal {
|
|
46
|
+
ctx;
|
|
47
|
+
table;
|
|
48
|
+
schemaReady = false;
|
|
49
|
+
lastId = '';
|
|
50
|
+
seq = 0;
|
|
51
|
+
leaseSeq = 0;
|
|
52
|
+
constructor(ctx, name) {
|
|
53
|
+
this.ctx = ctx;
|
|
54
|
+
if (!NAME_PATTERN.test(name)) {
|
|
55
|
+
throw new Error(`fabric: journal name '${name}' must match ${NAME_PATTERN}`);
|
|
56
|
+
}
|
|
57
|
+
this.table = `journal_${name}`;
|
|
58
|
+
}
|
|
59
|
+
ensureSchema() {
|
|
60
|
+
if (this.schemaReady)
|
|
61
|
+
return;
|
|
62
|
+
const sql = this.ctx.storage.sql;
|
|
63
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
64
|
+
id TEXT PRIMARY KEY,
|
|
65
|
+
payload TEXT NOT NULL,
|
|
66
|
+
dedupe_key TEXT,
|
|
67
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
68
|
+
state TEXT NOT NULL DEFAULT 'pending'
|
|
69
|
+
CHECK (state IN ('pending', 'done', 'dismissed')),
|
|
70
|
+
received_at INTEGER NOT NULL,
|
|
71
|
+
not_before INTEGER NOT NULL DEFAULT 0,
|
|
72
|
+
lease_id TEXT,
|
|
73
|
+
lease_until INTEGER,
|
|
74
|
+
done_at INTEGER,
|
|
75
|
+
dismiss_reason TEXT
|
|
76
|
+
)`);
|
|
77
|
+
sql.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.table}_dedupe
|
|
78
|
+
ON ${this.table} (dedupe_key) WHERE dedupe_key IS NOT NULL`);
|
|
79
|
+
// The claim scan in its own order, over pending rows only — done history
|
|
80
|
+
// grows without bound and must never be what recovery reads through.
|
|
81
|
+
sql.exec(`CREATE INDEX IF NOT EXISTS idx_${this.table}_pending
|
|
82
|
+
ON ${this.table} (priority DESC, id) WHERE state = 'pending'`);
|
|
83
|
+
const rows = [...sql.exec(`SELECT MAX(id) AS id FROM ${this.table}`)];
|
|
84
|
+
this.lastId = rows[0]?.id ?? '';
|
|
85
|
+
this.schemaReady = true;
|
|
86
|
+
}
|
|
87
|
+
/** Same shape as the outbox's: time-ordered, forced above every stored id. */
|
|
88
|
+
mintId(now) {
|
|
89
|
+
let id = `${now.toString(36).padStart(9, '0')}-${(this.seq++).toString(36).padStart(6, '0')}`;
|
|
90
|
+
if (this.lastId !== '' && id <= this.lastId)
|
|
91
|
+
id = `${this.lastId}0`;
|
|
92
|
+
this.lastId = id;
|
|
93
|
+
return id;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Append one event. A dedupe key that already exists — pending, done, or
|
|
97
|
+
* dismissed — refuses the append and names the existing id.
|
|
98
|
+
*/
|
|
99
|
+
publish(payload, opts = {}) {
|
|
100
|
+
this.ensureSchema();
|
|
101
|
+
const now = opts.now ?? Date.now();
|
|
102
|
+
const sql = this.ctx.storage.sql;
|
|
103
|
+
if (opts.dedupeKey !== undefined) {
|
|
104
|
+
const existing = [...sql.exec(`SELECT id FROM ${this.table} WHERE dedupe_key = ?`, opts.dedupeKey)];
|
|
105
|
+
if (existing.length > 0)
|
|
106
|
+
return { id: existing[0].id, admitted: false };
|
|
107
|
+
}
|
|
108
|
+
const id = this.mintId(now);
|
|
109
|
+
sql.exec(`INSERT INTO ${this.table} (id, payload, dedupe_key, priority, received_at, not_before)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?, 0)`, id, JSON.stringify(payload), opts.dedupeKey ?? null, opts.priority ?? 0, now);
|
|
111
|
+
return { id, admitted: true };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Claim deliverable events under a lease: pending, at or past their
|
|
115
|
+
* revisit time, and not held by a live lease. Higher priority first,
|
|
116
|
+
* arrival order within a priority. Expiry alone re-pends a row a dead
|
|
117
|
+
* holder left leased — that is the whole recovery path.
|
|
118
|
+
*/
|
|
119
|
+
claim(opts) {
|
|
120
|
+
this.ensureSchema();
|
|
121
|
+
const now = opts.now ?? Date.now();
|
|
122
|
+
const sql = this.ctx.storage.sql;
|
|
123
|
+
const rows = [...sql.exec(`SELECT id, payload, priority, received_at FROM ${this.table}
|
|
124
|
+
WHERE state = 'pending'
|
|
125
|
+
AND priority >= ?
|
|
126
|
+
AND not_before <= ?
|
|
127
|
+
AND (lease_until IS NULL OR lease_until <= ?)
|
|
128
|
+
ORDER BY priority DESC, id
|
|
129
|
+
LIMIT ?`, opts.minPriority ?? -2147483648, now, now, opts.limit ?? 50)].map((row) => ClaimRowSchema.parse(row));
|
|
130
|
+
const claims = [];
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
const leaseId = `${now.toString(36)}-${(this.leaseSeq++).toString(36)}`;
|
|
133
|
+
sql.exec(`UPDATE ${this.table} SET lease_id = ?, lease_until = ? WHERE id = ?`, leaseId, now + opts.leaseMs, row.id);
|
|
134
|
+
claims.push({
|
|
135
|
+
id: row.id,
|
|
136
|
+
payload: JSON.parse(row.payload),
|
|
137
|
+
priority: row.priority,
|
|
138
|
+
receivedAt: row.received_at,
|
|
139
|
+
lease: this.lease(row.id, leaseId),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return claims;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The settle ops all share one fence: they write only while the row is
|
|
146
|
+
* still pending under THIS lease's nonce. An expired-and-reclaimed row has
|
|
147
|
+
* a different nonce, so the dead holder's settle is a refused no-op —
|
|
148
|
+
* synchronous SQL makes the check-then-write atomic on the actor thread.
|
|
149
|
+
*/
|
|
150
|
+
lease(id, leaseId) {
|
|
151
|
+
const sql = this.ctx.storage.sql;
|
|
152
|
+
const holds = () => {
|
|
153
|
+
const rows = [...sql.exec(`SELECT 1 AS held FROM ${this.table} WHERE id = ? AND state = 'pending' AND lease_id = ?`, id, leaseId)];
|
|
154
|
+
return rows.length > 0;
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
done: () => {
|
|
158
|
+
if (!holds())
|
|
159
|
+
return false;
|
|
160
|
+
sql.exec(`UPDATE ${this.table} SET state = 'done', done_at = ?, lease_id = NULL, lease_until = NULL WHERE id = ?`, Date.now(), id);
|
|
161
|
+
return true;
|
|
162
|
+
},
|
|
163
|
+
defer: (at) => {
|
|
164
|
+
if (!holds())
|
|
165
|
+
return false;
|
|
166
|
+
sql.exec(`UPDATE ${this.table} SET not_before = ?, lease_id = NULL, lease_until = NULL WHERE id = ?`, at, id);
|
|
167
|
+
return true;
|
|
168
|
+
},
|
|
169
|
+
dismiss: (reason) => {
|
|
170
|
+
if (!holds())
|
|
171
|
+
return false;
|
|
172
|
+
sql.exec(`UPDATE ${this.table} SET state = 'dismissed', dismiss_reason = ?, lease_id = NULL, lease_until = NULL WHERE id = ?`, reason, id);
|
|
173
|
+
return true;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
package/dist/outbox.d.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outbox.ts — a durable retry outbox over a Durable Object's own SQLite.
|
|
3
|
+
*
|
|
4
|
+
* Proteus built this discipline twice by hand and says so: its email outbox
|
|
5
|
+
* (`cf-backend/src/email/outbox.ts:7` — "The discipline mirrors the peer
|
|
6
|
+
* outbox", 8 attempts from a 30s base) and its peer transport
|
|
7
|
+
* (`core/src/events/ingress/peer.ts` — 8 attempts from a 5s base, per-receiver
|
|
8
|
+
* ordering, an ask/reply waiter over the same rows). Both carry the same
|
|
9
|
+
* mechanism: a write-ahead intent row in `state='pending'`, an
|
|
10
|
+
* `attempt_count`, a `next_attempt_at` with a partial index over pending rows,
|
|
11
|
+
* backoff `next = now + base * 2**(attempts-1)`, and a `nextRetryAt()` folded
|
|
12
|
+
* into the object's single alarm. This module is that mechanism once.
|
|
13
|
+
*
|
|
14
|
+
* What the consumers proved and this keeps:
|
|
15
|
+
* - WRITE-AHEAD. The row commits before send runs. On a Durable Object the
|
|
16
|
+
* output gate orders it: an outbound message cannot leave before the
|
|
17
|
+
* writes of its turn are durable, so no explicit sync is needed here.
|
|
18
|
+
* - DISPOSITION, not a boolean. peer.ts:476-526 separates three failures: a
|
|
19
|
+
* RESOLVED refusal is permanent (dlq now), a THROWN send is transport
|
|
20
|
+
* trouble (backoff), a malformed row is poison (dlq now, no send). A
|
|
21
|
+
* boolean cannot carry that, and retrying a refusal re-offends.
|
|
22
|
+
* - ORDERING per key. Rows drain in id order. A transient failure blocks
|
|
23
|
+
* later rows with the same order key until the head clears (peer.ts:465
|
|
24
|
+
* head-of-line set); a dead-lettered head does not block, and keyless rows
|
|
25
|
+
* never block each other (the email outbox has no ordering at all).
|
|
26
|
+
* - IDEMPOTENCY. A dedupe key already queued or sent is refused with the
|
|
27
|
+
* existing id, and a sent key never reaches send again.
|
|
28
|
+
*
|
|
29
|
+
* What the consumers lacked and this adds:
|
|
30
|
+
* - The drain registers with `timers` (one reason in the shared map) instead
|
|
31
|
+
* of owning an alarm, and the dispatch-side handler re-arms through its
|
|
32
|
+
* RETURN value — calling schedule() from inside the dispatcher's chain
|
|
33
|
+
* would deadlock on the chain that serializes the reason map. A host
|
|
34
|
+
* whose alarm belongs to another framework (the Agents SDK, agent-core's
|
|
35
|
+
* reconciler) uses the scheduler-seam form instead: fabric hands every
|
|
36
|
+
* next due time to the policy's `schedule` and touches no timer storage.
|
|
37
|
+
* - The drain is turn-bounded through a {@link TurnBudget}: both consumer
|
|
38
|
+
* drains iterate every due row in one turn, which is the same
|
|
39
|
+
* thread-holding shape the pacer exists to end.
|
|
40
|
+
*
|
|
41
|
+
* ADOPTION IS A DATA MIGRATION for a live consumer. The rows live in
|
|
42
|
+
* fabric's own table (`outbox_<name>`), and nothing moves itself: a consumer
|
|
43
|
+
* with pending intents or a sent-key dedupe history in its old table keeps
|
|
44
|
+
* them there. The ported EmailOutbox went from `email_outbox` to
|
|
45
|
+
* `outbox_email` — deployed live without a migration, its pending mail would
|
|
46
|
+
* never send and every sent key would deliver a second time.
|
|
47
|
+
*/
|
|
48
|
+
import { type TimerContext, type TimerHandlerResult, type TimerHost, type TimerStorage } from './timers.js';
|
|
49
|
+
import type { TurnBudget } from './turn-budget.js';
|
|
50
|
+
/** Synchronous DO SQLite, as the outbox uses it. `exec` returns row objects. */
|
|
51
|
+
export interface OutboxSqlExec {
|
|
52
|
+
exec(query: string, ...bindings: Array<string | number | null>): Iterable<unknown>;
|
|
53
|
+
}
|
|
54
|
+
/** The hosting actor's storage: its SQLite plus the shared timer map's keys. */
|
|
55
|
+
export interface OutboxStorage extends TimerStorage {
|
|
56
|
+
sql: OutboxSqlExec;
|
|
57
|
+
}
|
|
58
|
+
export interface OutboxContext extends TimerContext {
|
|
59
|
+
storage: OutboxStorage;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* All a scheduler-seam outbox needs from its host: the SQLite the rows live
|
|
63
|
+
* in. The timer map's keys belong to the {@link TimerHost} path only.
|
|
64
|
+
*/
|
|
65
|
+
export interface OutboxSqlContext {
|
|
66
|
+
storage: {
|
|
67
|
+
sql: OutboxSqlExec;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* What one send attempt concluded. A resolved `retry` and a thrown send are
|
|
72
|
+
* the same transient class; `poison` is the resolved refusal that must never
|
|
73
|
+
* be retried — the split the peer transport proved necessary.
|
|
74
|
+
*/
|
|
75
|
+
export type OutboxDisposition = {
|
|
76
|
+
status: 'sent';
|
|
77
|
+
} | {
|
|
78
|
+
status: 'retry';
|
|
79
|
+
reason: string;
|
|
80
|
+
} | {
|
|
81
|
+
status: 'poison';
|
|
82
|
+
reason: string;
|
|
83
|
+
};
|
|
84
|
+
export interface OutboxPolicy<M, C = void> {
|
|
85
|
+
/** Attempts before a row dead-letters. Email uses 8; peer uses 8. */
|
|
86
|
+
maxAttempts: number;
|
|
87
|
+
/** Backoff base: `next = now + baseMs * 2**(attempts-1)`. */
|
|
88
|
+
baseMs: number;
|
|
89
|
+
/**
|
|
90
|
+
* Per-key delivery order (the peer transport's per-receiver key). Rows
|
|
91
|
+
* without a key deliver independently, as the email outbox's do.
|
|
92
|
+
*/
|
|
93
|
+
orderBy?(message: M): string;
|
|
94
|
+
/**
|
|
95
|
+
* Deliver one message. Throwing is transient, same as `retry`. `context`
|
|
96
|
+
* is the caller's per-drain state — Proteus's email outbox resolves its
|
|
97
|
+
* transport binding per call, and a policy closed at construction cannot
|
|
98
|
+
* hold it (the ported consumer smuggled it through an instance field).
|
|
99
|
+
*/
|
|
100
|
+
send(message: M, info: {
|
|
101
|
+
id: string;
|
|
102
|
+
attempt: number;
|
|
103
|
+
}, context: C): Promise<OutboxDisposition>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `context` carries the caller's per-drain state into `send`. Required when
|
|
107
|
+
* the policy declares one; absent (and unpayable) when it does not.
|
|
108
|
+
*/
|
|
109
|
+
export type OutboxDrainOptions<C> = {
|
|
110
|
+
budget?: TurnBudget;
|
|
111
|
+
} & ([C] extends [void] ? {
|
|
112
|
+
context?: C;
|
|
113
|
+
} : {
|
|
114
|
+
context: C;
|
|
115
|
+
});
|
|
116
|
+
type OutboxDrainArgs<C> = [C] extends [void] ? [opts?: OutboxDrainOptions<C>] : [opts: OutboxDrainOptions<C>];
|
|
117
|
+
type OutboxHandlerArgs<C> = [C] extends [void] ? [] : [context: C];
|
|
118
|
+
/**
|
|
119
|
+
* The policy of an outbox whose host owns the alarm. Proteus's Durable
|
|
120
|
+
* Object alarm belongs to the Agents SDK and agent-core's to its own
|
|
121
|
+
* reconciler; neither can give fabric the alarm slot. `schedule` receives
|
|
122
|
+
* every next due time — queue's admission instant, and the earliest pending
|
|
123
|
+
* deadline after each drain — and the consumer folds it into its own alarm.
|
|
124
|
+
*/
|
|
125
|
+
export interface ScheduledOutboxPolicy<M, C = void> extends OutboxPolicy<M, C> {
|
|
126
|
+
schedule(at: number): Promise<void>;
|
|
127
|
+
}
|
|
128
|
+
type OutboxScheduling = {
|
|
129
|
+
kind: 'timers';
|
|
130
|
+
host: TimerHost;
|
|
131
|
+
ctx: OutboxContext;
|
|
132
|
+
} | {
|
|
133
|
+
kind: 'schedule';
|
|
134
|
+
schedule: (at: number) => Promise<void>;
|
|
135
|
+
};
|
|
136
|
+
export interface OutboxDrainResult {
|
|
137
|
+
sent: number;
|
|
138
|
+
retried: number;
|
|
139
|
+
deadLettered: number;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* One row, as {@link Outbox.status} and {@link Outbox.find} report it. Both
|
|
143
|
+
* ported consumers read fabric's table with raw SQL for exactly this: the
|
|
144
|
+
* email outbox to answer sent/deduped/failed per key, the peer transport to
|
|
145
|
+
* correlate a reply with its stored ask — which is why the record carries
|
|
146
|
+
* the stored message, not just the state.
|
|
147
|
+
*/
|
|
148
|
+
export interface OutboxRecord<M> {
|
|
149
|
+
id: string;
|
|
150
|
+
state: 'pending' | 'sent' | 'dlq';
|
|
151
|
+
/** Null when the stored payload no longer parses (a poison-parse row). */
|
|
152
|
+
message: M | null;
|
|
153
|
+
dedupeKey: string | null;
|
|
154
|
+
attemptCount: number;
|
|
155
|
+
lastError: string | null;
|
|
156
|
+
}
|
|
157
|
+
export interface OutboxDeadLetter<M> {
|
|
158
|
+
id: string;
|
|
159
|
+
/** Null when the stored payload no longer parses (a poison-parse row). */
|
|
160
|
+
message: M | null;
|
|
161
|
+
dedupeKey: string | null;
|
|
162
|
+
attemptCount: number;
|
|
163
|
+
lastError: string;
|
|
164
|
+
}
|
|
165
|
+
/** One named outbox on one hosting actor. Cheap accessor, like `timers()`. */
|
|
166
|
+
export declare function outbox<M, C = void>(host: TimerHost, ctx: OutboxContext, name: string, policy: OutboxPolicy<M, C>): Outbox<M, C>;
|
|
167
|
+
/** The scheduler-seam form: the host owns the alarm, fabric owns the rows. */
|
|
168
|
+
export declare function outbox<M, C = void>(ctx: OutboxSqlContext, name: string, policy: ScheduledOutboxPolicy<M, C>): Outbox<M, C>;
|
|
169
|
+
export declare class Outbox<M, C = void> {
|
|
170
|
+
private readonly ctx;
|
|
171
|
+
private readonly policy;
|
|
172
|
+
private readonly scheduling;
|
|
173
|
+
/** The timer reason this outbox arms in the shared map. */
|
|
174
|
+
readonly reason: string;
|
|
175
|
+
private readonly table;
|
|
176
|
+
private schemaReady;
|
|
177
|
+
private draining;
|
|
178
|
+
/** Largest id ever seen, so a replacement instance mints above it. */
|
|
179
|
+
private lastId;
|
|
180
|
+
private seq;
|
|
181
|
+
constructor(ctx: OutboxSqlContext, name: string, policy: OutboxPolicy<M, C>, scheduling: OutboxScheduling);
|
|
182
|
+
private ensureSchema;
|
|
183
|
+
/**
|
|
184
|
+
* Ids order the drain, so they must grow: time-prefixed, tie-broken by a
|
|
185
|
+
* per-instance counter, and forced above the largest stored id so a
|
|
186
|
+
* replacement instance with a lagging clock cannot mint into the past.
|
|
187
|
+
*/
|
|
188
|
+
private mintId;
|
|
189
|
+
/**
|
|
190
|
+
* Write the intent ahead of any send. Returns `admitted: false` with the
|
|
191
|
+
* existing id when the dedupe key is already queued, sent, or dead-lettered
|
|
192
|
+
* — a sent key never reaches send again (the email outbox's short-circuit).
|
|
193
|
+
*
|
|
194
|
+
* `onDuplicate: 'retry-now'` treats a re-ask for an UNSENT key as new
|
|
195
|
+
* intent: the row's retry instant is clamped to `now` and a dead letter
|
|
196
|
+
* returns to 'pending', its attempt count kept — so every re-ask buys one
|
|
197
|
+
* more delivery attempt, past the attempt budget. The monitor's alert-once
|
|
198
|
+
* contract needs exactly this: a failed alert is retried by each sweep
|
|
199
|
+
* until it lands. A sent key stays final either way.
|
|
200
|
+
*
|
|
201
|
+
* Arms the scheduler at `now`, so delivery is owed by the alarm even
|
|
202
|
+
* when the caller never drains inline.
|
|
203
|
+
*/
|
|
204
|
+
queue(message: M, opts?: {
|
|
205
|
+
dedupeKey?: string;
|
|
206
|
+
now?: number;
|
|
207
|
+
onDuplicate?: 'refuse' | 'retry-now';
|
|
208
|
+
}): Promise<{
|
|
209
|
+
id: string;
|
|
210
|
+
admitted: boolean;
|
|
211
|
+
}>;
|
|
212
|
+
/** Hand a due time to whichever scheduler this outbox was built on. */
|
|
213
|
+
private arm;
|
|
214
|
+
/** The single-alarm fold: the earliest pending deadline, or null. */
|
|
215
|
+
nextRetryAt(): number | null;
|
|
216
|
+
/** The row queue() named, by its id. Null when no such row exists. */
|
|
217
|
+
status(id: string): OutboxRecord<M> | null;
|
|
218
|
+
/** The row a dedupe key admitted, whatever its state. Null when none. */
|
|
219
|
+
find(dedupeKey: string): OutboxRecord<M> | null;
|
|
220
|
+
private record;
|
|
221
|
+
/** Dead-lettered rows, for inspection. Terminal: nothing retries out. */
|
|
222
|
+
dlq(): Array<OutboxDeadLetter<M>>;
|
|
223
|
+
/**
|
|
224
|
+
* Deliver every due pending row, in id order, honouring per-key blocking.
|
|
225
|
+
* Reentrancy-guarded: the alarm and an inline post-queue drain overlap on
|
|
226
|
+
* the same activation (peer.ts:452 carries the same guard).
|
|
227
|
+
*
|
|
228
|
+
* `budget` bounds the turn: the drain spends each processed row's payload
|
|
229
|
+
* size and suspends when a chunk is full, so a large backlog crosses turns
|
|
230
|
+
* instead of holding the actor's only thread.
|
|
231
|
+
*
|
|
232
|
+
* `context` is handed to every `send` this drain makes — the seam for
|
|
233
|
+
* per-call state such as a transport binding resolved by the caller.
|
|
234
|
+
*/
|
|
235
|
+
drain(now?: number, ...rest: OutboxDrainArgs<C>): Promise<OutboxDrainResult>;
|
|
236
|
+
private deadLetter;
|
|
237
|
+
/**
|
|
238
|
+
* The dispatch-side entry for the embedder's timer handler map. Re-arms
|
|
239
|
+
* through the RETURN value: the dispatcher runs handlers inside the chain
|
|
240
|
+
* that serializes the reason map, so a schedule() call from here would
|
|
241
|
+
* deadlock on its own chain.
|
|
242
|
+
*
|
|
243
|
+
* An alarm fires with no caller, so a policy that declares a drain context
|
|
244
|
+
* receives it here once, closed over every alarm-driven drain.
|
|
245
|
+
*/
|
|
246
|
+
handler(...context: OutboxHandlerArgs<C>): (now: number) => Promise<TimerHandlerResult>;
|
|
247
|
+
}
|
|
248
|
+
export {};
|
|
249
|
+
//# sourceMappingURL=outbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox.d.ts","sourceRoot":"","sources":["../src/outbox.ts"],"names":[],"mappings":"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"}
|