@fungi.computer/carrier 0.2.0 → 0.2.2
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 +48 -73
- package/dist/inspection.js +12 -5
- package/dist/internal/owner.js +3 -1
- package/dist/internal/post-contract.d.ts +3 -3
- package/dist/internal/post-contract.js +1 -1
- package/dist/internal/post-policy.d.ts +5 -0
- package/dist/internal/post-policy.js +4 -0
- package/dist/internal/post-sql.d.ts +5 -10
- package/dist/internal/post-sql.js +65 -96
- package/dist/post-d1.d.ts +1 -1
- package/dist/post-d1.js +1 -7
- package/dist/post-postgres.js +0 -17
- package/dist/post.d.ts +7 -19
- package/dist/post.js +78 -209
- package/docs/api/README.md +0 -1
- package/docs/api/inspection.md +1 -1
- package/docs/api/post-d1.md +6 -15
- package/docs/api/post-postgres.md +3 -3
- package/docs/api/post.md +15 -132
- package/package.json +3 -7
- package/dist/post-outbox.d.ts +0 -52
- package/dist/post-outbox.js +0 -152
- package/dist/testing.d.ts +0 -16
- package/dist/testing.js +0 -20
- package/docs/api/testing.md +0 -46
package/dist/post.d.ts
CHANGED
|
@@ -2,9 +2,7 @@ import * as Schema from "effect/Schema";
|
|
|
2
2
|
import { type PostDeliveryResult } from "./internal/post-contract.js";
|
|
3
3
|
export { PostError, PostDeliveryResultSchema, } from "./internal/post-contract.js";
|
|
4
4
|
export type { PostDeliveryResult } from "./internal/post-contract.js";
|
|
5
|
-
import type { WatchdogRunningJob, WatchdogSqliteOwner,
|
|
6
|
-
export { makePostOutbox, postOutboxPending } from "./post-outbox.js";
|
|
7
|
-
export type { PostOutbox, PostOutboxStore } from "./post-outbox.js";
|
|
5
|
+
import type { WatchdogRunningJob, WatchdogSqliteOwner, WatchdogAttempt, WatchdogRetryPolicy, WatchdogExecutionDecision, WatchdogTransactionalProjection } from "@fungi.computer/watchdog";
|
|
8
6
|
/** Wire envelope parsed at the durable row and receiver boundaries. */
|
|
9
7
|
export declare const PostEnvelopeSchema: Schema.Struct<{
|
|
10
8
|
readonly key: Schema.String;
|
|
@@ -21,21 +19,17 @@ export declare const PostRowSchema: Schema.fromJsonString<Schema.Union<readonly
|
|
|
21
19
|
readonly address: Schema.Unknown;
|
|
22
20
|
readonly event: Schema.Unknown;
|
|
23
21
|
}>;
|
|
24
|
-
readonly attempts: Schema.Int;
|
|
25
|
-
readonly nextAttemptAt: Schema.Number;
|
|
26
22
|
readonly deadlineAt: Schema.optional<Schema.Number>;
|
|
27
|
-
readonly jobId: Schema.
|
|
23
|
+
readonly jobId: Schema.String;
|
|
28
24
|
readonly delivery: Schema.optional<Schema.Struct<{
|
|
29
25
|
readonly key: Schema.String;
|
|
30
26
|
readonly kind: Schema.String;
|
|
31
27
|
readonly address: Schema.Unknown;
|
|
32
28
|
readonly event: Schema.Unknown;
|
|
33
29
|
}>>;
|
|
34
|
-
readonly nextKey: Schema.optional<Schema.String>;
|
|
35
30
|
}>, Schema.Struct<{
|
|
36
31
|
readonly state: Schema.Literal<"delivered">;
|
|
37
32
|
readonly receipt: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
38
|
-
readonly nextKey: Schema.optional<Schema.String>;
|
|
39
33
|
}>, Schema.Struct<{
|
|
40
34
|
readonly state: Schema.Literal<"dead">;
|
|
41
35
|
readonly envelope: Schema.Struct<{
|
|
@@ -54,7 +48,6 @@ export declare const PostRowSchema: Schema.fromJsonString<Schema.Union<readonly
|
|
|
54
48
|
readonly address: Schema.Unknown;
|
|
55
49
|
readonly event: Schema.Unknown;
|
|
56
50
|
}>>;
|
|
57
|
-
readonly nextKey: Schema.optional<Schema.String>;
|
|
58
51
|
}>]>>;
|
|
59
52
|
/** One addressed event identified by the producer's stable key. */
|
|
60
53
|
export type PostEnvelope = Schema.Schema.Type<typeof PostEnvelopeSchema>;
|
|
@@ -147,13 +140,11 @@ export type PostRuntime<R extends Record<string, PostRegistryRoute>> = Readonly<
|
|
|
147
140
|
address: Schema.Schema.Type<R[K]["address"]>;
|
|
148
141
|
event: Schema.Schema.Type<R[K]["event"]>;
|
|
149
142
|
}>) => void;
|
|
150
|
-
/**
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
enqueueDue: (limit?: number, afterKey?: string) => Promise<string | undefined>;
|
|
156
|
-
/** Release settled pending retries from backoff without resetting attempts. */
|
|
143
|
+
/** Resolve the retained route deadline or ordinary attempt ceiling. */
|
|
144
|
+
retryPolicy: (job: WatchdogRunningJob) => Promise<WatchdogRetryPolicy>;
|
|
145
|
+
/** Deliver using Watchdog's durably reserved attempt. */
|
|
146
|
+
execute: (job: WatchdogRunningJob, attempt: WatchdogAttempt | undefined, signal?: AbortSignal) => Promise<WatchdogExecutionDecision>;
|
|
147
|
+
/** Expedite queued deliveries without resetting their charged attempt count. */
|
|
157
148
|
releaseNow: (limit?: number, afterKey?: string) => Promise<string | undefined>;
|
|
158
149
|
/** Read an ordered page of refused, exhausted or expired keys for operators. */
|
|
159
150
|
deadLetters: (limit?: number, afterKey?: string) => Readonly<{
|
|
@@ -174,15 +165,12 @@ export type PostRuntime<R extends Record<string, PostRegistryRoute>> = Readonly<
|
|
|
174
165
|
retry: (key: string) => Promise<void>;
|
|
175
166
|
/** Inspect one producer-local row or receipt. */
|
|
176
167
|
read: (key: string) => PostRow | undefined;
|
|
177
|
-
/** Earliest pending queue head, including a settled job waiting to retry. */
|
|
178
|
-
nextWake: () => number | null;
|
|
179
168
|
}>;
|
|
180
169
|
/** Decode at both persisted and DO boundaries with the route's Effect Schema. */
|
|
181
170
|
export declare function createPost<R extends Record<string, PostRegistryRoute>>(options: Readonly<{
|
|
182
171
|
routes: R;
|
|
183
172
|
owner: WatchdogSqliteOwner;
|
|
184
173
|
transactional: WatchdogTransactionalProjection;
|
|
185
|
-
readJob: WatchdogRuntime["read"];
|
|
186
174
|
now: () => number;
|
|
187
175
|
scheduleWake: (at: number) => Promise<void>;
|
|
188
176
|
}>): PostRuntime<R>;
|
package/dist/post.js
CHANGED
|
@@ -3,9 +3,7 @@ import { PostError, } from "./internal/post-contract.js";
|
|
|
3
3
|
export { PostError, PostDeliveryResultSchema, } from "./internal/post-contract.js";
|
|
4
4
|
import { deliverPost, postDisposition, deliveryReceipt, } from "./internal/post-execution.js";
|
|
5
5
|
import { defineIdFamily } from "@fungi.computer/cairn";
|
|
6
|
-
import {
|
|
7
|
-
import { maxDeliveryAttempts, postRetryPolicy } from "./post-outbox.js";
|
|
8
|
-
export { makePostOutbox, postOutboxPending } from "./post-outbox.js";
|
|
6
|
+
import { maxDeliveryAttempts, postRetryPolicy, } from "./internal/post-policy.js";
|
|
9
7
|
const Key = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
|
|
10
8
|
const JobPayload = Schema.Struct({ postKey: Key });
|
|
11
9
|
const PageLimit = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(64));
|
|
@@ -27,17 +25,13 @@ export const PostRowSchema = Schema.fromJsonString(Schema.Union([
|
|
|
27
25
|
Schema.Struct({
|
|
28
26
|
state: Schema.Literal("pending"),
|
|
29
27
|
envelope: PostEnvelopeSchema,
|
|
30
|
-
attempts: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
31
|
-
nextAttemptAt: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
32
28
|
deadlineAt: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
33
|
-
jobId:
|
|
29
|
+
jobId: Key,
|
|
34
30
|
delivery: Schema.optional(PostEnvelopeSchema),
|
|
35
|
-
nextKey: Schema.optional(Key),
|
|
36
31
|
}),
|
|
37
32
|
Schema.Struct({
|
|
38
33
|
state: Schema.Literal("delivered"),
|
|
39
34
|
receipt: Schema.optional(Schema.Json),
|
|
40
|
-
nextKey: Schema.optional(Key),
|
|
41
35
|
}),
|
|
42
36
|
Schema.Struct({
|
|
43
37
|
state: Schema.Literal("dead"),
|
|
@@ -47,7 +41,6 @@ export const PostRowSchema = Schema.fromJsonString(Schema.Union([
|
|
|
47
41
|
deadlineAt: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
48
42
|
receipt: Schema.optional(Schema.Json),
|
|
49
43
|
delivery: Schema.optional(PostEnvelopeSchema),
|
|
50
|
-
nextKey: Schema.optional(Key),
|
|
51
44
|
}),
|
|
52
45
|
]));
|
|
53
46
|
function receiptOperations(address, event, hooks) {
|
|
@@ -103,13 +96,17 @@ export function definePostRoute(route) {
|
|
|
103
96
|
};
|
|
104
97
|
}
|
|
105
98
|
const StoredRow = Schema.Struct({ key: Schema.String, value: Schema.String });
|
|
106
|
-
const DueRow = Schema.Struct({
|
|
107
|
-
due: Schema.NullOr(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
108
|
-
});
|
|
109
99
|
const openPostStore = (owner) => {
|
|
110
100
|
const sql = owner.sql;
|
|
111
101
|
owner.transactionSync(() => {
|
|
112
102
|
sql.exec("CREATE TABLE IF NOT EXISTS carrier_post_rows (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
|
|
103
|
+
// Rows written before the Watchdog scheduler carry nextAttemptAt or nextKey;
|
|
104
|
+
// Fungi 0.15.0 resets that store by refusing it, never loading or rewriting it.
|
|
105
|
+
const preScheduler = sql
|
|
106
|
+
.exec("SELECT key FROM carrier_post_rows WHERE key LIKE 'post:%' AND (json_type(value,'$.nextAttemptAt') IS NOT NULL OR json_type(value,'$.nextKey') IS NOT NULL) LIMIT 1")
|
|
107
|
+
.toArray();
|
|
108
|
+
if (preScheduler.length > 0)
|
|
109
|
+
throw new PostError("pre-scheduler Post store is not loaded; preserve it and authorize a reset through its owning host", "pre-scheduler-store");
|
|
113
110
|
});
|
|
114
111
|
return {
|
|
115
112
|
commit: (mutation) => owner.transactionSync(mutation),
|
|
@@ -127,16 +124,12 @@ const openPostStore = (owner) => {
|
|
|
127
124
|
list: (limit, afterKey = "") => Schema.decodeUnknownSync(Schema.Array(StoredRow))(sql
|
|
128
125
|
.exec("SELECT key,value FROM carrier_post_rows WHERE key LIKE 'post:%' AND key>? ORDER BY key LIMIT ?", afterKey, limit)
|
|
129
126
|
.toArray()),
|
|
130
|
-
nextWake: () => Schema.decodeUnknownSync(DueRow)(sql
|
|
131
|
-
.exec("SELECT MIN(json_extract(value,'$.nextAttemptAt')) AS due FROM carrier_post_rows WHERE key LIKE 'post:%' AND json_extract(value,'$.state')='pending' AND json_type(value,'$.jobId')='text'")
|
|
132
|
-
.toArray()[0]).due,
|
|
133
127
|
};
|
|
134
128
|
};
|
|
135
129
|
/** Decode at both persisted and DO boundaries with the route's Effect Schema. */
|
|
136
130
|
export function createPost(options) {
|
|
137
131
|
const store = openPostStore(options.owner);
|
|
138
132
|
const rowKey = (key) => `post:${key}`;
|
|
139
|
-
const tailKey = (queue) => `post-tail:${queue}`;
|
|
140
133
|
const headKey = (kind, queue) => `post-head:${JSON.stringify([kind, queue])}`;
|
|
141
134
|
const readRow = (key) => {
|
|
142
135
|
const raw = store.read(rowKey(key));
|
|
@@ -144,7 +137,6 @@ export function createPost(options) {
|
|
|
144
137
|
? undefined
|
|
145
138
|
: Schema.decodeUnknownSync(PostRowSchema)(raw);
|
|
146
139
|
};
|
|
147
|
-
const nextWake = () => store.nextWake();
|
|
148
140
|
const putRow = (key, row) => {
|
|
149
141
|
store.put(rowKey(key), Schema.encodeSync(PostRowSchema)(row));
|
|
150
142
|
};
|
|
@@ -159,10 +151,9 @@ export function createPost(options) {
|
|
|
159
151
|
return JSON.stringify([route.addressKind, route.nameOf(envelope)]);
|
|
160
152
|
};
|
|
161
153
|
const enqueueJob = (row) => {
|
|
162
|
-
|
|
163
|
-
putRow(row.envelope.key, { ...row, jobId });
|
|
154
|
+
putRow(row.envelope.key, row);
|
|
164
155
|
options.transactional.enqueueAcceptedJob({
|
|
165
|
-
jobId,
|
|
156
|
+
jobId: row.jobId,
|
|
166
157
|
queue: queueOf(row.envelope),
|
|
167
158
|
lane: "post",
|
|
168
159
|
priority: 0,
|
|
@@ -182,26 +173,16 @@ export function createPost(options) {
|
|
|
182
173
|
return;
|
|
183
174
|
}
|
|
184
175
|
const queue = queueOf(envelope);
|
|
185
|
-
const previousKey = store.read(tailKey(queue));
|
|
186
|
-
const previous = previousKey === undefined ? undefined : readRow(previousKey);
|
|
187
176
|
const admittedAt = options.now();
|
|
188
177
|
const baseRow = {
|
|
189
178
|
state: "pending",
|
|
190
179
|
envelope,
|
|
191
|
-
|
|
192
|
-
nextAttemptAt: admittedAt,
|
|
180
|
+
jobId: ids.work.generate(),
|
|
193
181
|
};
|
|
194
182
|
const row = route.deadlineMs === undefined
|
|
195
183
|
? baseRow
|
|
196
184
|
: { ...baseRow, deadlineAt: admittedAt + route.deadlineMs };
|
|
197
|
-
|
|
198
|
-
if (previousKey !== undefined && previous?.state === "pending") {
|
|
199
|
-
putRow(previousKey, { ...previous, nextKey: envelope.key });
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
enqueueJob(row);
|
|
203
|
-
}
|
|
204
|
-
store.put(tailKey(queue), envelope.key);
|
|
185
|
+
enqueueJob(row);
|
|
205
186
|
store.put(headKey(envelope.kind, queue), envelope.key);
|
|
206
187
|
};
|
|
207
188
|
const latest = (kind, address) => {
|
|
@@ -222,20 +203,13 @@ export function createPost(options) {
|
|
|
222
203
|
throw new PostError("latest delivery row is missing");
|
|
223
204
|
return { key, row };
|
|
224
205
|
};
|
|
225
|
-
const
|
|
226
|
-
if (nextKey === undefined)
|
|
227
|
-
return;
|
|
228
|
-
const next = readRow(nextKey);
|
|
229
|
-
if (next?.state === "pending" && next.jobId === undefined)
|
|
230
|
-
enqueueJob(next);
|
|
231
|
-
};
|
|
232
|
-
const finish = (key, row, terminal, metadata) => {
|
|
206
|
+
const finish = (key, row, terminal, attempts, metadata) => {
|
|
233
207
|
let receipt;
|
|
234
208
|
if (terminal.state === "dead") {
|
|
235
209
|
const dead = {
|
|
236
210
|
...terminal,
|
|
237
211
|
envelope: row.envelope,
|
|
238
|
-
attempts
|
|
212
|
+
attempts,
|
|
239
213
|
};
|
|
240
214
|
const settled = row.deadlineAt === undefined
|
|
241
215
|
? dead
|
|
@@ -249,41 +223,45 @@ export function createPost(options) {
|
|
|
249
223
|
receipt = terminal;
|
|
250
224
|
if (metadata !== undefined)
|
|
251
225
|
receipt = { ...receipt, receipt: metadata };
|
|
252
|
-
putRow(key,
|
|
253
|
-
? receipt
|
|
254
|
-
: { ...receipt, nextKey: row.nextKey });
|
|
255
|
-
releaseNext(row.nextKey);
|
|
226
|
+
putRow(key, receipt);
|
|
256
227
|
};
|
|
257
|
-
const
|
|
228
|
+
const retryPolicy = (job) => {
|
|
258
229
|
const { postKey } = Schema.decodeUnknownSync(JobPayload)(job.payload);
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
230
|
+
const row = readRow(postKey);
|
|
231
|
+
if (row === undefined)
|
|
232
|
+
throw new PostError("delivery row is missing");
|
|
233
|
+
const limit = row.state !== "pending" || row.jobId !== job.jobId
|
|
234
|
+
? { maxAttempts: 0 }
|
|
235
|
+
: row.deadlineAt === undefined
|
|
236
|
+
? { maxAttempts: maxDeliveryAttempts }
|
|
237
|
+
: { deadlineAt: row.deadlineAt };
|
|
238
|
+
return Promise.resolve({ ...postRetryPolicy, limit });
|
|
239
|
+
};
|
|
240
|
+
const execute = async (job, reservation, signal) => {
|
|
241
|
+
if (reservation === undefined)
|
|
242
|
+
throw new PostError("delivery attempt is missing");
|
|
243
|
+
const { postKey } = Schema.decodeUnknownSync(JobPayload)(job.payload);
|
|
244
|
+
const row = store.commit(() => {
|
|
245
|
+
const current = readRow(postKey);
|
|
246
|
+
if (current === undefined)
|
|
262
247
|
throw new PostError("delivery row is missing");
|
|
263
|
-
if (
|
|
248
|
+
if (current.state !== "pending" || current.jobId !== job.jobId)
|
|
264
249
|
return undefined;
|
|
265
|
-
if (
|
|
266
|
-
finish(postKey,
|
|
250
|
+
if (reservation.status === "exhausted") {
|
|
251
|
+
finish(postKey, current, {
|
|
252
|
+
state: "dead",
|
|
253
|
+
reason: current.deadlineAt === undefined ? "exhausted" : "expired",
|
|
254
|
+
}, reservation.count);
|
|
267
255
|
return undefined;
|
|
268
256
|
}
|
|
269
|
-
|
|
270
|
-
finish(postKey, row, { state: "dead", reason: "exhausted" });
|
|
271
|
-
return undefined;
|
|
272
|
-
}
|
|
273
|
-
const attempts = row.attempts + 1;
|
|
274
|
-
const nextAttemptAt = Math.min(options.now() + backoffDelay(attempts, postRetryPolicy), row.deadlineAt ?? Number.POSITIVE_INFINITY);
|
|
275
|
-
const started = { ...row, attempts, nextAttemptAt };
|
|
276
|
-
// Preserve recovery evidence before invoking the receiver. A failed receipt
|
|
277
|
-
// write leaves this row available for retry after Watchdog settles the job.
|
|
278
|
-
putRow(postKey, started);
|
|
279
|
-
return started;
|
|
257
|
+
return current;
|
|
280
258
|
});
|
|
281
|
-
if (
|
|
259
|
+
if (row === undefined)
|
|
282
260
|
return { kind: "completed" };
|
|
283
|
-
|
|
284
|
-
const route = routeFor(attempt.envelope);
|
|
261
|
+
const route = routeFor(row.envelope);
|
|
285
262
|
const evidence = await deliverPost({
|
|
286
|
-
attempt,
|
|
263
|
+
attempt: row,
|
|
264
|
+
signal,
|
|
287
265
|
route,
|
|
288
266
|
savePrepared: (envelope) => {
|
|
289
267
|
store.commit(() => {
|
|
@@ -294,173 +272,65 @@ export function createPost(options) {
|
|
|
294
272
|
});
|
|
295
273
|
},
|
|
296
274
|
});
|
|
297
|
-
|
|
298
|
-
store.commit(() => {
|
|
299
|
-
// Another send may have linked a successor during the remote call.
|
|
275
|
+
return store.commit(() => {
|
|
300
276
|
const current = readRow(postKey);
|
|
301
277
|
if (current?.state !== "pending" || current.jobId !== job.jobId)
|
|
302
|
-
return;
|
|
303
|
-
const disposition = postDisposition(evidence, route,
|
|
278
|
+
return { kind: "completed" };
|
|
279
|
+
const disposition = postDisposition(evidence, route, reservation.count, maxDeliveryAttempts, {
|
|
304
280
|
deadlineExpired: current.deadlineAt !== undefined &&
|
|
305
281
|
options.now() >= current.deadlineAt,
|
|
306
282
|
retryUntilDeadline: current.deadlineAt !== undefined,
|
|
307
283
|
});
|
|
308
|
-
if (disposition.state === "keep")
|
|
309
|
-
return;
|
|
284
|
+
if (disposition.state === "pending" || disposition.state === "keep")
|
|
285
|
+
return { kind: "retry" };
|
|
310
286
|
if (disposition.state === "delivered") {
|
|
311
287
|
if (evidence.envelope === undefined)
|
|
312
288
|
throw new PostError("missing delivered fact");
|
|
313
289
|
if (disposition.cursor !== undefined)
|
|
314
290
|
route.recordCursor?.(evidence.envelope, disposition.cursor);
|
|
315
291
|
route.recordDelivered?.(evidence.envelope);
|
|
316
|
-
finish(postKey, current, { state: "delivered" }, deliveryReceipt(evidence));
|
|
317
|
-
}
|
|
318
|
-
else if (disposition.state === "dead") {
|
|
319
|
-
if (disposition.reason === "refused" && evidence.envelope !== undefined)
|
|
320
|
-
route.recordRefused?.(evidence.envelope);
|
|
321
|
-
finish(postKey, current, disposition, deliveryReceipt(evidence));
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
const now = options.now();
|
|
325
|
-
if (current.deadlineAt !== undefined && now >= current.deadlineAt) {
|
|
326
|
-
finish(postKey, current, { state: "dead", reason: "expired" });
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
const due = Math.min(now + backoffDelay(current.attempts, postRetryPolicy), current.deadlineAt ?? Number.POSITIVE_INFINITY);
|
|
330
|
-
if (due !== current.nextAttemptAt) {
|
|
331
|
-
putRow(postKey, { ...current, nextAttemptAt: due });
|
|
332
|
-
retryAt = due;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
292
|
}
|
|
293
|
+
else if (disposition.reason === "refused" &&
|
|
294
|
+
evidence.envelope !== undefined)
|
|
295
|
+
route.recordRefused?.(evidence.envelope);
|
|
296
|
+
finish(postKey, current, disposition, reservation.count, deliveryReceipt(evidence));
|
|
297
|
+
return { kind: "completed" };
|
|
336
298
|
});
|
|
337
|
-
if (retryAt !== undefined)
|
|
338
|
-
await options.scheduleWake(retryAt);
|
|
339
|
-
return { kind: "completed" };
|
|
340
|
-
};
|
|
341
|
-
const enqueueDue = async (limit = 64, afterKey) => {
|
|
342
|
-
const pageSize = Schema.decodeUnknownSync(PageLimit)(limit);
|
|
343
|
-
const rows = store.list(pageSize, afterKey);
|
|
344
|
-
for (const stored of rows) {
|
|
345
|
-
const row = Schema.decodeUnknownSync(PostRowSchema)(stored.value);
|
|
346
|
-
if (row.state !== "pending" || row.jobId === undefined)
|
|
347
|
-
continue;
|
|
348
|
-
const work = await options.readJob(row.jobId);
|
|
349
|
-
if (work.status === "missing")
|
|
350
|
-
throw new PostError("delivery job is missing");
|
|
351
|
-
if (work.job.state !== "settled")
|
|
352
|
-
continue;
|
|
353
|
-
if (row.nextAttemptAt > options.now()) {
|
|
354
|
-
await options.scheduleWake(row.nextAttemptAt);
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
store.commit(() => {
|
|
358
|
-
const current = readRow(row.envelope.key);
|
|
359
|
-
if (current?.state !== "pending" || current.jobId !== row.jobId)
|
|
360
|
-
return;
|
|
361
|
-
if (current.deadlineAt !== undefined &&
|
|
362
|
-
options.now() >= current.deadlineAt)
|
|
363
|
-
finish(current.envelope.key, current, {
|
|
364
|
-
state: "dead",
|
|
365
|
-
reason: "expired",
|
|
366
|
-
});
|
|
367
|
-
else if (current.deadlineAt === undefined &&
|
|
368
|
-
current.attempts >= maxDeliveryAttempts)
|
|
369
|
-
finish(current.envelope.key, current, {
|
|
370
|
-
state: "dead",
|
|
371
|
-
reason: "exhausted",
|
|
372
|
-
});
|
|
373
|
-
else
|
|
374
|
-
enqueueJob(current);
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
return rows.length === pageSize ? rows.at(-1)?.key : undefined;
|
|
378
299
|
};
|
|
379
300
|
const retry = async (input) => {
|
|
380
301
|
const key = Schema.decodeUnknownSync(Key)(input);
|
|
381
|
-
const row = readRow(key);
|
|
382
|
-
if (row === undefined || row.state === "delivered")
|
|
383
|
-
return;
|
|
384
|
-
const work = row.state === "pending" && row.jobId !== undefined
|
|
385
|
-
? await options.readJob(row.jobId)
|
|
386
|
-
: undefined;
|
|
387
|
-
if (work?.status === "missing")
|
|
388
|
-
throw new PostError("delivery job is missing");
|
|
389
|
-
if (work?.status === "found" && work.job.state !== "settled")
|
|
390
|
-
return;
|
|
391
302
|
store.commit(() => {
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
303
|
+
const row = readRow(key);
|
|
304
|
+
if (row === undefined || row.state === "delivered")
|
|
305
|
+
return;
|
|
306
|
+
if (row.state === "pending") {
|
|
307
|
+
options.transactional.expediteQueuedJob(row.jobId, options.now());
|
|
394
308
|
return;
|
|
395
|
-
if (current.state === "pending") {
|
|
396
|
-
if (row.state !== "pending" || current.jobId !== row.jobId)
|
|
397
|
-
return;
|
|
398
|
-
const pending = {
|
|
399
|
-
...current,
|
|
400
|
-
attempts: 0,
|
|
401
|
-
nextAttemptAt: options.now(),
|
|
402
|
-
};
|
|
403
|
-
putRow(key, pending);
|
|
404
|
-
if (current.jobId !== undefined)
|
|
405
|
-
enqueueJob(pending);
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
const retry = {
|
|
409
|
-
state: "pending",
|
|
410
|
-
envelope: current.envelope,
|
|
411
|
-
attempts: 0,
|
|
412
|
-
nextAttemptAt: options.now(),
|
|
413
|
-
delivery: current.delivery,
|
|
414
|
-
};
|
|
415
|
-
const pending = current.deadlineAt === undefined
|
|
416
|
-
? retry
|
|
417
|
-
: { ...retry, deadlineAt: current.deadlineAt };
|
|
418
|
-
putRow(key, pending);
|
|
419
|
-
const queue = queueOf(pending.envelope);
|
|
420
|
-
const previousKey = store.read(tailKey(queue));
|
|
421
|
-
const previous = previousKey === undefined || previousKey === key
|
|
422
|
-
? undefined
|
|
423
|
-
: readRow(previousKey);
|
|
424
|
-
if (previous?.state === "pending" && previousKey !== undefined)
|
|
425
|
-
putRow(previousKey, { ...previous, nextKey: key });
|
|
426
|
-
else
|
|
427
|
-
enqueueJob(pending);
|
|
428
|
-
store.put(tailKey(queue), key);
|
|
429
309
|
}
|
|
310
|
+
enqueueJob({
|
|
311
|
+
state: "pending",
|
|
312
|
+
envelope: row.envelope,
|
|
313
|
+
jobId: ids.work.generate(),
|
|
314
|
+
delivery: row.delivery,
|
|
315
|
+
deadlineAt: row.deadlineAt,
|
|
316
|
+
});
|
|
430
317
|
});
|
|
431
318
|
await options.scheduleWake(options.now());
|
|
432
319
|
};
|
|
433
|
-
const releasePendingRow = async (row, now) => {
|
|
434
|
-
if (row.state !== "pending" ||
|
|
435
|
-
row.jobId === undefined ||
|
|
436
|
-
row.nextAttemptAt <= now)
|
|
437
|
-
return false;
|
|
438
|
-
const work = await options.readJob(row.jobId);
|
|
439
|
-
if (work.status === "missing")
|
|
440
|
-
throw new PostError("delivery job is missing");
|
|
441
|
-
if (work.job.state !== "settled")
|
|
442
|
-
return false;
|
|
443
|
-
return store.commit(() => {
|
|
444
|
-
const current = readRow(row.envelope.key);
|
|
445
|
-
if (current?.state !== "pending" ||
|
|
446
|
-
current.jobId !== row.jobId ||
|
|
447
|
-
current.nextAttemptAt <= now)
|
|
448
|
-
return false;
|
|
449
|
-
const pending = { ...current, nextAttemptAt: now };
|
|
450
|
-
putRow(row.envelope.key, pending);
|
|
451
|
-
enqueueJob(pending);
|
|
452
|
-
return true;
|
|
453
|
-
});
|
|
454
|
-
};
|
|
455
320
|
const releaseNow = async (limit = 64, afterKey) => {
|
|
456
321
|
const pageSize = Schema.decodeUnknownSync(PageLimit)(limit);
|
|
457
322
|
const rows = store.list(pageSize, afterKey);
|
|
458
323
|
const now = options.now();
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
324
|
+
const released = store.commit(() => {
|
|
325
|
+
let changed = false;
|
|
326
|
+
for (const stored of rows) {
|
|
327
|
+
const row = Schema.decodeUnknownSync(PostRowSchema)(stored.value);
|
|
328
|
+
if (row.state === "pending")
|
|
329
|
+
changed =
|
|
330
|
+
options.transactional.expediteQueuedJob(row.jobId, now) || changed;
|
|
331
|
+
}
|
|
332
|
+
return changed;
|
|
333
|
+
});
|
|
464
334
|
if (released)
|
|
465
335
|
await options.scheduleWake(now);
|
|
466
336
|
return rows.length === pageSize ? rows.at(-1)?.key : undefined;
|
|
@@ -491,11 +361,10 @@ export function createPost(options) {
|
|
|
491
361
|
latest,
|
|
492
362
|
retry,
|
|
493
363
|
execute,
|
|
494
|
-
|
|
364
|
+
retryPolicy,
|
|
495
365
|
releaseNow,
|
|
496
366
|
deadLetters,
|
|
497
367
|
read: readRow,
|
|
498
|
-
nextWake,
|
|
499
368
|
});
|
|
500
369
|
}
|
|
501
370
|
/** Consumer admission runs inside the receiver's own SQLite transaction. */
|
package/docs/api/README.md
CHANGED
package/docs/api/inspection.md
CHANGED
|
@@ -278,4 +278,4 @@ jobs or arming a wake. The trusted host must admit an operator before calling.
|
|
|
278
278
|
| Name | Type | Description |
|
|
279
279
|
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
280
280
|
| `email()` | (`input`) => \| \{ `status`: `"uninitialized"`; \} \| \{ `status`: `"available"`; `records`: readonly `object`[]; `nextCursor`: `string` \| `null`; \} | Read aggregate dispatch evidence without email cargo or recipient addresses. |
|
|
281
|
-
| `post()` | (`input`) => \| \{ `status`: `"uninitialized"`; \} \| \{ `status`: `"available"`; `records`: readonly ( \| \{ `key`: `string`; `state`: `"delivered"`; \} \| \{ `key`: `string`; `state`: `"pending"`; `kind`: `string`; `attempts`: `number`; `nextAttemptAt`: `number`; \} \| \{ `key`: `string`; `state`: `"dead"`; `kind`: `string`; `attempts`: `number`; `reason`: `"expired"` \| `"
|
|
281
|
+
| `post()` | (`input`) => \| \{ `status`: `"uninitialized"`; \} \| \{ `status`: `"available"`; `records`: readonly ( \| \{ `key`: `string`; `state`: `"delivered"`; \} \| \{ `key`: `string`; `state`: `"pending"`; `kind`: `string`; `attempts`: `number`; `nextAttemptAt`: `number`; \} \| \{ `key`: `string`; `state`: `"dead"`; `kind`: `string`; `attempts`: `number`; `reason`: `"expired"` \| `"exhausted"` \| `"refused"`; \})[]; `nextCursor`: `string` \| `null`; \} | Read Post metadata; filtered pages retain their continuation through other states. |
|
package/docs/api/post-d1.md
CHANGED
|
@@ -22,7 +22,6 @@ type SqlPostOptions<R> = Readonly<{
|
|
|
22
22
|
maxDelayMs: number;
|
|
23
23
|
}>;
|
|
24
24
|
now: () => number;
|
|
25
|
-
scheduleWake: (at) => Promise<void>;
|
|
26
25
|
}>;
|
|
27
26
|
```
|
|
28
27
|
|
|
@@ -40,15 +39,8 @@ SQL native Carrier runtime configuration; route decisions remain in Post.
|
|
|
40
39
|
|
|
41
40
|
```ts
|
|
42
41
|
type SqlPostRuntime = Readonly<{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
signal?,
|
|
46
|
-
) => Promise<
|
|
47
|
-
Readonly<{
|
|
48
|
-
kind: "completed";
|
|
49
|
-
}>
|
|
50
|
-
>;
|
|
51
|
-
enqueueDue: (limit?, afterKey?) => Promise<string | undefined>;
|
|
42
|
+
retryPolicy: (job) => Promise<WatchdogRetryPolicy>;
|
|
43
|
+
execute: (job, attempt, signal?) => Promise<WatchdogExecutionDecision>;
|
|
52
44
|
read: (key) => Promise<PostRow | undefined>;
|
|
53
45
|
}>;
|
|
54
46
|
```
|
|
@@ -66,8 +58,7 @@ type D1PostSource = Readonly<{
|
|
|
66
58
|
}>;
|
|
67
59
|
```
|
|
68
60
|
|
|
69
|
-
Trusted SELECT yielding post_key, envelope_json, queue_name
|
|
70
|
-
job_id.
|
|
61
|
+
Trusted SELECT yielding post_key, envelope_json, queue_name and job_id.
|
|
71
62
|
|
|
72
63
|
---
|
|
73
64
|
|
|
@@ -124,9 +115,9 @@ Bind the Post registry to D1; route deadlines are rejected with PostError.
|
|
|
124
115
|
|
|
125
116
|
#### Parameters
|
|
126
117
|
|
|
127
|
-
| Parameter | Type
|
|
128
|
-
| --------- |
|
|
129
|
-
| `options` | `Readonly`\<\{ `routes`: `R`; `failureDetail?`: (`cause`) => `string`; `retry?`: `Readonly`\<\{ `maxAttempts`: `number`; `baseDelayMs`: `number`; `maxDelayMs`: `number`; \}\>; `now`: () => `number`;
|
|
118
|
+
| Parameter | Type |
|
|
119
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `options` | `Readonly`\<\{ `routes`: `R`; `failureDetail?`: (`cause`) => `string`; `retry?`: `Readonly`\<\{ `maxAttempts`: `number`; `baseDelayMs`: `number`; `maxDelayMs`: `number`; \}\>; `now`: () => `number`; \}\> & `Readonly`\<\{ `database`: `D1Database`; `watchdog`: `D1Watchdog`; \}\> |
|
|
130
121
|
|
|
131
122
|
#### Returns
|
|
132
123
|
|
|
@@ -63,9 +63,9 @@ PostError.
|
|
|
63
63
|
|
|
64
64
|
#### Parameters
|
|
65
65
|
|
|
66
|
-
| Parameter | Type
|
|
67
|
-
| --------- |
|
|
68
|
-
| `options` | `Readonly`\<\{ `routes`: `R`; `failureDetail?`: (`cause`) => `string`; `retry?`: `Readonly`\<\{ `maxAttempts`: `number`; `baseDelayMs`: `number`; `maxDelayMs`: `number`; \}\>; `now`: () => `number`;
|
|
66
|
+
| Parameter | Type |
|
|
67
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `options` | `Readonly`\<\{ `routes`: `R`; `failureDetail?`: (`cause`) => `string`; `retry?`: `Readonly`\<\{ `maxAttempts`: `number`; `baseDelayMs`: `number`; `maxDelayMs`: `number`; \}\>; `now`: () => `number`; \}\> & `Readonly`\<\{ `database`: `Pick`\<`Pool`, `"query"` \| `"connect"`\>; `watchdog`: `PostgresWatchdog`; \}\> |
|
|
69
69
|
|
|
70
70
|
#### Returns
|
|
71
71
|
|