@fungi.computer/carrier 0.1.0 → 0.2.1

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 CHANGED
@@ -215,20 +215,26 @@ ambiguous first dispatch has zero. Post `attempts` counts receiver invocations.
215
215
 
216
216
  ## Post
217
217
 
218
- Post uses Almanac's shared pure backoff calculation with its retained policy: 2
218
+ Post's ordinary retry policy uses Almanac's shared pure backoff calculation: 2
219
219
  seconds initially, doubling to a 5-minute cap, with at most 20 receiver
220
220
  attempts. The synchronous and SQL adapters share that default policy; explicit
221
- SQL retry policies retain their own delays. Almanac supplies arithmetic only,
222
- not a second scheduler or retry owner.
221
+ SQL retry policies retain their own delays. A SQLite route with `deadlineMs`
222
+ uses its finite lifetime instead of the attempt ceiling. Almanac supplies
223
+ arithmetic only, not a second scheduler or retry owner.
223
224
 
224
225
  `@fungi.computer/carrier/post` delivers typed, point-to-point envelopes between
225
226
  named Durable Objects. `definePostRoute` pairs each address schema and event
226
227
  schema with its Durable Object namespace, name function and fixed `addressKind`
227
- label. Every event route to the same namespace shares that label, so different
228
- event kinds for one recipient keep producer order. The receiver implements
229
- `deliver(envelope)` and calls `admitPost` inside its own SQLite transaction, so
230
- the event mutation and admission receipt commit together. Keys must identify the
231
- producing event across all senders to that receiver.
228
+ label. A SQLite-backed route may opt into a retry lifetime of at most seven days
229
+ with `deadlineMs`; Post stores its absolute deadline at admission and settles an
230
+ undelivered row as an `expired` dead letter when the deadline is reached.
231
+ Deadline routes retry until expiry rather than using the ordinary attempt
232
+ ceiling. Omitted deadlines preserve the existing retry budget. Every event route
233
+ to the same namespace shares that label, so different event kinds for one
234
+ recipient keep producer order. The receiver implements `deliver(envelope)` and
235
+ calls `admitPost` inside its own SQLite transaction, so the event mutation and
236
+ admission receipt commit together. Keys must identify the producing event across
237
+ all senders to that receiver.
232
238
 
233
239
  Construct `createPost` with the producer's structural SQLite `owner`, typed
234
240
  `routes`, Watchdog's `transactional` projection and `readJob` operation, a
@@ -265,9 +271,10 @@ acknowledge a domain ledger in the same producer transaction as the Post
265
271
  receipt. An acknowledgment failure leaves both pending for retry. Prepared facts
266
272
  should carry sealed grants, never raw credentials.
267
273
 
268
- A definite refusal or an exhausted retry budget keeps a dead-letter envelope,
269
- its reason and its attempt count. `deadLetters` returns `letters` and
270
- `nextCursor`, including a continuation through pages that contain only receipts.
274
+ A definite refusal, an exhausted retry budget or a route deadline keeps a
275
+ dead-letter envelope, its reason and its attempt count. `deadLetters` returns
276
+ `letters` and `nextCursor`, including a continuation through pages that contain
277
+ only receipts.
271
278
 
272
279
  The same retry and receipt machinery is exported as `makePostOutbox` for a
273
280
  string-backed outbox. [BirdDog](../bird-dog/README.md) uses it for agent reply
@@ -280,9 +287,13 @@ malformed rows do not request a wake.
280
287
  ## Post transactions on D1 and PostgreSQL
281
288
 
282
289
  `@fungi.computer/carrier/post-d1` and `/post-postgres` bind the ordinary Post
283
- address and event registry to Watchdog's database adapters. Apply `postD1Schema`
284
- or `postPostgresSchema` alongside the matching Watchdog schema through your
285
- migration tool.
290
+ address and event registry to Watchdog's database adapters. These relational
291
+ adapters preserve their existing schema and retry contract and do not support
292
+ route deadlines. Constructing either adapter with a route that sets `deadlineMs`
293
+ throws `PostError` with `reason: "relational-deadline-unsupported"` before
294
+ database access. Use SQLite-backed Post for deadline routes. Apply
295
+ `postD1Schema` or `postPostgresSchema` alongside the matching Watchdog schema
296
+ through your migration tool.
286
297
 
287
298
  On D1, `post.send(envelope)` returns the envelope and job statements. Include
288
299
  both in the same `database.batch` as the producer fact. `sendFromSelect` accepts
@@ -30,7 +30,7 @@ export declare const PostReceiptSummarySchema: Schema.Union<readonly [Schema.Str
30
30
  readonly state: Schema.Literal<"dead">;
31
31
  readonly kind: Schema.String;
32
32
  readonly attempts: Schema.Int;
33
- readonly reason: Schema.Literals<readonly ["refused", "exhausted"]>;
33
+ readonly reason: Schema.Literals<readonly ["refused", "exhausted", "expired"]>;
34
34
  }>]>;
35
35
  /** Metadata for one retained Post row. */
36
36
  export type PostReceiptSummary = Schema.Schema.Type<typeof PostReceiptSummarySchema>;
@@ -53,7 +53,7 @@ export declare const PostInspectionPageSchema: Schema.Union<readonly [Schema.Str
53
53
  readonly state: Schema.Literal<"dead">;
54
54
  readonly kind: Schema.String;
55
55
  readonly attempts: Schema.Int;
56
- readonly reason: Schema.Literals<readonly ["refused", "exhausted"]>;
56
+ readonly reason: Schema.Literals<readonly ["refused", "exhausted", "expired"]>;
57
57
  }>]>>;
58
58
  readonly nextCursor: Schema.NullOr<Schema.String>;
59
59
  }>]>;
@@ -29,7 +29,7 @@ export const PostReceiptSummarySchema = Schema.Union([
29
29
  state: Schema.Literal("dead"),
30
30
  kind: Key,
31
31
  attempts: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
32
- reason: Schema.Literals(["refused", "exhausted"]),
32
+ reason: Schema.Literals(["refused", "exhausted", "expired"]),
33
33
  }),
