@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/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, WatchdogRuntime, WatchdogTransactionalProjection } from "@fungi.computer/watchdog";
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.optional<Schema.String>;
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
- /** Run as the Watchdog executor for a claimed Post job. */
151
- execute: (job: WatchdogRunningJob) => Promise<Readonly<{
152
- kind: "completed";
153
- }>>;
154
- /** Recover settled delivery jobs and enqueue due retries after a wake. */
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 { backoffDelay } from "@fungi.computer/almanac";
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: Schema.optional(Key),
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
- const jobId = ids.work.generate();
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
- attempts: 0,
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
- putRow(envelope.key, row);
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 releaseNext = (nextKey) => {
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: row.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, row.nextKey === undefined
253
- ? receipt
254
- : { ...receipt, nextKey: row.nextKey });
255
- releaseNext(row.nextKey);
226
+ putRow(key, receipt);
256
227
  };
257
- const execute = async (job) => {
228
+ const retryPolicy = (job) => {
258
229
  const { postKey } = Schema.decodeUnknownSync(JobPayload)(job.payload);
259
- const attempt = store.commit(() => {
260
- const row = readRow(postKey);
261
- if (row === undefined)
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 (row.state !== "pending" || row.jobId !== job.jobId)
248
+ if (current.state !== "pending" || current.jobId !== job.jobId)
264
249
  return undefined;
265
- if (row.deadlineAt !== undefined && options.now() >= row.deadlineAt) {
266
- finish(postKey, row, { state: "dead", reason: "expired" });
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
- if (row.deadlineAt === undefined && row.attempts >= maxDeliveryAttempts) {
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 (attempt === undefined)
259
+ if (row === undefined)
282
260
  return { kind: "completed" };
283
- await options.scheduleWake(attempt.nextAttemptAt);
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
- let retryAt;
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, current.attempts, maxDeliveryAttempts, {
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 current = readRow(key);
393
- if (current === undefined || current.state === "delivered")
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
- let released = false;
460
- for (const stored of rows) {
461
- const row = Schema.decodeUnknownSync(PostRowSchema)(stored.value);
462
- released = (await releasePendingRow(row, now)) || released;
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
- enqueueDue,
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. */
@@ -17,4 +17,3 @@
17
17
  | [post-d1](post-d1.md) | - |
18
18
  | [post-postgres](post-postgres.md) | - |
19
19
  | [post](post.md) | - |
20
- | [testing](testing.md) | - |
@@ -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"` \| `"refused"` \| `"exhausted"`; \})[]; `nextCursor`: `string` \| `null`; \} | Read Post metadata; filtered pages retain their continuation through other states. |
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. |
@@ -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
- execute: (
44
- job,
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, next_attempt_at and
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`; `scheduleWake`: (`at`) => `Promise`\<`void`\>; \}\> & `Readonly`\<\{ `database`: `D1Database`; `watchdog`: `D1Watchdog`; \}\> |
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`; `scheduleWake`: (`at`) => `Promise`\<`void`\>; \}\> & `Readonly`\<\{ `database`: `Pick`\<`Pool`, `"query"` \| `"connect"`\>; `watchdog`: `PostgresWatchdog`; \}\> |
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