@fungi.computer/carrier 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +468 -2
  3. package/dist/cloudflare.d.ts +32 -0
  4. package/dist/cloudflare.js +80 -0
  5. package/dist/email.d.ts +2 -0
  6. package/dist/email.js +2 -0
  7. package/dist/index.d.ts +21 -0
  8. package/dist/index.js +28 -0
  9. package/dist/inspection.d.ts +108 -0
  10. package/dist/inspection.js +188 -0
  11. package/dist/internal/cloudflare-send.d.ts +9 -0
  12. package/dist/internal/cloudflare-send.js +79 -0
  13. package/dist/internal/contracts.d.ts +94 -0
  14. package/dist/internal/contracts.js +7 -0
  15. package/dist/internal/owner.d.ts +24 -0
  16. package/dist/internal/owner.js +419 -0
  17. package/dist/internal/policy.d.ts +25 -0
  18. package/dist/internal/policy.js +18 -0
  19. package/dist/internal/post-contract.d.ts +27 -0
  20. package/dist/internal/post-contract.js +22 -0
  21. package/dist/internal/post-execution.d.ts +45 -0
  22. package/dist/internal/post-execution.js +73 -0
  23. package/dist/internal/post-sql.d.ts +51 -0
  24. package/dist/internal/post-sql.js +259 -0
  25. package/dist/internal/receipt-schema.d.ts +3 -0
  26. package/dist/internal/receipt-schema.js +12 -0
  27. package/dist/post-d1.d.ts +36 -0
  28. package/dist/post-d1.js +55 -0
  29. package/dist/post-outbox.d.ts +52 -0
  30. package/dist/post-outbox.js +152 -0
  31. package/dist/post-postgres.d.ts +25 -0
  32. package/dist/post-postgres.js +79 -0
  33. package/dist/post.d.ts +189 -0
  34. package/dist/post.js +437 -0
  35. package/dist/testing.d.ts +16 -0
  36. package/dist/testing.js +20 -0
  37. package/docs/api/README.md +20 -0
  38. package/docs/api/cloudflare.md +100 -0
  39. package/docs/api/email.md +87 -0
  40. package/docs/api/index.md +343 -0
  41. package/docs/api/inspection.md +281 -0
  42. package/docs/api/post-d1.md +184 -0
  43. package/docs/api/post-postgres.md +108 -0
  44. package/docs/api/post.md +599 -0
  45. package/docs/api/testing.md +46 -0
  46. package/package.json +95 -3