34
34
  ]);
35
35
  /** Bounded evidence with an explicit distinction between empty and uninitialized storage. */
@@ -18,10 +18,11 @@ export declare const PostDeliveryResultSchema: Schema.Union<readonly [Schema.Lit
18
18
  declare const PostError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
19
19
  readonly _tag: "PostError";
20
20
  } & Readonly<A>;
21
- /** Post rejected an unknown route or conflicting key. */
21
+ /** Post rejected an unknown route, conflicting key, or unsupported policy. */
22
22
  export declare class PostError extends PostError_base<{
23
23
  readonly message: string;
24
+ readonly reason?: "relational-deadline-unsupported";
24
25
  }> {
25
- constructor(message: string);
26
+ constructor(message: string, reason?: "relational-deadline-unsupported");
26
27
  }
27
28
  export {};
@@ -14,9 +14,9 @@ export const PostDeliveryResultSchema = Schema.Union([
14
14
  receipt: Schema.Json,
15
15
  }),
16
16
  ]);
17
- /** Post rejected an unknown route or conflicting key. */
17
+ /** Post rejected an unknown route, conflicting key, or unsupported policy. */
18
18
  export class PostError extends Data.TaggedError("PostError") {
19
- constructor(message) {
20
- super({ message });
19
+ constructor(message, reason) {
20
+ super(reason === undefined ? { message } : { message, reason });
21
21
  }
22
22
  }
@@ -6,6 +6,18 @@ export type DeliveryEvidence = Readonly<{
6
6
  envelope: PostEnvelope | undefined;
7
7
  failure?: unknown;
8
8
  }>;
9
+ /** The settlement a delivery attempt resolves to; internal to the Post backends. */
10
+ export type PostDisposition = Readonly<{
11
+ state: "dead";
12
+ reason: "refused" | "exhausted" | "expired";
13
+ }> | Readonly<{
14
+ state: "pending";
15
+ }> | Readonly<{
16
+ state: "delivered";
17
+ cursor?: number;
18
+ }> | Readonly<{
19
+ state: "keep";
20
+ }>;
9
21
  /** Shared preparation, bounded receiver I/O and response decoding for every Post backend. */
10
22
  export declare function deliverPost(input: Readonly<{
11
23
  attempt: Extract<PostRow, {
@@ -16,30 +28,9 @@ export declare function deliverPost(input: Readonly<{
16
28
  savePrepared: (envelope: PostEnvelope) => void | Promise<void>;
17
29
  }>): Promise<DeliveryEvidence>;
18
30
  /** One settlement decision, shared by native storage adapters. */
19
- export declare function postDisposition(evidence: DeliveryEvidence, route: PostRegistryRoute, attempts: number, maxAttempts: number): {
20
- readonly state: "dead";
21
- readonly reason: "exhausted";
22
- readonly cursor?: never;
23
- } | {
24
- readonly state: "pending";
25
- readonly reason?: never;
26
- readonly cursor?: never;
27
- } | {
28
- readonly state: "dead";
29
- readonly reason: "refused";
30
- readonly cursor?: never;
31
- } | {
32
- readonly state: "keep";
33
- readonly reason?: never;
34
- readonly cursor?: never;
35
- } | {
36
- readonly state: "delivered";
37
- readonly reason?: never;
38
- readonly cursor?: never;
39
- } | {
40
- readonly state: "delivered";
41
- readonly cursor: number;
42
- readonly reason?: never;
43
- };
31
+ export declare function postDisposition(evidence: DeliveryEvidence, route: PostRegistryRoute, attempts: number, maxAttempts: number, options?: Readonly<{
32
+ deadlineExpired?: boolean;
33
+ retryUntilDeadline?: boolean;
34
+ }>): PostDisposition;
44
35
  /** Durable provider/domain receipt data, separate from the staged event. */
45
36
  export declare function deliveryReceipt(evidence: DeliveryEvidence): Schema.Json | undefined;
@@ -38,10 +38,12 @@ export async function deliverPost(input) {
38
38
  return { result: decoded.success, envelope };
39
39
  }
40
40
  /** One settlement decision, shared by native storage adapters. */
41
- export function postDisposition(evidence, route, attempts, maxAttempts) {
41
+ export function postDisposition(evidence, route, attempts, maxAttempts, options = {}) {
42
42
  const { result, envelope } = evidence;
43
+ if (result === "unavailable" && options.deadlineExpired === true)
44
+ return { state: "dead", reason: "expired" };
43
45
  if (result === "unavailable")
44
- return attempts >= maxAttempts
46
+ return !options.retryUntilDeadline && attempts >= maxAttempts
45
47
  ? { state: "dead", reason: "exhausted" }
46
48
  : { state: "pending" };
47
49
  if (envelope === undefined)
@@ -12,7 +12,7 @@ export type PostSql = Readonly<{
12
12
  }>;
13
13
  /** SQL native Carrier runtime configuration; route decisions remain in Post. */
14
14
  export type SqlPostOptions<R extends Record<string, PostRegistryRoute>> = Readonly<{
15
- /** The same address/event registry used by SQLite Post. */
15
+ /** The same registry used by SQLite Post; routes with deadlines are rejected. */
16
16
  routes: R;
17
17
  /** Optional bounded, redacted family diagnostic; never persist raw provider errors. */
18
18
  failureDetail?: (cause: unknown) => string;
@@ -128,6 +128,8 @@ function publicRow(stored) {
128
128
  };
129
129
  }
130
130
  export function openSqlPost(database, options) {
131
+ if (Object.values(options.routes).some((route) => route.deadlineMs !== undefined))
132
+ throw new PostError("relational Post adapters do not support route deadlines", "relational-deadline-unsupported");
131
133
  const policy = Schema.decodeUnknownSync(Schema.Struct({
132
134
  maxAttempts: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: maxDeliveryAttempts })),
133
135
  baseDelayMs: Schema.Int.check(Schema.isGreaterThan(0)),
package/dist/post-d1.d.ts CHANGED
@@ -19,7 +19,7 @@ export type D1Post = SqlPostRuntime & Readonly<{
19
19
  }>;
20
20
  /** Carrier-owned D1 schema, applied by the deployment migration owner. */
21
21
  export declare const postD1Schema: readonly string[];
22
- /** Bind the Post registry to native D1 batch admission and the shared delivery engine. */
22
+ /** Bind the Post registry to D1; route deadlines are rejected with PostError. */
23
23
  export declare function createD1Post<R extends Record<string, PostRegistryRoute>>(options: SqlPostOptions<R> & Readonly<{
24
24
  database: D1Database;
25
25
  watchdog: D1Watchdog;
package/dist/post-d1.js CHANGED
@@ -2,7 +2,7 @@ import { purgeD1Queues, prepareD1Admission, } from "@fungi.computer/watchdog/d1"
2
2
  import { admitPostSelect, jobSelect, openSqlPost, postSchema, preparePost, } from "./internal/post-sql.js";
3
3
  /** Carrier-owned D1 schema, applied by the deployment migration owner. */
4
4
  export const postD1Schema = postSchema(false);
5
- /** Bind the Post registry to native D1 batch admission and the shared delivery engine. */
5
+ /** Bind the Post registry to D1; route deadlines are rejected with PostError. */
6
6
  export function createD1Post(options) {
7
7
  // Only package-owned templates use $N; caller SELECTs remain native SQL.
8
8
  const prepare = (statement) => options.database
@@ -10,7 +10,7 @@ export type PostgresPost = SqlPostRuntime & Readonly<{
10
10
  }>;
11
11
  /** Carrier-owned PostgreSQL schema, applied by the deployment migration owner. */
12
12
  export declare const postPostgresSchema: readonly string[];
13
- /** Bind the Post registry to a PostgreSQL owner and the shared delivery engine. */
13
+ /** Bind the Post registry to PostgreSQL; route deadlines are rejected with PostError. */
14
14
  export declare function createPostgresPost<R extends Record<string, PostRegistryRoute>>(options: SqlPostOptions<R> & Readonly<{
15
15
  database: Pick<Pool, "query" | "connect">;
16
16
  watchdog: PostgresWatchdog;
@@ -10,7 +10,7 @@ const Job = Schema.Struct({
10
10
  priority: Schema.Number,
11
11
  payload_json: Schema.fromJsonString(Schema.Unknown),
12
12
  });
13
- /** Bind the Post registry to a PostgreSQL owner and the shared delivery engine. */
13
+ /** Bind the Post registry to PostgreSQL; route deadlines are rejected with PostError. */
14
14
  export function createPostgresPost(options) {
15
15
  const enqueue = async (client, source) => {
16
16
  const native = source;
package/dist/post.d.ts CHANGED
@@ -23,6 +23,7 @@ export declare const PostRowSchema: Schema.fromJsonString<Schema.Union<readonly
23
23
  }>;
24
24
  readonly attempts: Schema.Int;
25
25
  readonly nextAttemptAt: Schema.Number;
26
+ readonly deadlineAt: Schema.optional<Schema.Number>;
26
27
  readonly jobId: Schema.optional<Schema.String>;
27
28
  readonly delivery: Schema.optional<Schema.Struct<{
28
29
  readonly key: Schema.String;
@@ -44,7 +45,8 @@ export declare const PostRowSchema: Schema.fromJsonString<Schema.Union<readonly
44
45
  readonly event: Schema.Unknown;
45
46
  }>;
46
47
  readonly attempts: Schema.Int;
47
- readonly reason: Schema.Literals<readonly ["refused", "exhausted"]>;
48
+ readonly reason: Schema.Literals<readonly ["refused", "exhausted", "expired"]>;
49
+ readonly deadlineAt: Schema.optional<Schema.Number>;
48
50
  readonly receipt: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
49
51
  readonly delivery: Schema.optional<Schema.Struct<{
50
52
  readonly key: Schema.String;
@@ -62,6 +64,8 @@ export type PostRow = Schema.Schema.Type<typeof PostRowSchema>;
62
64
  export type PostRoute<A, E, D = E, AI = A, EI = E, DI = EI> = Readonly<{
63
65
  /** Fixed registry label shared by routes to one recipient namespace. */
64
66
  addressKind: string;
67
+ /** Optional SQLite Post lifetime of 1 ms to 7 days from admission; relational adapters reject it. */
68
+ deadlineMs?: number | undefined;
65
69
  /** Address parser for this kind. */
66
70
  address: Schema.Codec<A, AI>;
67
71
  /** Staged event parser for this kind. */
@@ -104,6 +108,8 @@ export type PostRoute<A, E, D = E, AI = A, EI = E, DI = EI> = Readonly<{
104
108
  export type PostRegistryRoute = Readonly<{
105
109
  /** Recipient namespace identity used for per-address ordering. */
106
110
  addressKind: string;
111
+ /** Optional SQLite Post lifetime of 1 ms to 7 days from admission; relational adapters reject it. */
112
+ deadlineMs?: number | undefined;
107
113
  /** Parser for this address kind. */
108
114
  address: Schema.Codec<unknown, unknown>;
109
115
  /** Parser for this event kind. */
@@ -147,11 +153,13 @@ export type PostRuntime<R extends Record<string, PostRegistryRoute>> = Readonly<
147
153
  }>>;
148
154
  /** Recover settled delivery jobs and enqueue due retries after a wake. */
149
155
  enqueueDue: (limit?: number, afterKey?: string) => Promise<string | undefined>;
150
- /** Read an ordered page of refused or exhausted keys for operators. */
156
+ /** Release settled pending retries from backoff without resetting attempts. */
157
+ releaseNow: (limit?: number, afterKey?: string) => Promise<string | undefined>;
158
+ /** Read an ordered page of refused, exhausted or expired keys for operators. */
151
159
  deadLetters: (limit?: number, afterKey?: string) => Readonly<{
152
160
  letters: readonly Readonly<{
153
161
  key: string;
154
- reason: "refused" | "exhausted";
162
+ reason: "refused" | "exhausted" | "expired";
155
163
  envelope: PostEnvelope;
156
164
  attempts: number;
157
165
  }>[];
package/dist/post.js CHANGED
@@ -9,6 +9,7 @@ export { makePostOutbox, postOutboxPending } from "./post-outbox.js";
9
9
  const Key = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
10
10
  const JobPayload = Schema.Struct({ postKey: Key });
11
11
  const PageLimit = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(64));
12
+ const maxRouteDeadlineMs = 7 * 24 * 60 * 60 * 1_000;
12
13
  const ids = defineIdFamily({
13
14
  family: "post",
14
15
  version: 1,
@@ -28,6 +29,7 @@ export const PostRowSchema = Schema.fromJsonString(Schema.Union([
28
29
  envelope: PostEnvelopeSchema,
29
30
  attempts: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
30
31
  nextAttemptAt: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)),
32
+ deadlineAt: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
31
33
  jobId: Schema.optional(Key),
32
34
  delivery: Schema.optional(PostEnvelopeSchema),
33
35
  nextKey: Schema.optional(Key),
@@ -41,7 +43,8 @@ export const PostRowSchema = Schema.fromJsonString(Schema.Union([
41
43
  state: Schema.Literal("dead"),
42
44
  envelope: PostEnvelopeSchema,
43
45
  attempts: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
44
- reason: Schema.Literals(["refused", "exhausted"]),
46
+ reason: Schema.Literals(["refused", "exhausted", "expired"]),
47
+ deadlineAt: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
45
48
  receipt: Schema.optional(Schema.Json),
46
49
  delivery: Schema.optional(PostEnvelopeSchema),
47
50
  nextKey: Schema.optional(Key),
@@ -79,6 +82,8 @@ export function definePostRoute(route) {
79
82
  ? receiptOperations(route.address, route.event, route)
80
83
  : receiptOperations(route.address, route.prepare.event, route);
81
84
  const prepare = route.prepare;
85
+ if (route.deadlineMs !== undefined)
86
+ Schema.decodeUnknownSync(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: maxRouteDeadlineMs })))(route.deadlineMs);
82
87
  return {
83
88
  ...route,
84
89
  ...delivery,
@@ -179,12 +184,16 @@ export function createPost(options) {
179
184
  const queue = queueOf(envelope);
180
185
  const previousKey = store.read(tailKey(queue));
181
186
  const previous = previousKey === undefined ? undefined : readRow(previousKey);
182
- const row = {
187
+ const admittedAt = options.now();
188
+ const baseRow = {
183
189
  state: "pending",
184
190
  envelope,
185
191
  attempts: 0,
186
- nextAttemptAt: options.now(),
192
+ nextAttemptAt: admittedAt,
187
193
  };
194
+ const row = route.deadlineMs === undefined
195
+ ? baseRow
196
+ : { ...baseRow, deadlineAt: admittedAt + route.deadlineMs };
188
197
  putRow(envelope.key, row);
189
198
  if (previousKey !== undefined && previous?.state === "pending") {
190
199
  putRow(previousKey, { ...previous, nextKey: envelope.key });
@@ -228,8 +237,13 @@ export function createPost(options) {
228
237
  envelope: row.envelope,
229
238
  attempts: row.attempts,
230
239
  };
240
+ const settled = row.deadlineAt === undefined
241
+ ? dead
242
+ : { ...dead, deadlineAt: row.deadlineAt };
231
243
  receipt =
232
- row.delivery === undefined ? dead : { ...dead, delivery: row.delivery };
244
+ row.delivery === undefined
245
+ ? settled
246
+ : { ...settled, delivery: row.delivery };
233
247
  }
234
248
  else
235
249
  receipt = terminal;
@@ -248,12 +262,16 @@ export function createPost(options) {
248
262
  throw new PostError("delivery row is missing");
249
263
  if (row.state !== "pending" || row.jobId !== job.jobId)
250
264
  return undefined;
251
- if (row.attempts >= maxDeliveryAttempts) {
265
+ if (row.deadlineAt !== undefined && options.now() >= row.deadlineAt) {
266
+ finish(postKey, row, { state: "dead", reason: "expired" });
267
+ return undefined;
268
+ }
269
+ if (row.deadlineAt === undefined && row.attempts >= maxDeliveryAttempts) {
252
270
  finish(postKey, row, { state: "dead", reason: "exhausted" });
253
271
  return undefined;
254
272
  }
255
273
  const attempts = row.attempts + 1;
256
- const nextAttemptAt = options.now() + backoffDelay(attempts, postRetryPolicy);
274
+ const nextAttemptAt = Math.min(options.now() + backoffDelay(attempts, postRetryPolicy), row.deadlineAt ?? Number.POSITIVE_INFINITY);
257
275
  const started = { ...row, attempts, nextAttemptAt };
258
276
  // Preserve recovery evidence before invoking the receiver. A failed receipt
259
277
  // write leaves this row available for retry after Watchdog settles the job.
@@ -282,7 +300,11 @@ export function createPost(options) {
282
300
  const current = readRow(postKey);
283
301
  if (current?.state !== "pending" || current.jobId !== job.jobId)
284
302
  return;
285
- const disposition = postDisposition(evidence, route, current.attempts, maxDeliveryAttempts);
303
+ const disposition = postDisposition(evidence, route, current.attempts, maxDeliveryAttempts, {
304
+ deadlineExpired: current.deadlineAt !== undefined &&
305
+ options.now() >= current.deadlineAt,
306
+ retryUntilDeadline: current.deadlineAt !== undefined,
307
+ });
286
308
  if (disposition.state === "keep")
287
309
  return;
288
310
  if (disposition.state === "delivered") {
@@ -299,10 +321,16 @@ export function createPost(options) {
299
321
  finish(postKey, current, disposition, deliveryReceipt(evidence));
300
322
  }
301
323
  else {
302
- const due = options.now() + backoffDelay(current.attempts, postRetryPolicy);
303
- if (due !== current.nextAttemptAt) {
304
- putRow(postKey, { ...current, nextAttemptAt: due });
305
- retryAt = due;
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
+ }
306
334
  }
307
335
  }
308
336
  });
@@ -330,7 +358,14 @@ export function createPost(options) {
330
358
  const current = readRow(row.envelope.key);
331
359
  if (current?.state !== "pending" || current.jobId !== row.jobId)
332
360
  return;
333
- if (current.attempts >= maxDeliveryAttempts)
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)
334
369
  finish(current.envelope.key, current, {
335
370
  state: "dead",
336
371
  reason: "exhausted",
@@ -370,13 +405,16 @@ export function createPost(options) {
370
405
  enqueueJob(pending);
371
406
  }
372
407
  else {
373
- const pending = {
408
+ const retry = {
374
409
  state: "pending",
375
410
  envelope: current.envelope,
376
411
  attempts: 0,
377
412
  nextAttemptAt: options.now(),
378
413
  delivery: current.delivery,
379
414
  };
415
+ const pending = current.deadlineAt === undefined
416
+ ? retry
417
+ : { ...retry, deadlineAt: current.deadlineAt };
380
418
  putRow(key, pending);
381
419
  const queue = queueOf(pending.envelope);
382
420
  const previousKey = store.read(tailKey(queue));
@@ -392,6 +430,41 @@ export function createPost(options) {
392
430
  });
393
431
  await options.scheduleWake(options.now());
394
432
  };
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
+ const releaseNow = async (limit = 64, afterKey) => {
456
+ const pageSize = Schema.decodeUnknownSync(PageLimit)(limit);
457
+ const rows = store.list(pageSize, afterKey);
458
+ 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
+ }
464
+ if (released)
465
+ await options.scheduleWake(now);
466
+ return rows.length === pageSize ? rows.at(-1)?.key : undefined;
467
+ };
395
468
  const deadLetters = (limit = 64, afterKey) => {
396
469
  const pageSize = Schema.decodeUnknownSync(PageLimit)(limit);
397
470
  const rows = store.list(pageSize, afterKey);
@@ -419,6 +492,7 @@ export function createPost(options) {
419
492
  retry,
420
493
  execute,
421
494
  enqueueDue,
495
+ releaseNow,
422
496
  deadLetters,
423
497
  read: readRow,
424
498
  nextWake,
@@ -157,7 +157,7 @@ const PostReceiptSummarySchema: Union<
157
157
  state: Literal<"dead">;
158
158
  kind: String;
159
159
  attempts: Int;
160
- reason: Literals<readonly ["refused", "exhausted"]>;
160
+ reason: Literals<readonly ["refused", "exhausted", "expired"]>;
161
161
  }>,
162
162
  ]
163
163
  >;
@@ -188,7 +188,7 @@ const PostInspectionPageSchema: Union<readonly [Struct<{
188
188
  state: Literal<"dead">;
189
189
  kind: String;
190
190
  attempts: Int;
191
- reason: Literals<readonly [..., ...]>;
191
+ reason: Literals<readonly [..., ..., ...]>;
192
192
  }>]>>;
193
193
  nextCursor: NullOr<String>;
194
194
  }>]>;
@@ -275,7 +275,7 @@ jobs or arming a wake. The trusted host must admit an operator before calling.
275
275
 
276
276
  #### Returns
277
277
 
278
- | Name | Type | Description |
279
- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
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`: `"refused"` \| `"exhausted"`; \})[]; `nextCursor`: `string` \| `null`; \} | Read Post metadata; filtered pages retain their continuation through other states. |
278
+ | Name | Type | Description |
279
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
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. |
@@ -114,14 +114,13 @@ Carrier-owned D1 schema, applied by the deployment migration owner.
114
114
  function createD1Post<R>(options): D1Post;
115
115
  ```
116
116
 
117
- Bind the Post registry to native D1 batch admission and the shared delivery
118
- engine.
117
+ Bind the Post registry to D1; route deadlines are rejected with PostError.
119
118
 
120
119
  #### Type Parameters
121
120
 
122
- | Type Parameter |
123
- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
124
- | `R` _extends_ `Record`\<`string`, `Readonly`\<\{ `addressKind`: `string`; `address`: `Codec`\<`unknown`, `unknown`\>; `event`: `Codec`\<`unknown`, `unknown`\>; `encodeEnvelope`: (`envelope`) => `object`; `parseDelivery`: (`envelope`) => `object`; `prepareEnvelope?`: (`envelope`, `signal`) => `Promise`\<\{ `key`: `string`; `kind`: `string`; `address`: `unknown`; `event`: `unknown`; \}\>; `recordDelivered?`: (`envelope`) => `void`; `nameOf`: (`envelope`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<[`PostDeliveryResult`](post.md#postdeliveryresult)\>; \}\>; \}\>; `recordCursor?`: (`envelope`, `cursor`) => `void`; `recordRefused?`: (`envelope`) => `void`; \}\>\> |
121
+ | Type Parameter |
122
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
123
+ | `R` _extends_ `Record`\<`string`, `Readonly`\<\{ `addressKind`: `string`; `deadlineMs?`: `number`; `address`: `Codec`\<`unknown`, `unknown`\>; `event`: `Codec`\<`unknown`, `unknown`\>; `encodeEnvelope`: (`envelope`) => `object`; `parseDelivery`: (`envelope`) => `object`; `prepareEnvelope?`: (`envelope`, `signal`) => `Promise`\<\{ `key`: `string`; `kind`: `string`; `address`: `unknown`; `event`: `unknown`; \}\>; `recordDelivered?`: (`envelope`) => `void`; `nameOf`: (`envelope`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<[`PostDeliveryResult`](post.md#postdeliveryresult)\>; \}\>; \}\>; `recordCursor?`: (`envelope`, `cursor`) => `void`; `recordRefused?`: (`envelope`) => `void`; \}\>\> |
125
124
 
126
125
  #### Parameters
127
126
 
@@ -52,13 +52,14 @@ Carrier-owned PostgreSQL schema, applied by the deployment migration owner.
52
52
  function createPostgresPost<R>(options): PostgresPost;
53
53
  ```
54
54
 
55
- Bind the Post registry to a PostgreSQL owner and the shared delivery engine.
55
+ Bind the Post registry to PostgreSQL; route deadlines are rejected with
56
+ PostError.
56
57
 
57
58
  #### Type Parameters
58
59
 
59
- | Type Parameter |
60
- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
61
- | `R` _extends_ `Record`\<`string`, `Readonly`\<\{ `addressKind`: `string`; `address`: `Codec`\<`unknown`, `unknown`\>; `event`: `Codec`\<`unknown`, `unknown`\>; `encodeEnvelope`: (`envelope`) => `object`; `parseDelivery`: (`envelope`) => `object`; `prepareEnvelope?`: (`envelope`, `signal`) => `Promise`\<\{ `key`: `string`; `kind`: `string`; `address`: `unknown`; `event`: `unknown`; \}\>; `recordDelivered?`: (`envelope`) => `void`; `nameOf`: (`envelope`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<[`PostDeliveryResult`](post.md#postdeliveryresult)\>; \}\>; \}\>; `recordCursor?`: (`envelope`, `cursor`) => `void`; `recordRefused?`: (`envelope`) => `void`; \}\>\> |
60
+ | Type Parameter |
61
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
+ | `R` _extends_ `Record`\<`string`, `Readonly`\<\{ `addressKind`: `string`; `deadlineMs?`: `number`; `address`: `Codec`\<`unknown`, `unknown`\>; `event`: `Codec`\<`unknown`, `unknown`\>; `encodeEnvelope`: (`envelope`) => `object`; `parseDelivery`: (`envelope`) => `object`; `prepareEnvelope?`: (`envelope`, `signal`) => `Promise`\<\{ `key`: `string`; `kind`: `string`; `address`: `unknown`; `event`: `unknown`; \}\>; `recordDelivered?`: (`envelope`) => `void`; `nameOf`: (`envelope`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<[`PostDeliveryResult`](post.md#postdeliveryresult)\>; \}\>; \}\>; `recordCursor?`: (`envelope`, `cursor`) => `void`; `recordRefused?`: (`envelope`) => `void`; \}\>\> |
62
63
 
63
64
  #### Parameters
64
65
 
package/docs/api/post.md CHANGED
@@ -12,26 +12,27 @@
12
12
 
13
13
  ### PostError
14
14
 
15
- Post rejected an unknown route or conflicting key.
15
+ Post rejected an unknown route, conflicting key, or unsupported policy.
16
16
 
17
17
  #### Extends
18
18
 
19
19
  - `YieldableError`\<`this`\> & `object` & `Readonly`\<\{ `message`: `string`;
20
- \}\>
20
+ `reason?`: `"relational-deadline-unsupported"`; \}\>
21
21
 
22
22
  #### Constructors
23
23
 
24
24
  ##### Constructor
25
25
 
26
26
  ```ts
27
- new PostError(message): PostError;
27
+ new PostError(message, reason?): PostError;
28
28
  ```
29
29
 
30
30
  ###### Parameters
31
31
 
32
- | Parameter | Type |
33
- | --------- | -------- |
34
- | `message` | `string` |
32
+ | Parameter | Type |
33
+ | --------- | ----------------------------------- |
34
+ | `message` | `string` |
35
+ | `reason?` | `"relational-deadline-unsupported"` |
35
36
 
36
37
  ###### Returns
37
38
 
@@ -42,11 +43,24 @@ new PostError(message): PostError;
42
43
  ```ts
43
44
  Data.TaggedError("PostError")<{
44
45
  readonly message: string;
46
+ readonly reason?: "relational-deadline-unsupported";
45
47
  }>.constructor
46
48
  ```
47
49
 
48
50
  #### Properties
49
51
 
52
+ ##### reason?
53
+
54
+ ```ts
55
+ readonly optional reason?: "relational-deadline-unsupported";
56
+ ```
57
+
58
+ ###### Inherited from
59
+
60
+ ```ts
61
+ Data.TaggedError("PostError").reason;
62
+ ```
63
+
50
64
  ##### message
51
65
 
52
66
  ```ts
@@ -159,6 +173,7 @@ Pending row, terminal receipt, or dead letter.
159
173
  ```ts
160
174
  type PostRoute<A, E, D, AI, EI, DI> = Readonly<{
161
175
  addressKind: string;
176
+ deadlineMs?: number;
162
177
  address: Schema.Codec<A, AI>;
163
178
  event: Schema.Codec<E, EI>;
164
179
  name: (address) => string;
@@ -205,6 +220,7 @@ Schema-correlated ordinary events or prepared delivery facts.
205
220
  ```ts
206
221
  type PostRegistryRoute = Readonly<{
207
222
  addressKind: string;
223
+ deadlineMs?: number;
208
224
  address: Schema.Codec<unknown, unknown>;
209
225
  event: Schema.Codec<unknown, unknown>;
210
226
  encodeEnvelope: (envelope) => PostEnvelope;
@@ -233,13 +249,14 @@ type PostRuntime<R> = Readonly<{
233
249
  }>
234
250
  >;
235
251
  enqueueDue: (limit?, afterKey?) => Promise<string | undefined>;
252
+ releaseNow: (limit?, afterKey?) => Promise<string | undefined>;
236
253
  deadLetters: (
237
254
  limit?,
238
255
  afterKey?,
239
256
  ) => Readonly<{
240
257
  letters: readonly Readonly<{
241
258
  key: string;
242
- reason: "refused" | "exhausted";
259
+ reason: "refused" | "exhausted" | "expired";
243
260
  envelope: PostEnvelope;
244
261
  attempts: number;
245
262
  }>[];
@@ -323,6 +340,7 @@ const PostRowSchema: fromJsonString<
323
340
  }>;
324
341
  attempts: Int;
325
342
  nextAttemptAt: Number;
343
+ deadlineAt: optional<Number>;
326
344
  jobId: optional<String>;
327
345
  delivery: optional<
328
346
  Struct<{
@@ -348,7 +366,8 @@ const PostRowSchema: fromJsonString<
348
366
  event: Unknown;
349
367
  }>;
350
368
  attempts: Int;
351
- reason: Literals<readonly ["refused", "exhausted"]>;
369
+ reason: Literals<readonly ["refused", "exhausted", "expired"]>;
370
+ deadlineAt: optional<Number>;
352
371
  receipt: optional<Codec<Json, Json, never, never>>;
353
372
  delivery: optional<
354
373
  Struct<{
@@ -432,6 +451,7 @@ may let their owner rest.
432
451
  ```ts
433
452
  function definePostRoute<A, E, AI, EI>(route): PostRoute<A, E, E, AI, EI, EI> & Omit<Readonly<{
434
453
  addressKind: string;
454
+ deadlineMs?: number;
435
455
  address: Codec<unknown, unknown>;
436
456
  event: Codec<unknown, unknown>;
437
457
  encodeEnvelope: (envelope) => object;
@@ -467,29 +487,30 @@ Preserve each route's inferred address and event types in a registry.
467
487
 
468
488
  ##### Parameters
469
489
 
470
- | Parameter | Type |
471
- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
472
- | `route` | `Readonly`\<\{ `addressKind`: `string`; `address`: `Codec`\<`A`, `AI`\>; `event`: `Codec`\<`E`, `EI`\>; `name`: (`address`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<[`PostDeliveryResult`](#postdeliveryresult)\>; \}\>; \}\>; \}\> & `Readonly`\<\{ `prepare?`: `undefined`; `onDelivered?`: (`address`, `event`) => `void`; `onCursor?`: (`address`, `event`, `cursor`) => `void`; `onRefused?`: (`address`, `event`) => `void`; \}\> |
490
+ | Parameter | Type |
491
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
492
+ | `route` | `Readonly`\<\{ `addressKind`: `string`; `deadlineMs?`: `number`; `address`: `Codec`\<`A`, `AI`\>; `event`: `Codec`\<`E`, `EI`\>; `name`: (`address`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<[`PostDeliveryResult`](#postdeliveryresult)\>; \}\>; \}\>; \}\> & `Readonly`\<\{ `prepare?`: `undefined`; `onDelivered?`: (`address`, `event`) => `void`; `onCursor?`: (`address`, `event`, `cursor`) => `void`; `onRefused?`: (`address`, `event`) => `void`; \}\> |
473
493
 
474
494
  ##### Returns
475
495
 
476
496
  [`PostRoute`](#postroute)\<`A`, `E`, `E`, `AI`, `EI`, `EI`\> &
477
- `Omit`\<`Readonly`\<\{ `addressKind`: `string`; `address`: `Codec`\<`unknown`,
478
- `unknown`\>; `event`: `Codec`\<`unknown`, `unknown`\>; `encodeEnvelope`:
479
- (`envelope`) => `object`; `parseDelivery`: (`envelope`) => `object`;
480
- `prepareEnvelope?`: (`envelope`, `signal`) => `Promise`\<\{ `key`: `string`;
481
- `kind`: `string`; `address`: `unknown`; `event`: `unknown`; \}\>;
482
- `recordDelivered?`: (`envelope`) => `void`; `nameOf`: (`envelope`) => `string`;
483
- `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`:
484
- (`envelope`) => `Promise`\<...\>; \}\>; \}\>; `recordCursor?`: (`envelope`,
485
- `cursor`) => `void`; `recordRefused?`: (`envelope`) => `void`; \}\>, `"address"`
486
- \| `"event"`\>
497
+ `Omit`\<`Readonly`\<\{ `addressKind`: `string`; `deadlineMs?`: `number`;
498
+ `address`: `Codec`\<`unknown`, `unknown`\>; `event`: `Codec`\<`unknown`,
499
+ `unknown`\>; `encodeEnvelope`: (`envelope`) => `object`; `parseDelivery`:
500
+ (`envelope`) => `object`; `prepareEnvelope?`: (`envelope`, `signal`) =>
501
+ `Promise`\<\{ `key`: `string`; `kind`: `string`; `address`: `unknown`; `event`:
502
+ `unknown`; \}\>; `recordDelivered?`: (`envelope`) => `void`; `nameOf`:
503
+ (`envelope`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) =>
504
+ `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<...\>; \}\>; \}\>;
505
+ `recordCursor?`: (`envelope`, `cursor`) => `void`; `recordRefused?`:
506
+ (`envelope`) => `void`; \}\>, `"address"` \| `"event"`\>
487
507
 
488
508
  #### Call Signature
489
509
 
490
510
  ```ts
491
511
  function definePostRoute<A, E, D, AI, EI, DI>(route): PostRoute<A, E, D, AI, EI, DI> & Omit<Readonly<{
492
512
  addressKind: string;
513
+ deadlineMs?: number;
493
514
  address: Codec<unknown, unknown>;
494
515
  event: Codec<unknown, unknown>;
495
516
  encodeEnvelope: (envelope) => object;
@@ -527,23 +548,23 @@ Preserve each route's inferred address and event types in a registry.
527
548
 
528
549
  ##### Parameters
529
550
 
530
- | Parameter | Type |
531
- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
532
- | `route` | `Readonly`\<\{ `addressKind`: `string`; `address`: `Codec`\<`A`, `AI`\>; `event`: `Codec`\<`E`, `EI`\>; `name`: (`address`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<[`PostDeliveryResult`](#postdeliveryresult)\>; \}\>; \}\>; \}\> & `Readonly`\<\{ `prepare`: `Readonly`\<\{ `event`: `Schema.Codec`\<`D`, `DI`\>; `run`: (`address`, `intent`, `signal`) => `Promise`\<`D`\>; \}\>; `onDelivered?`: (`address`, `event`) => `void`; `onCursor?`: (`address`, `event`, `cursor`) => `void`; `onRefused?`: (`address`, `event`) => `void`; \}\> |
551
+ | Parameter | Type |
552
+ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
553
+ | `route` | `Readonly`\<\{ `addressKind`: `string`; `deadlineMs?`: `number`; `address`: `Codec`\<`A`, `AI`\>; `event`: `Codec`\<`E`, `EI`\>; `name`: (`address`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<[`PostDeliveryResult`](#postdeliveryresult)\>; \}\>; \}\>; \}\> & `Readonly`\<\{ `prepare`: `Readonly`\<\{ `event`: `Schema.Codec`\<`D`, `DI`\>; `run`: (`address`, `intent`, `signal`) => `Promise`\<`D`\>; \}\>; `onDelivered?`: (`address`, `event`) => `void`; `onCursor?`: (`address`, `event`, `cursor`) => `void`; `onRefused?`: (`address`, `event`) => `void`; \}\> |
533
554
 
534
555
  ##### Returns
535
556
 
536
557
  [`PostRoute`](#postroute)\<`A`, `E`, `D`, `AI`, `EI`, `DI`\> &
537
- `Omit`\<`Readonly`\<\{ `addressKind`: `string`; `address`: `Codec`\<`unknown`,
538
- `unknown`\>; `event`: `Codec`\<`unknown`, `unknown`\>; `encodeEnvelope`:
539
- (`envelope`) => `object`; `parseDelivery`: (`envelope`) => `object`;
540
- `prepareEnvelope?`: (`envelope`, `signal`) => `Promise`\<\{ `key`: `string`;
541
- `kind`: `string`; `address`: `unknown`; `event`: `unknown`; \}\>;
542
- `recordDelivered?`: (`envelope`) => `void`; `nameOf`: (`envelope`) => `string`;
543
- `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`:
544
- (`envelope`) => `Promise`\<...\>; \}\>; \}\>; `recordCursor?`: (`envelope`,
545
- `cursor`) => `void`; `recordRefused?`: (`envelope`) => `void`; \}\>, `"address"`
546
- \| `"event"`\>
558
+ `Omit`\<`Readonly`\<\{ `addressKind`: `string`; `deadlineMs?`: `number`;
559
+ `address`: `Codec`\<`unknown`, `unknown`\>; `event`: `Codec`\<`unknown`,
560
+ `unknown`\>; `encodeEnvelope`: (`envelope`) => `object`; `parseDelivery`:
561
+ (`envelope`) => `object`; `prepareEnvelope?`: (`envelope`, `signal`) =>
562
+ `Promise`\<\{ `key`: `string`; `kind`: `string`; `address`: `unknown`; `event`:
563
+ `unknown`; \}\>; `recordDelivered?`: (`envelope`) => `void`; `nameOf`:
564
+ (`envelope`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) =>
565
+ `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<...\>; \}\>; \}\>;
566
+ `recordCursor?`: (`envelope`, `cursor`) => `void`; `recordRefused?`:
567
+ (`envelope`) => `void`; \}\>, `"address"` \| `"event"`\>
547
568
 
548
569
  ---
549
570
 
@@ -557,9 +578,9 @@ Decode at both persisted and DO boundaries with the route's Effect Schema.
557
578
 
558
579
  #### Type Parameters
559
580
 
560
- | Type Parameter |
561
- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
562
- | `R` _extends_ `Record`\<`string`, `Readonly`\<\{ `addressKind`: `string`; `address`: `Codec`\<`unknown`, `unknown`\>; `event`: `Codec`\<`unknown`, `unknown`\>; `encodeEnvelope`: (`envelope`) => `object`; `parseDelivery`: (`envelope`) => `object`; `prepareEnvelope?`: (`envelope`, `signal`) => `Promise`\<\{ `key`: `string`; `kind`: `string`; `address`: `unknown`; `event`: `unknown`; \}\>; `recordDelivered?`: (`envelope`) => `void`; `nameOf`: (`envelope`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<[`PostDeliveryResult`](#postdeliveryresult)\>; \}\>; \}\>; `recordCursor?`: (`envelope`, `cursor`) => `void`; `recordRefused?`: (`envelope`) => `void`; \}\>\> |
581
+ | Type Parameter |
582
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
583
+ | `R` _extends_ `Record`\<`string`, `Readonly`\<\{ `addressKind`: `string`; `deadlineMs?`: `number`; `address`: `Codec`\<`unknown`, `unknown`\>; `event`: `Codec`\<`unknown`, `unknown`\>; `encodeEnvelope`: (`envelope`) => `object`; `parseDelivery`: (`envelope`) => `object`; `prepareEnvelope?`: (`envelope`, `signal`) => `Promise`\<\{ `key`: `string`; `kind`: `string`; `address`: `unknown`; `event`: `unknown`; \}\>; `recordDelivered?`: (`envelope`) => `void`; `nameOf`: (`envelope`) => `string`; `namespace`: `Readonly`\<\{ `getByName`: (`name`) => `Readonly`\<\{ `deliver`: (`envelope`) => `Promise`\<[`PostDeliveryResult`](#postdeliveryresult)\>; \}\>; \}\>; `recordCursor?`: (`envelope`, `cursor`) => `void`; `recordRefused?`: (`envelope`) => `void`; \}\>\> |
563
584
 
564
585
  #### Parameters
565
586
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/carrier",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "Durable email submission and recipient delivery tracking",
6
6
  "type": "module",
@@ -48,7 +48,7 @@
48
48
  ],
49
49
  "dependencies": {
50
50
  "@cloudflare/workers-types": "5.20260828.1",
51
- "@fungi.computer/almanac": "0.2.0",
51
+ "@fungi.computer/almanac": "0.2.1",
52
52
  "@fungi.computer/cairn": "0.1.0",
53
53
  "@fungi.computer/watchdog": "0.1.0",
54
54
  "effect": "4.0.2",