@fungi.computer/carrier 0.2.1 → 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/docs/api/post.md CHANGED
@@ -12,12 +12,13 @@
12
12
 
13
13
  ### PostError
14
14
 
15
- Post rejected an unknown route, conflicting key, or unsupported policy.
15
+ Post rejected an unknown route, conflicting key, unsupported policy or store.
16
16
 
17
17
  #### Extends
18
18
 
19
19
  - `YieldableError`\<`this`\> & `object` & `Readonly`\<\{ `message`: `string`;
20
- `reason?`: `"relational-deadline-unsupported"`; \}\>
20
+ `reason?`: `"relational-deadline-unsupported"` \| `"pre-scheduler-store"`;
21
+ \}\>
21
22
 
22
23
  #### Constructors
23
24
 
@@ -29,10 +30,10 @@ new PostError(message, reason?): PostError;
29
30
 
30
31
  ###### Parameters
31
32
 
32
- | Parameter | Type |
33
- | --------- | ----------------------------------- |
34
- | `message` | `string` |
35
- | `reason?` | `"relational-deadline-unsupported"` |
33
+ | Parameter | Type |
34
+ | --------- | -------------------------------------------------------------- |
35
+ | `message` | `string` |
36
+ | `reason?` | `"relational-deadline-unsupported"` \| `"pre-scheduler-store"` |
36
37
 
37
38
  ###### Returns
38
39
 
@@ -43,7 +44,7 @@ new PostError(message, reason?): PostError;
43
44
  ```ts
44
45
  Data.TaggedError("PostError")<{
45
46
  readonly message: string;
46
- readonly reason?: "relational-deadline-unsupported";
47
+ readonly reason?: "relational-deadline-unsupported" | "pre-scheduler-store";
47
48
  }>.constructor
48
49
  ```
49
50
 