package/dist/post.js ADDED
@@ -0,0 +1,437 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { PostError, } from "./internal/post-contract.js";
3
+ export { PostError, PostDeliveryResultSchema, } from "./internal/post-contract.js";
4
+ import { deliverPost, postDisposition, deliveryReceipt, } from "./internal/post-execution.js";
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";
9
+ const Key = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
10
+ const JobPayload = Schema.Struct({ postKey: Key });
11
+ const PageLimit = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(64));
12
+ const ids = defineIdFamily({
13
+ family: "post",
14
+ version: 1,
15
+ kinds: { work: { prefix: "post_work" } },
16
+ });
17
+ /** Wire envelope parsed at the durable row and receiver boundaries. */
18
+ export const PostEnvelopeSchema = Schema.Struct({
19
+ key: Key,
20
+ kind: Key,
21
+ address: Schema.Unknown,
22
+ event: Schema.Unknown,
23
+ });
24
+ /** Persisted Post row parser. */
25
+ export const PostRowSchema = Schema.fromJsonString(Schema.Union([
26
+ Schema.Struct({
27
+ state: Schema.Literal("pending"),
28
+ envelope: PostEnvelopeSchema,
29
+ attempts: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
30
+ nextAttemptAt: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)),
31
+ jobId: Schema.optional(Key),
32
+ delivery: Schema.optional(PostEnvelopeSchema),
33
+ nextKey: Schema.optional(Key),
34
+ }),
35
+ Schema.Struct({
36
+ state: Schema.Literal("delivered"),
37
+ receipt: Schema.optional(Schema.Json),
38
+ nextKey: Schema.optional(Key),
39
+ }),
40
+ Schema.Struct({
41
+ state: Schema.Literal("dead"),
42
+ envelope: PostEnvelopeSchema,
43
+ attempts: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
44
+ reason: Schema.Literals(["refused", "exhausted"]),
45
+ receipt: Schema.optional(Schema.Json),
46
+ delivery: Schema.optional(PostEnvelopeSchema),
47
+ nextKey: Schema.optional(Key),
48
+ }),
49
+ ]));
50
+ function receiptOperations(address, event, hooks) {
51
+ const { onCursor, onRefused, onDelivered } = hooks;
52
+ const decodeAddress = Schema.decodeUnknownSync(address);
53
+ const decodeEvent = Schema.decodeUnknownSync(event);
54
+ return {
55
+ recordCursor: onCursor === undefined
56
+ ? undefined
57
+ : (envelope, cursor) => {
58
+ onCursor(decodeAddress(envelope.address), decodeEvent(envelope.event), cursor);
59
+ },
60
+ recordRefused: onRefused === undefined
61
+ ? undefined
62
+ : (envelope) => {
63
+ onRefused(decodeAddress(envelope.address), decodeEvent(envelope.event));
64
+ },
65
+ recordDelivered: onDelivered === undefined
66
+ ? undefined
67
+ : (envelope) => {
68
+ onDelivered(decodeAddress(envelope.address), decodeEvent(envelope.event));
69
+ },
70
+ parseDelivery: (envelope) => ({
71
+ ...envelope,
72
+ address: Schema.encodeSync(address)(decodeAddress(envelope.address)),
73
+ event: Schema.encodeSync(event)(decodeEvent(envelope.event)),
74
+ }),
75
+ };
76
+ }
77
+ export function definePostRoute(route) {
78
+ const delivery = route.prepare === undefined
79
+ ? receiptOperations(route.address, route.event, route)
80
+ : receiptOperations(route.address, route.prepare.event, route);
81
+ const prepare = route.prepare;
82
+ return {
83
+ ...route,
84
+ ...delivery,
85
+ addressKind: Schema.decodeUnknownSync(Key)(route.addressKind),
86
+ prepareEnvelope: prepare === undefined
87
+ ? undefined
88
+ : async (envelope, signal) => ({
89
+ ...envelope,
90
+ event: Schema.encodeUnknownSync(prepare.event)(await prepare.run(Schema.decodeUnknownSync(route.address)(envelope.address), Schema.decodeUnknownSync(route.event)(envelope.event), signal)),
91
+ }),
92
+ encodeEnvelope: (envelope) => ({
93
+ ...envelope,
94
+ address: Schema.encodeUnknownSync(route.address)(envelope.address),
95
+ event: Schema.encodeUnknownSync(route.event)(envelope.event),
96
+ }),
97
+ nameOf: (envelope) => route.name(Schema.decodeUnknownSync(route.address)(envelope.address)),
98
+ };
99
+ }
100
+ const StoredRow = Schema.Struct({ key: Schema.String, value: Schema.String });
101
+ const DueRow = Schema.Struct({
102
+ due: Schema.NullOr(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
103
+ });
104
+ const openPostStore = (owner) => {
105
+ const sql = owner.sql;
106
+ owner.transactionSync(() => {
107
+ sql.exec("CREATE TABLE IF NOT EXISTS carrier_post_rows (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
108
+ });
109
+ return {
110
+ commit: (mutation) => owner.transactionSync(mutation),
111
+ read: (key) => {
112
+ const row = sql
113
+ .exec("SELECT key,value FROM carrier_post_rows WHERE key=?", key)
114
+ .toArray()[0];
115
+ return row === undefined
116
+ ? undefined
117
+ : Schema.decodeUnknownSync(StoredRow)(row).value;
118
+ },
119
+ put: (key, value) => {
120
+ sql.exec("INSERT INTO carrier_post_rows VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", key, value);
121
+ },
122
+ list: (limit, afterKey = "") => Schema.decodeUnknownSync(Schema.Array(StoredRow))(sql
123
+ .exec("SELECT key,value FROM carrier_post_rows WHERE key LIKE 'post:%' AND key>? ORDER BY key LIMIT ?", afterKey, limit)
124
+ .toArray()),
125
+ nextWake: () => Schema.decodeUnknownSync(DueRow)(sql
126
+ .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'")
127
+ .toArray()[0]).due,
128
+ };
129
+ };
130
+ /** Decode at both persisted and DO boundaries with the route's Effect Schema. */
131
+ export function createPost(options) {
132
+ const store = openPostStore(options.owner);
133
+ const rowKey = (key) => `post:${key}`;
134
+ const tailKey = (queue) => `post-tail:${queue}`;
135
+ const headKey = (kind, queue) => `post-head:${JSON.stringify([kind, queue])}`;
136
+ const readRow = (key) => {
137
+ const raw = store.read(rowKey(key));
138
+ return raw === undefined
139
+ ? undefined
140
+ : Schema.decodeUnknownSync(PostRowSchema)(raw);
141
+ };
142
+ const nextWake = () => store.nextWake();
143
+ const putRow = (key, row) => {
144
+ store.put(rowKey(key), Schema.encodeSync(PostRowSchema)(row));
145
+ };
146
+ const routeFor = (envelope) => {
147
+ const route = options.routes[envelope.kind];
148
+ if (route === undefined)
149
+ throw new PostError("unknown address kind");
150
+ return route;
151
+ };
152
+ const queueOf = (envelope) => {
153
+ const route = routeFor(envelope);
154
+ return JSON.stringify([route.addressKind, route.nameOf(envelope)]);
155
+ };
156
+ const enqueueJob = (row) => {
157
+ const jobId = ids.work.generate();
158
+ putRow(row.envelope.key, { ...row, jobId });
159
+ options.transactional.enqueueAcceptedJob({
160
+ jobId,
161
+ queue: queueOf(row.envelope),
162
+ lane: "post",
163
+ priority: 0,
164
+ payload: { postKey: row.envelope.key },
165
+ state: "queued",
166
+ });
167
+ };
168
+ const send = (input) => {
169
+ const incoming = Schema.decodeUnknownSync(PostEnvelopeSchema)(input);
170
+ const route = routeFor(incoming);
171
+ const envelope = route.encodeEnvelope(incoming);
172
+ const existing = readRow(envelope.key);
173
+ if (existing !== undefined) {
174
+ if (existing.state !== "delivered" &&
175
+ JSON.stringify(existing.envelope) !== JSON.stringify(envelope))
176
+ throw new PostError("conflicting post key");
177
+ return;
178
+ }
179
+ const queue = queueOf(envelope);
180
+ const previousKey = store.read(tailKey(queue));
181
+ const previous = previousKey === undefined ? undefined : readRow(previousKey);
182
+ const row = {
183
+ state: "pending",
184
+ envelope,
185
+ attempts: 0,
186
+ nextAttemptAt: options.now(),
187
+ };
188
+ putRow(envelope.key, row);
189
+ if (previousKey !== undefined && previous?.state === "pending") {
190
+ putRow(previousKey, { ...previous, nextKey: envelope.key });
191
+ }
192
+ else {
193
+ enqueueJob(row);
194
+ }
195
+ store.put(tailKey(queue), envelope.key);
196
+ store.put(headKey(envelope.kind, queue), envelope.key);
197
+ };
198
+ const latest = (kind, address) => {
199
+ const route = options.routes[kind];
200
+ if (route === undefined)
201
+ throw new PostError("unknown address kind");
202
+ const queue = JSON.stringify([
203
+ route.addressKind,
204
+ route.nameOf({
205
+ address: Schema.encodeUnknownSync(route.address)(address),
206
+ }),
207
+ ]);
208
+ const key = store.read(headKey(kind, queue));
209
+ if (key === undefined)
210
+ return undefined;
211
+ const row = readRow(key);
212
+ if (row === undefined)
213
+ throw new PostError("latest delivery row is missing");
214
+ return { key, row };
215
+ };
216
+ const releaseNext = (nextKey) => {
217
+ if (nextKey === undefined)
218
+ return;
219
+ const next = readRow(nextKey);
220
+ if (next?.state === "pending" && next.jobId === undefined)
221
+ enqueueJob(next);
222
+ };
223
+ const finish = (key, row, terminal, metadata) => {
224
+ let receipt;
225
+ if (terminal.state === "dead") {
226
+ const dead = {
227
+ ...terminal,
228
+ envelope: row.envelope,
229
+ attempts: row.attempts,
230
+ };
231
+ receipt =
232
+ row.delivery === undefined ? dead : { ...dead, delivery: row.delivery };
233
+ }
234
+ else
235
+ receipt = terminal;
236
+ if (metadata !== undefined)
237
+ receipt = { ...receipt, receipt: metadata };
238
+ putRow(key, row.nextKey === undefined
239
+ ? receipt
240
+ : { ...receipt, nextKey: row.nextKey });
241
+ releaseNext(row.nextKey);
242
+ };
243
+ const execute = async (job) => {
244
+ const { postKey } = Schema.decodeUnknownSync(JobPayload)(job.payload);
245
+ const attempt = store.commit(() => {
246
+ const row = readRow(postKey);
247
+ if (row === undefined)
248
+ throw new PostError("delivery row is missing");
249
+ if (row.state !== "pending" || row.jobId !== job.jobId)
250
+ return undefined;
251
+ if (row.attempts >= maxDeliveryAttempts) {
252
+ finish(postKey, row, { state: "dead", reason: "exhausted" });
253
+ return undefined;
254
+ }
255
+ const attempts = row.attempts + 1;
256
+ const nextAttemptAt = options.now() + backoffDelay(attempts, postRetryPolicy);
257
+ const started = { ...row, attempts, nextAttemptAt };
258
+ // Preserve recovery evidence before invoking the receiver. A failed receipt
259
+ // write leaves this row available for retry after Watchdog settles the job.
260
+ putRow(postKey, started);
261
+ return started;
262
+ });
263
+ if (attempt === undefined)
264
+ return { kind: "completed" };
265
+ await options.scheduleWake(attempt.nextAttemptAt);
266
+ const route = routeFor(attempt.envelope);
267
+ const evidence = await deliverPost({
268
+ attempt,
269
+ route,
270
+ savePrepared: (envelope) => {
271
+ store.commit(() => {
272
+ const current = readRow(postKey);
273
+ if (current?.state !== "pending" || current.jobId !== job.jobId)
274
+ throw new PostError("preparation superseded");
275
+ putRow(postKey, { ...current, delivery: envelope });
276
+ });
277
+ },
278
+ });
279
+ let retryAt;
280
+ store.commit(() => {
281
+ // Another send may have linked a successor during the remote call.
282
+ const current = readRow(postKey);
283
+ if (current?.state !== "pending" || current.jobId !== job.jobId)
284
+ return;
285
+ const disposition = postDisposition(evidence, route, current.attempts, maxDeliveryAttempts);
286
+ if (disposition.state === "keep")
287
+ return;
288
+ if (disposition.state === "delivered") {
289
+ if (evidence.envelope === undefined)
290
+ throw new PostError("missing delivered fact");
291
+ if (disposition.cursor !== undefined)
292
+ route.recordCursor?.(evidence.envelope, disposition.cursor);
293
+ route.recordDelivered?.(evidence.envelope);
294
+ finish(postKey, current, { state: "delivered" }, deliveryReceipt(evidence));
295
+ }
296
+ else if (disposition.state === "dead") {
297
+ if (disposition.reason === "refused" && evidence.envelope !== undefined)
298
+ route.recordRefused?.(evidence.envelope);
299
+ finish(postKey, current, disposition, deliveryReceipt(evidence));
300
+ }
301
+ else {
302
+ const due = options.now() + backoffDelay(current.attempts, postRetryPolicy);
303
+ if (due !== current.nextAttemptAt) {
304
+ putRow(postKey, { ...current, nextAttemptAt: due });
305
+ retryAt = due;
306
+ }
307
+ }
308
+ });
309
+ if (retryAt !== undefined)
310
+ await options.scheduleWake(retryAt);
311
+ return { kind: "completed" };
312
+ };
313
+ const enqueueDue = async (limit = 64, afterKey) => {
314
+ const pageSize = Schema.decodeUnknownSync(PageLimit)(limit);
315
+ const rows = store.list(pageSize, afterKey);
316
+ for (const stored of rows) {
317
+ const row = Schema.decodeUnknownSync(PostRowSchema)(stored.value);
318
+ if (row.state !== "pending" || row.jobId === undefined)
319
+ continue;
320
+ const work = await options.readJob(row.jobId);
321
+ if (work.status === "missing")
322
+ throw new PostError("delivery job is missing");
323
+ if (work.job.state !== "settled")
324
+ continue;
325
+ if (row.nextAttemptAt > options.now()) {
326
+ await options.scheduleWake(row.nextAttemptAt);
327
+ continue;
328
+ }
329
+ store.commit(() => {
330
+ const current = readRow(row.envelope.key);
331
+ if (current?.state !== "pending" || current.jobId !== row.jobId)
332
+ return;
333
+ if (current.attempts >= maxDeliveryAttempts)
334
+ finish(current.envelope.key, current, {
335
+ state: "dead",
336
+ reason: "exhausted",
337
+ });
338
+ else
339
+ enqueueJob(current);
340
+ });
341
+ }
342
+ return rows.length === pageSize ? rows.at(-1)?.key : undefined;
343
+ };
344
+ const retry = async (input) => {
345
+ const key = Schema.decodeUnknownSync(Key)(input);
346
+ const row = readRow(key);
347
+ if (row === undefined || row.state === "delivered")
348
+ return;
349
+ const work = row.state === "pending" && row.jobId !== undefined
350
+ ? await options.readJob(row.jobId)
351
+ : undefined;
352
+ if (work?.status === "missing")
353
+ throw new PostError("delivery job is missing");
354
+ if (work?.status === "found" && work.job.state !== "settled")
355
+ return;
356
+ store.commit(() => {
357
+ const current = readRow(key);
358
+ if (current === undefined || current.state === "delivered")
359
+ return;
360
+ if (current.state === "pending") {
361
+ if (row.state !== "pending" || current.jobId !== row.jobId)
362
+ return;
363
+ const pending = {
364
+ ...current,
365
+ attempts: 0,
366
+ nextAttemptAt: options.now(),
367
+ };
368
+ putRow(key, pending);
369
+ if (current.jobId !== undefined)
370
+ enqueueJob(pending);
371
+ }
372
+ else {
373
+ const pending = {
374
+ state: "pending",
375
+ envelope: current.envelope,
376
+ attempts: 0,
377
+ nextAttemptAt: options.now(),
378
+ delivery: current.delivery,
379
+ };
380
+ putRow(key, pending);
381
+ const queue = queueOf(pending.envelope);
382
+ const previousKey = store.read(tailKey(queue));
383
+ const previous = previousKey === undefined || previousKey === key
384
+ ? undefined
385
+ : readRow(previousKey);
386
+ if (previous?.state === "pending" && previousKey !== undefined)
387
+ putRow(previousKey, { ...previous, nextKey: key });
388
+ else
389
+ enqueueJob(pending);
390
+ store.put(tailKey(queue), key);
391
+ }
392
+ });
393
+ await options.scheduleWake(options.now());
394
+ };
395
+ const deadLetters = (limit = 64, afterKey) => {
396
+ const pageSize = Schema.decodeUnknownSync(PageLimit)(limit);
397
+ const rows = store.list(pageSize, afterKey);
398
+ const letters = rows.flatMap(({ key, value }) => {
399
+ const row = Schema.decodeUnknownSync(PostRowSchema)(value);
400
+ return row.state === "dead"
401
+ ? [
402
+ {
403
+ key,
404
+ reason: row.reason,
405
+ envelope: row.envelope,
406
+ attempts: row.attempts,
407
+ },
408
+ ]
409
+ : [];
410
+ });
411
+ return {
412
+ letters,
413
+ nextCursor: rows.length === pageSize ? rows.at(-1)?.key : undefined,
414
+ };
415
+ };
416
+ return Object.freeze({
417
+ send,
418
+ latest,
419
+ retry,
420
+ execute,
421
+ enqueueDue,
422
+ deadLetters,
423
+ read: readRow,
424
+ nextWake,
425
+ });
426
+ }
427
+ /** Consumer admission runs inside the receiver's own SQLite transaction. */
428
+ export function admitPost(options) {
429
+ const key = Schema.decodeUnknownSync(Key)(options.key);
430
+ const event = Schema.decodeUnknownSync(options.schema)(options.event);
431
+ if (options.hasReceipt(key))
432
+ return "duplicate";
433
+ const result = options.apply(event);
434
+ if (result === "accepted")
435
+ options.putReceipt(key);
436
+ return result;
437
+ }
@@ -0,0 +1,16 @@
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;
@@ -0,0 +1,20 @@
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
+ }
@@ -0,0 +1,20 @@
1
+ <!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
2
+
3
+ **@fungi.computer/carrier**
4
+
5
+ ---
6
+
7
+ # @fungi.computer/carrier
8
+
9
+ ## Modules
10
+
11
+ | Module | Description |
12
+ | --------------------------------- | ----------- |
13
+ | [cloudflare](cloudflare.md) | - |
14
+ | [email](email.md) | - |
15
+ | [index](index.md) | - |
16
+ | [inspection](inspection.md) | - |
17
+ | [post-d1](post-d1.md) | - |
18
+ | [post-postgres](post-postgres.md) | - |
19
+ | [post](post.md) | - |
20
+ | [testing](testing.md) | - |
@@ -0,0 +1,100 @@
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) / cloudflare
8
+
9
+ # cloudflare
10
+
11
+ ## Type Aliases
12
+
13
+ ### CloudflareCarrierStorage
14
+
15
+ ```ts
16
+ type CloudflareCarrierStorage = WatchdogSqliteOwner &
17
+ Pick<
18
+ DurableObjectStorage,
19
+ "transaction" | "getAlarm" | "setAlarm" | "deleteAlarm"
20
+ >;
21
+ ```
22
+
23
+ Native SQLite and alarms; the Cloudflare adapter owns their atomic composition.
24
+
25
+ ---
26
+
27
+ ### CloudflareCarrierOptions
28
+
29
+ ```ts
30
+ type CloudflareCarrierOptions = Readonly<{
31
+ storage: CloudflareCarrierStorage;
32
+ canSend?: (emailId) => Promise<boolean>;
33
+ }> &
34
+ | Readonly<{
35
+ email: SendEmail;
36
+ transport?: never;
37
+ }>
38
+ | Readonly<{
39
+ transport: Options["transport"];
40
+ email?: never;
41
+ }>;
42
+ ```
43
+
44
+ Native storage/wake composition with exactly one provider capability.
45
+
46
+ ---
47
+
48
+ ### CloudflareCarrier
49
+
50
+ ```ts
51
+ type CloudflareCarrier = Pick<Carrier, "send" | "get"> &
52
+ Readonly<{
53
+ alarm: () => Promise<void>;
54
+ }>;
55
+ ```
56
+
57
+ Ordinary submission operations and the native Durable Object alarm door.
58
+
59
+ ## Functions
60
+
61
+ ### createCloudflareCarrier()
62
+
63
+ ```ts
64
+ function createCloudflareCarrier(options): Promise<CloudflareCarrier>;
65
+ ```
66
+
67
+ Construct one owner per Durable Object. Forward its native alarm to `alarm`.
68
+ Acceptance commits the Email, recipient Messages, Watchdog job and wake
69
+ together.
70
+
71
+ #### Parameters
72
+
73
+ | Parameter | Type |
74
+ | --------- | ------------------------------------------------------- |
75
+ | `options` | [`CloudflareCarrierOptions`](#cloudflarecarrieroptions) |
76
+
77
+ #### Returns
78
+
79
+ `Promise`\<[`CloudflareCarrier`](#cloudflarecarrier)\>
80
+
81
+ ---
82
+
83
+ ### cloudflareEmailTransport()
84
+
85
+ ```ts
86
+ function cloudflareEmailTransport(binding): (email) => Promise<SendEvidence>;
87
+ ```
88
+
89
+ The native binding as a Carrier transport. Exported so a host that holds the
90
+ binding elsewhere (the Hub's Cloudflare edge) classifies sends identically.
91
+
92
+ #### Parameters
93
+
94
+ | Parameter | Type |
95
+ | --------- | ----------- |
96
+ | `binding` | `SendEmail` |
97
+
98
+ #### Returns
99
+
100
+ (`email`) => `Promise`\<[`SendEvidence`](index.md#sendevidence)\>
@@ -0,0 +1,87 @@
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) / email
8
+
9
+ # email
10
+
11
+ ## References
12
+
13
+ ### CarrierError
14
+
15
+ Re-exports [CarrierError](index.md#carriererror)
16
+
17
+ ---
18
+
19
+ ### CarrierStatusSchema
20
+
21
+ Re-exports [CarrierStatusSchema](index.md#carrierstatusschema)
22
+
23
+ ---
24
+
25
+ ### Address
26
+
27
+ Re-exports [Address](index.md#address)
28
+
29
+ ---
30
+
31
+ ### Email
32
+
33
+ Re-exports [Email](index.md#email)
34
+
35
+ ---
36
+
37
+ ### Host
38
+
39
+ Re-exports [Host](index.md#host)
40
+
41
+ ---
42
+
43
+ ### Message
44
+
45
+ Re-exports [Message](index.md#message)
46
+
47
+ ---
48
+
49
+ ### Options
50
+
51
+ Re-exports [Options](index.md#options)
52
+
53
+ ---
54
+
55
+ ### ProviderRejection
56
+
57
+ Re-exports [ProviderRejection](index.md#providerrejection)
58
+
59
+ ---
60
+
61
+ ### Receipt
62
+
63
+ Re-exports [Receipt](index.md#receipt)
64
+
65
+ ---
66
+
67
+ ### SendEvidence
68
+
69
+ Re-exports [SendEvidence](index.md#sendevidence)
70
+
71
+ ---
72
+
73
+ ### Submission
74
+
75
+ Re-exports [Submission](index.md#submission)
76
+
77
+ ---
78
+
79
+ ### Carrier
80
+
81
+ Re-exports [Carrier](index.md#carrier)
82
+
83
+ ---
84
+
85
+ ### createCarrier
86
+
87
+ Re-exports [createCarrier](index.md#createcarrier)