@@ -52,7 +53,7 @@ Data.TaggedError("PostError")<{
52
53
  ##### reason?
53
54
 
54
55
  ```ts
55
- readonly optional reason?: "relational-deadline-unsupported";
56
+ readonly optional reason?: "relational-deadline-unsupported" | "pre-scheduler-store";
56
57
  ```
57
58
 
58
59
  ###### Inherited from
@@ -96,58 +97,6 @@ Receiver's idempotent admission decision.
96
97
 
97
98
  ---
98
99
 
99
- ### PostOutboxStore
100
-
101
- ```ts
102
- type PostOutboxStore<E> = Readonly<{
103
- put: (key, value) => Effect.Effect<void, E>;
104
- read: (key) => Effect.Effect<string | undefined, E>;
105
- list: (
106
- limit,
107
- afterKey?,
108
- ) => Effect.Effect<
109
- readonly Readonly<{
110
- key: string;
111
- value: string;
112
- }>[],
113
- E
114
- >;
115
- delete: (key) => Effect.Effect<void, E>;
116
- }>;
117
- ```
118
-
119
- Durable, string-backed rows. A delivered receipt must outlive source replay.
120
-
121
- #### Type Parameters
122
-
123
- | Type Parameter |
124
- | -------------- |
125
- | `E` |
126
-
127
- ---
128
-
129
- ### PostOutbox
130
-
131
- ```ts
132
- type PostOutbox<M, E> = Readonly<{
133
- enqueue: (message, at?) => Effect.Effect<number | undefined, E>;
134
- drain: () => Effect.Effect<void, E>;
135
- nextWakeAt: () => Effect.Effect<number | undefined, E>;
136
- deadLetters: (limit, afterKey?) => Effect.Effect<M[], E>;
137
- }>;
138
- ```
139
-
140
- Bounded generic delivery owned by Post.
141
-
142
- #### Type Parameters
143
-
144
- | Type Parameter |
145
- | -------------- |
146
- | `M` |
147
- | `E` |
148
-
149
- ---
150
-
151
100
  ### PostEnvelope
152
101
 
153
102
  ```ts
@@ -243,12 +192,8 @@ Runtime operations that preserve the route's schema evidence.
243
192
  ```ts
244
193
  type PostRuntime<R> = Readonly<{
245
194
  send: <K>(input) => void;
246
- execute: (job) => Promise<
247
- Readonly<{
248
- kind: "completed";
249
- }>
250
- >;
251
- enqueueDue: (limit?, afterKey?) => Promise<string | undefined>;
195
+ retryPolicy: (job) => Promise<WatchdogRetryPolicy>;
196
+ execute: (job, attempt, signal?) => Promise<WatchdogExecutionDecision>;
252
197
  releaseNow: (limit?, afterKey?) => Promise<string | undefined>;
253
198
  deadLetters: (
254
199
  limit?,
@@ -273,7 +218,6 @@ type PostRuntime<R> = Readonly<{
273
218
  | undefined;
274
219
  retry: (key) => Promise<void>;
275
220
  read: (key) => PostRow | undefined;
276
- nextWake: () => number | null;
277
221
  }>;
278
222
  ```
279
223
 
@@ -338,10 +282,8 @@ const PostRowSchema: fromJsonString<
338
282
  address: Unknown;
339
283
  event: Unknown;
340
284
  }>;
341
- attempts: Int;
342
- nextAttemptAt: Number;
343
285
  deadlineAt: optional<Number>;
344
- jobId: optional<String>;
286
+ jobId: String;
345
287
  delivery: optional<
346
288
  Struct<{
347
289
  key: String;
@@ -350,12 +292,10 @@ const PostRowSchema: fromJsonString<
350
292
  event: Unknown;
351
293
  }>
352
294
  >;
353
- nextKey: optional<String>;
354
295
  }>,
355
296
  Struct<{
356
297
  state: Literal<"delivered">;
357
298
  receipt: optional<Codec<Json, Json, never, never>>;
358
- nextKey: optional<String>;
359
299
  }>,
360
300
  Struct<{
361
301
  state: Literal<"dead">;
@@ -377,7 +317,6 @@ const PostRowSchema: fromJsonString<
377
317
  event: Unknown;
378
318
  }>
379
319
  >;
380
- nextKey: optional<String>;
381
320
  }>,
382
321
  ]
383
322
  >
@@ -388,62 +327,6 @@ Persisted Post row parser.
388
327
 
389
328
  ## Functions
390
329
 
391
- ### makePostOutbox()
392
-
393
- ```ts
394
- function makePostOutbox<M, E>(options): PostOutbox<M, E>;
395
- ```
396
-
397
- The same bounded delivery and receipt machinery serves Post and BirdDog.
398
-
399
- #### Type Parameters
400
-
401
- | Type Parameter |
402
- | -------------- |
403
- | `M` |
404
- | `E` |
405
-
406
- #### Parameters
407
-
408
- | Parameter | Type |
409
- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
410
- | `options` | `Readonly`\<\{ `message`: `Schema.Codec`\<`M`\>; `key`: (`message`) => `string`; `outbox`: [`PostOutboxStore`](#postoutboxstore)\<`E`\>; `deliver`: (`message`) => `Effect.Effect`\<`boolean`, `E`\>; `scheduleWake`: (`at`) => `Effect.Effect`\<`void`, `E`\>; `now`: () => `number`; `invalidRow`: () => `E`; `encodingFailed`: () => `E`; \}\> |
411
-
412
- #### Returns
413
-
414
- [`PostOutbox`](#postoutbox)\<`M`, `E`\>
415
-
416
- ---
417
-
418
- ### postOutboxPending()
419
-
420
- ```ts
421
- function postOutboxPending<M, E>(options): Effect<boolean, E>;
422
- ```
423
-
424
- Whether any message in an outbox still awaits a delivery attempt. Delivered
425
- receipts and exhausted dead letters are retained but owe nothing more, so a host
426
- may let their owner rest.
427
-
428
- #### Type Parameters
429
-
430
- | Type Parameter |
431
- | -------------- |
432
- | `M` |
433
- | `E` |
434
-
435
- #### Parameters
436
-
437
- | Parameter | Type |
438
- | --------- | --------------------------------------------------------------------------------------------------------------------------------- |
439
- | `options` | `Readonly`\<\{ `message`: `Schema.Codec`\<`M`\>; `outbox`: `Pick`\<[`PostOutboxStore`](#postoutboxstore)\<`E`\>, `"list"`\>; \}\> |
440
-
441
- #### Returns
442
-
443
- `Effect`\<`boolean`, `E`\>
444
-
445
- ---
446
-
447
330
  ### definePostRoute()
448
331
 
449
332
  #### Call Signature
@@ -584,9 +467,9 @@ Decode at both persisted and DO boundaries with the route's Effect Schema.
584
467
 
585
468
  #### Parameters
586
469
 
587
- | Parameter | Type |
588
- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
589
- | `options` | `Readonly`\<\{ `routes`: `R`; `owner`: `WatchdogSqliteOwner`; `transactional`: `WatchdogTransactionalProjection`; `readJob`: `WatchdogRuntime`\[`"read"`\]; `now`: () => `number`; `scheduleWake`: (`at`) => `Promise`\<`void`\>; \}\> |
470
+ | Parameter | Type |
471
+ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
472
+ | `options` | `Readonly`\<\{ `routes`: `R`; `owner`: `WatchdogSqliteOwner`; `transactional`: `WatchdogTransactionalProjection`; `now`: () => `number`; `scheduleWake`: (`at`) => `Promise`\<`void`\>; \}\> |
590
473
 
591
474
  #### Returns
592
475
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/carrier",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "Durable email submission and recipient delivery tracking",
6
6
  "type": "module",
@@ -34,10 +34,6 @@
34
34
  "./post-postgres": {
35
35
  "types": "./dist/post-postgres.d.ts",
36
36
  "import": "./dist/post-postgres.js"
37
- },
38
- "./testing": {
39
- "types": "./dist/testing.d.ts",
40
- "import": "./dist/testing.js"
41
37
  }
42
38
  },
43
39
  "files": [
@@ -50,7 +46,7 @@
50
46
  "@cloudflare/workers-types": "5.20260828.1",
51
47
  "@fungi.computer/almanac": "0.2.1",
52
48
  "@fungi.computer/cairn": "0.1.0",
53
- "@fungi.computer/watchdog": "0.1.0",
49
+ "@fungi.computer/watchdog": "0.1.1",
54
50
  "effect": "4.0.2",
55
51
  "pg": "8.23.0",
56
52
  "@types/pg": "8.23.1"
@@ -1,52 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import * as Schema from "effect/Schema";
3
- import { type RetryPolicy } from "@fungi.computer/almanac";
4
- /** Maximum receiver attempts retained from BirdDog. */
5
- export declare const maxDeliveryAttempts = 20;
6
- /** BirdDog's retained receiver retry policy, shared by every Post adapter. */
7
- export declare const postRetryPolicy: RetryPolicy;
8
- /** Durable, string-backed rows. A delivered receipt must outlive source replay. */
9
- export type PostOutboxStore<E> = Readonly<{
10
- /** Commit one pending row or completion receipt. */
11
- put: (key: string, value: string) => Effect.Effect<void, E>;
12
- /** Inspect one durable row by key. */
13
- read: (key: string) => Effect.Effect<string | undefined, E>;
14
- /** Read an ordered bounded page. */
15
- list: (limit: number, afterKey?: string) => Effect.Effect<readonly Readonly<{
16
- key: string;
17
- value: string;
18
- }>[], E>;
19
- /** Discard one malformed row. */
20
- delete: (key: string) => Effect.Effect<void, E>;
21
- }>;
22
- /** Bounded generic delivery owned by Post. */
23
- export type PostOutbox<M, E> = Readonly<{
24
- /** Queue a message and return its wake hint so a fan-out can schedule once. */
25
- enqueue: (message: M, at?: number) => Effect.Effect<number | undefined, E>;
26
- /** Attempt one bounded page and arrange the next wake. */
27
- drain: () => Effect.Effect<void, E>;
28
- /** Earliest owed delivery, including a future retry; absent when none remain. */
29
- nextWakeAt: () => Effect.Effect<number | undefined, E>;
30
- /** Inspect exhausted messages in an ordered page. */
31
- deadLetters: (limit: number, afterKey?: string) => Effect.Effect<M[], E>;
32
- }>;
33
- /** The same bounded delivery and receipt machinery serves Post and BirdDog. */
34
- export declare function makePostOutbox<M, E>(options: Readonly<{
35
- message: Schema.Codec<M>;
36
- key: (message: M) => string;
37
- outbox: PostOutboxStore<E>;
38
- deliver: (message: M) => Effect.Effect<boolean, E>;
39
- scheduleWake: (at: number) => Effect.Effect<void, E>;
40
- now: () => number;
41
- invalidRow: () => E;
42
- encodingFailed: () => E;
43
- }>): PostOutbox<M, E>;
44
- /**
45
- * Whether any message in an outbox still awaits a delivery attempt. Delivered
46
- * receipts and exhausted dead letters are retained but owe nothing more, so a
47
- * host may let their owner rest.
48
- */
49
- export declare function postOutboxPending<M, E>(options: Readonly<{
50
- message: Schema.Codec<M>;
51
- outbox: Pick<PostOutboxStore<E>, "list">;
52
- }>): Effect.Effect<boolean, E>;
@@ -1,152 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import * as Schema from "effect/Schema";
3
- import { backoffDelay } from "@fungi.computer/almanac";
4
- const PageSize = 64;
5
- /** Maximum receiver attempts retained from BirdDog. */
6
- export const maxDeliveryAttempts = 20;
7
- /** BirdDog's retained receiver retry policy, shared by every Post adapter. */
8
- export const postRetryPolicy = {
9
- baseMs: 2_000,
10
- maxMs: 300_000,
11
- };
12
- const earlier = (a, b) => b === undefined ? a : a === undefined ? b : Math.min(a, b);
13
- const logDeliveryFailure = (cause) => {
14
- console.error("Carrier Post outbox delivery failed", {
15
- failureKind: cause instanceof Error ? "Error" : "NonErrorThrown",
16
- });
17
- };
18
- /** One durable row: a message still owed delivery, or its delivered receipt. */
19
- function postRowText(message) {
20
- return Schema.fromJsonString(Schema.Union([
21
- Schema.Struct({
22
- mail: message,
23
- attempts: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
24
- nextAttemptAt: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
25
- }),
26
- Schema.Struct({ state: Schema.Literal("delivered") }),
27
- ]));
28
- }
29
- /** The same bounded delivery and receipt machinery serves Post and BirdDog. */
30
- export function makePostOutbox(options) {
31
- const Text = postRowText(options.message);
32
- const decode = (value) => Schema.decodeUnknownEffect(Text)(value).pipe(Effect.mapError(options.invalidRow));
33
- const encode = (row) => Effect.try({
34
- try: () => Schema.encodeSync(Text)(row),
35
- catch: options.encodingFailed,
36
- });
37
- const enqueue = (message, at = options.now()) => Effect.gen(function* () {
38
- const key = options.key(message);
39
- const existing = yield* options.outbox.read(key);
40
- if (existing !== undefined) {
41
- const row = yield* decode(existing);
42
- if ("state" in row || row.attempts >= maxDeliveryAttempts)
43
- return;
44
- return Math.max(at, row.nextAttemptAt ?? at);
45
- }
46
- const value = yield* encode({ mail: message, attempts: 0 });
47
- yield* options.outbox.put(key, value);
48
- return at;
49
- });
50
- const process = (stored, now) => Effect.gen(function* () {
51
- const decoded = yield* Effect.result(decode(stored.value));
52
- if (decoded._tag === "Failure") {
53
- yield* options.outbox.delete(stored.key);
54
- return undefined;
55
- }
56
- const row = decoded.success;
57
- if ("state" in row || row.attempts >= maxDeliveryAttempts)
58
- return undefined;
59
- if (row.nextAttemptAt !== undefined && row.nextAttemptAt > now)
60
- return row.nextAttemptAt;
61
- const delivered = yield* options.deliver(row.mail).pipe(Effect.catch((cause) => Effect.sync(() => {
62
- logDeliveryFailure(cause);
63
- }).pipe(Effect.as(false))));
64
- if (delivered) {
65
- yield* options.outbox.put(stored.key, yield* encode({ state: "delivered" }));
66
- return undefined;
67
- }
68
- const attempts = row.attempts + 1;
69
- const nextAttemptAt = now + backoffDelay(attempts, postRetryPolicy);
70
- yield* options.outbox.put(stored.key, yield* encode({ mail: row.mail, attempts, nextAttemptAt }));
71
- return attempts < maxDeliveryAttempts ? nextAttemptAt : undefined;
72
- });
73
- let cursor;
74
- let earliestFuture;
75
- const drain = () => Effect.gen(function* () {
76
- const now = options.now();
77
- const rows = yield* options.outbox.list(PageSize, cursor);
78
- for (const row of rows)
79
- earliestFuture = earlier(earliestFuture, yield* process(row, now));
80
- if (rows.length === PageSize) {
81
- const lastKey = rows[rows.length - 1]?.key;
82
- if (lastKey !== undefined &&
83
- (yield* options.outbox.list(1, lastKey)).length > 0) {
84
- cursor = lastKey;
85
- yield* options.scheduleWake(now);
86
- return;
87
- }
88
- }
89
- cursor = undefined;
90
- if (earliestFuture !== undefined)
91
- yield* options.scheduleWake(Math.max(now, earliestFuture));
92
- earliestFuture = undefined;
93
- });
94
- const deadLetters = (limit, afterKey) => Effect.gen(function* () {
95
- const rows = yield* options.outbox.list(limit, afterKey);
96
- const dead = [];
97
- for (const stored of rows) {
98
- const row = yield* decode(stored.value);
99
- if (!("state" in row) && row.attempts >= maxDeliveryAttempts)
100
- dead.push(row.mail);
101
- }
102
- return dead;
103
- });
104
- const nextWakeAt = () => Effect.gen(function* () {
105
- let afterKey;
106
- let due;
107
- for (;;) {
108
- const rows = yield* options.outbox.list(PageSize, afterKey);
109
- for (const stored of rows) {
110
- // Malformed rows are discarded by drain, not deliverable work.
111
- const decoded = yield* Effect.result(decode(stored.value));
112
- if (decoded._tag === "Failure")
113
- continue;
114
- const row = decoded.success;
115
- if (!("state" in row) && row.attempts < maxDeliveryAttempts)
116
- due = earlier(due, row.nextAttemptAt ?? options.now());
117
- }
118
- const lastKey = rows.at(-1)?.key;
119
- if (rows.length < PageSize || lastKey === undefined)
120
- return due;
121
- afterKey = lastKey;
122
- }
123
- });
124
- return { enqueue, drain, deadLetters, nextWakeAt };
125
- }
126
- /**
127
- * Whether any message in an outbox still awaits a delivery attempt. Delivered
128
- * receipts and exhausted dead letters are retained but owe nothing more, so a
129
- * host may let their owner rest.
130
- */
131
- export function postOutboxPending(options) {
132
- const Text = postRowText(options.message);
133
- return Effect.gen(function* () {
134
- let afterKey;
135
- for (;;) {
136
- const rows = yield* options.outbox.list(PageSize, afterKey);
137
- for (const stored of rows) {
138
- // A malformed row is not deliverable; the next drain discards it.
139
- const decoded = yield* Effect.result(Schema.decodeUnknownEffect(Text)(stored.value));
140
- if (decoded._tag === "Failure")
141
- continue;
142
- const row = decoded.success;
143
- if (!("state" in row) && row.attempts < maxDeliveryAttempts)
144
- return true;
145
- }
146
- const lastKey = rows.at(-1)?.key;
147
- if (rows.length < PageSize || lastKey === undefined)
148
- return false;
149
- afterKey = lastKey;
150
- }
151
- });
152
- }
package/dist/testing.d.ts DELETED
@@ -1,16 +0,0 @@
1
- /** String-row outbox for deterministic tests and quick starts; no persistence. */
2
- export type MemoryOutbox = Readonly<{
3
- /** Store or replace one encoded row. */
4
- put: (key: string, value: string) => Promise<void>;
5
- /** Read a retained row, if present. */
6
- read: (key: string) => Promise<string | undefined>;
7
- /** Read a key-ordered page strictly after the supplied cursor. */
8
- list: (limit: number, afterKey?: string) => Promise<readonly Readonly<{
9
- key: string;
10
- value: string;
11
- }>[]>;
12
- /** Remove one row. */
13
- delete: (key: string) => Promise<void>;
14
- }>;
15
- /** Create an independent in-process outbox for trusted tests and examples. */
16
- export declare function createMemoryOutbox(): MemoryOutbox;
package/dist/testing.js DELETED
@@ -1,20 +0,0 @@
1
- /** Create an independent in-process outbox for trusted tests and examples. */
2
- export function createMemoryOutbox() {
3
- const rows = new Map();
4
- return {
5
- put: (key, value) => {
6
- rows.set(key, value);
7
- return Promise.resolve();
8
- },
9
- read: (key) => Promise.resolve(rows.get(key)),
10
- list: (limit, afterKey) => Promise.resolve([...rows]
11
- .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
12
- .filter(([key]) => afterKey === undefined || key > afterKey)
13
- .slice(0, limit)
14
- .map(([key, value]) => ({ key, value }))),
15
- delete: (key) => {
16
- rows.delete(key);
17
- return Promise.resolve();
18
- },
19
- };
20
- }
@@ -1,46 +0,0 @@
1
- <!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
2
-
3
- [**@fungi.computer/carrier**](README.md)
4
-
5
- ---
6
-
7
- [@fungi.computer/carrier](README.md) / testing
8
-
9
- # testing
10
-
11
- ## Type Aliases
12
-
13
- ### MemoryOutbox
14
-
15
- ```ts
16
- type MemoryOutbox = Readonly<{
17
- put: (key, value) => Promise<void>;
18
- read: (key) => Promise<string | undefined>;
19
- list: (
20
- limit,
21
- afterKey?,
22
- ) => Promise<
23
- readonly Readonly<{
24
- key: string;
25
- value: string;
26
- }>[]
27
- >;
28
- delete: (key) => Promise<void>;
29
- }>;
30
- ```
31
-
32
- String-row outbox for deterministic tests and quick starts; no persistence.
33
-
34
- ## Functions
35
-
36
- ### createMemoryOutbox()
37
-
38
- ```ts
39
- function createMemoryOutbox(): MemoryOutbox;
40
- ```
41
-
42
- Create an independent in-process outbox for trusted tests and examples.
43
-
44
- #### Returns
45
-
46
- [`MemoryOutbox`](#memoryoutbox)