@shivaedev/email 0.1.2 → 0.3.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.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/server/defineEmailQueue.d.ts +2 -1
- package/dist/server/defineEmailQueue.d.ts.map +1 -1
- package/package.json +8 -13
- package/dist/server/testing/adapters/inbox.d.ts +0 -25
- package/dist/server/testing/adapters/inbox.d.ts.map +0 -1
- package/dist/server/testing/adapters/inbox.js +0 -39
- package/dist/server/testing/adapters/inbox.js.map +0 -1
- package/src/server/testing/adapters/inbox.ts +0 -44
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 - 2026-11-03
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- Move `inbox`, `emailLink` and `ReceivedEmail` from `@shivaedev/email/server/testing/adapters/inbox.ts` to `@shivaedev/test-kits/email/adapters/inbox.ts`. Add test-kits as a development dependency. Email no longer has a test-story peer dependency.
|
|
8
|
+
|
|
9
|
+
## 0.2.0 - 2026-11-03
|
|
10
|
+
|
|
11
|
+
### Breaking
|
|
12
|
+
|
|
13
|
+
- Queue definitions returned by `defineEmailQueue` expose jobs-owned queue and worker options. Import `QueueOptions` from `@shivaedev/jobs/server/queueOptions.ts` and `WorkerOptions` from `@shivaedev/jobs/server/workerOptions.ts` for explicit annotations.
|
|
14
|
+
|
|
3
15
|
## 0.1.2 - 2026-11-02
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ The email worker sends its job id as Resend's idempotency key on every attempt.
|
|
|
56
56
|
|
|
57
57
|
### Read email in stories
|
|
58
58
|
|
|
59
|
-
Install `@shivaedev/test-
|
|
59
|
+
Install `@shivaedev/test-kits` as a development dependency. In a scoped story setup, acquire `inbox()` from `@shivaedev/test-kits/email/adapters/inbox.ts` and provide its `mailerLayer` to the real worker. It uses the Resend SDK against a loopback fake HTTP endpoint.
|
|
60
60
|
|
|
61
61
|
`nextEmail` waits for the next accepted message, including its `idempotencyKey`; `emails()` reads accepted messages so far. `emailLink(email)` reads the first HTTP link in the message's text. A story can follow that link through the application's HTTP or React boundary. The inbox's `endpoint` exposes the native fake HTTP controls for scripting provider failures.
|
|
62
62
|
|
|
@@ -11,7 +11,8 @@ declare function makeEmailQueue<TSchema extends JobPayloadSchema, E, R, const TN
|
|
|
11
11
|
Jobs: import("@shivaedev/jobs/server/service.ts").PgBossDefinition<`@shivaedev/email/${TName}`>;
|
|
12
12
|
producerLayer: import("effect/Layer").Layer<`@shivaedev/email/${TName}`, import("@shivaedev/jobs/server/error.ts").PgBossError | import("@shivaedev/jobs/server/error.ts").PgBossQueueNamesError | import("@shivaedev/jobs/server/error.ts").PgBossQueueOptionsError, import("@shivaedev/jobs/server/PgBoss.ts").PgBoss>;
|
|
13
13
|
queue: import("@shivaedev/jobs/server/definition.ts").QueueDefinition<TName, TSchema> & {
|
|
14
|
-
readonly
|
|
14
|
+
readonly id?: undefined;
|
|
15
|
+
readonly key?: undefined;
|
|
15
16
|
};
|
|
16
17
|
worker: import("@shivaedev/jobs/server/definition.ts").QueueWorker<TSchema, "@shivaedev/email/Mailer" | R>;
|
|
17
18
|
workerLayer: import("effect/Layer").Layer<`@shivaedev/email/${TName}`, import("@shivaedev/jobs/server/error.ts").PgBossError | import("@shivaedev/jobs/server/error.ts").PgBossQueueNamesError | import("@shivaedev/jobs/server/error.ts").PgBossQueueOptionsError, import("@shivaedev/jobs/server/PgBoss.ts").PgBoss | Exclude<"@shivaedev/email/Mailer", `@shivaedev/email/${TName}`> | Exclude<R, `@shivaedev/email/${TName}`> | Exclude<TSchema["DecodingServices"], `@shivaedev/email/${TName}`>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defineEmailQueue.d.ts","sourceRoot":"","sources":["../../src/server/defineEmailQueue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAE1F,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAGvC,KAAK,MAAM,CAAC,OAAO,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAE3H,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,OAAO,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,EAC5E,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,GAC3B,UAAU,CAAC,OAAO,cAAc,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,qBAAqB,CAAC,CAAC,CAAC;AAC3E,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,OAAO,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,SAAS,MAAM,EACxG,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,EAC7B,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GAC/B,UAAU,CAAC,OAAO,cAAc,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AAS3D,iBAAS,cAAc,CAAC,OAAO,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,SAAS,MAAM,EACzF,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,EAC7B,IAAI,EAAE,KAAK;uBAgBS,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC
|
|
1
|
+
{"version":3,"file":"defineEmailQueue.d.ts","sourceRoot":"","sources":["../../src/server/defineEmailQueue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAE1F,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAGvC,KAAK,MAAM,CAAC,OAAO,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAE3H,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,OAAO,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,EAC5E,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,GAC3B,UAAU,CAAC,OAAO,cAAc,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,qBAAqB,CAAC,CAAC,CAAC;AAC3E,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,OAAO,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,SAAS,MAAM,EACxG,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,EAC7B,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GAC/B,UAAU,CAAC,OAAO,cAAc,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AAS3D,iBAAS,cAAc,CAAC,OAAO,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,SAAS,MAAM,EACzF,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,EAC7B,IAAI,EAAE,KAAK;uBAgBS,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;;;;;;;;;EAO/C"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/email",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Transactional email with Resend
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Transactional email with Resend and durable Effect jobs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
7
7
|
],
|
|
@@ -45,30 +45,25 @@
|
|
|
45
45
|
"CHANGELOG.md"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@shivaedev/http": "0.
|
|
49
|
-
"@shivaedev/jobs": "0.
|
|
48
|
+
"@shivaedev/http": "0.5.0",
|
|
49
|
+
"@shivaedev/jobs": "0.32.0",
|
|
50
50
|
"@shivaedev/service": "0.6.1",
|
|
51
51
|
"resend": "6.34.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@effect/platform-node": "4.0.6",
|
|
55
|
-
"@shivaedev/quality": "0.
|
|
56
|
-
"@shivaedev/test-
|
|
57
|
-
"@shivaedev/test-
|
|
55
|
+
"@shivaedev/quality": "0.57.1",
|
|
56
|
+
"@shivaedev/test-kits": "0.1.0",
|
|
57
|
+
"@shivaedev/test-sql": "0.18.0",
|
|
58
|
+
"@shivaedev/test-story": "0.36.0",
|
|
58
59
|
"@types/node": "24.19.2",
|
|
59
60
|
"effect": "4.0.6",
|
|
60
61
|
"typescript": "7.0.2",
|
|
61
62
|
"vitest": "5.0.5"
|
|
62
63
|
},
|
|
63
64
|
"peerDependencies": {
|
|
64
|
-
"@shivaedev/test-story": "0.34.0",
|
|
65
65
|
"effect": "4.0.6"
|
|
66
66
|
},
|
|
67
|
-
"peerDependenciesMeta": {
|
|
68
|
-
"@shivaedev/test-story": {
|
|
69
|
-
"optional": true
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
67
|
"engines": {
|
|
73
68
|
"node": ">=24"
|
|
74
69
|
},
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { Email } from "#Email.ts";
|
|
3
|
-
export interface ReceivedEmail extends Email {
|
|
4
|
-
readonly idempotencyKey: string | undefined;
|
|
5
|
-
}
|
|
6
|
-
export declare const inbox: () => Effect.Effect<{
|
|
7
|
-
emails: () => Effect.Effect<ReceivedEmail[], never, never>;
|
|
8
|
-
endpoint: {
|
|
9
|
-
completed: import("effect/Stream").Stream<import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpRequestRecord, never, never>;
|
|
10
|
-
fault: (route: string, fault: import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpFault, scope?: import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpScope) => Effect.Effect<void, never, never>;
|
|
11
|
-
hold: (route: string, holdOptions?: import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpHoldOptions) => Effect.Effect<import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpHold, never, never>;
|
|
12
|
-
narrate: (sink: (line: string) => void) => void;
|
|
13
|
-
received: import("effect/Stream").Stream<import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpRequestRecord, never, never>;
|
|
14
|
-
releaseOnArrival: (route: string, arrival: string, scope?: import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpScope) => Effect.Effect<void, never, never>;
|
|
15
|
-
readonly requests: readonly import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpRequestRecord[];
|
|
16
|
-
scriptFault: (route: string, fault: import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpFault, scriptOptions?: import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpScriptOptions) => Effect.Effect<import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpScript, never, never>;
|
|
17
|
-
stall: (route: string, scope?: import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpScope) => Effect.Effect<import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpHold, never, never>;
|
|
18
|
-
untilAbandoned: (matches: (request: import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpRequestRecord) => boolean, wait: import("@shivaedev/test-story/server/until.ts").UntilOptions) => Effect.Effect<import("@shivaedev/test-story/server/fakeHttpService.ts").FakeHttpRequestRecord, import("@shivaedev/test-story/server/until.ts").SignalNotReceived, never>;
|
|
19
|
-
url: string;
|
|
20
|
-
};
|
|
21
|
-
mailerLayer: import("effect/Layer").Layer<"@shivaedev/email/Mailer", import("effect/Config").ConfigError, never>;
|
|
22
|
-
nextEmail: Effect.Effect<ReceivedEmail, import("effect/Cause").TimeoutError, never>;
|
|
23
|
-
}, import("effect/Cause").UnknownError, import("effect/Scope").Scope>;
|
|
24
|
-
export declare function emailLink(email: Email): string;
|
|
25
|
-
//# sourceMappingURL=inbox.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../../../src/server/testing/adapters/inbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA2B,MAAM,QAAQ,CAAC;AAGzD,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAGlC,MAAM,WAAW,aAAc,SAAQ,KAAK;IAC3C,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5C;AAED,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;qEAyBhB,CAAC;AAEH,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAM9C"}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Effect, Queue, Redacted, Schema } from "effect";
|
|
2
|
-
import { fakeHttpService } from "@shivaedev/test-story/server/fakeHttpService.ts";
|
|
3
|
-
import { routeOutbound } from "@shivaedev/test-story/server/routeOutbound.ts";
|
|
4
|
-
import { Email } from "#Email.ts";
|
|
5
|
-
import { resendMailerLayer } from "#server/adapters/resendMailerLayer.ts";
|
|
6
|
-
export const inbox = Effect.fnUntraced(function* () {
|
|
7
|
-
const arrivals = yield* Queue.unbounded();
|
|
8
|
-
const emails = [];
|
|
9
|
-
const decode = Schema.decodeUnknownSync(Schema.fromJsonString(Email));
|
|
10
|
-
const endpoint = yield* fakeHttpService({
|
|
11
|
-
routes: {
|
|
12
|
-
send: {
|
|
13
|
-
method: "POST",
|
|
14
|
-
path: "/emails",
|
|
15
|
-
reply: (request) => {
|
|
16
|
-
const email = { ...decode(request.body.toString()), idempotencyKey: request.headers["idempotency-key"]?.toString() };
|
|
17
|
-
emails.push(email);
|
|
18
|
-
Queue.offerUnsafe(arrivals, email);
|
|
19
|
-
return { body: { id: `email-${emails.length}` } };
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
yield* routeOutbound({ "api.resend.com": endpoint.url });
|
|
25
|
-
return {
|
|
26
|
-
emails: () => Effect.sync(() => [...emails]),
|
|
27
|
-
endpoint,
|
|
28
|
-
mailerLayer: resendMailerLayer({ apiKey: Redacted.make("re_story") }),
|
|
29
|
-
nextEmail: Queue.take(arrivals).pipe(Effect.timeout("10 seconds")),
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
export function emailLink(email) {
|
|
33
|
-
const link = email.text.match(/https?:\/\/[^\s<>]+/u)?.[0];
|
|
34
|
-
if (link === undefined) {
|
|
35
|
-
throw new Error("The delivered email has no link in its text");
|
|
36
|
-
}
|
|
37
|
-
return link;
|
|
38
|
-
}
|
|
39
|
-
//# sourceMappingURL=inbox.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../../../src/server/testing/adapters/inbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AAClF,OAAO,EAAE,aAAa,EAAE,MAAM,+CAA+C,CAAC;AAC9E,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAM1E,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC;IAC/C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,SAAS,EAAiB,CAAC;IACzD,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,MAAM,MAAM,GAAG,MAAM,CAAC,iBAAiB,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC;QACvC,MAAM,EAAE;YACP,IAAI,EAAE;gBACL,MAAM,EAAE,MAAM;gBACd,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE;oBAClB,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC;oBACrH,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBACnB,KAAK,CAAC,WAAW,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;oBACnC,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC;gBACnD,CAAC;aACD;SACD;KACD,CAAC,CAAC;IACH,KAAK,CAAC,CAAC,aAAa,CAAC,EAAE,gBAAgB,EAAE,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;IACzD,OAAO;QACN,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;QAC5C,QAAQ;QACR,WAAW,EAAE,iBAAiB,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACrE,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;KAClE,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,MAAM,UAAU,SAAS,CAAC,KAAY;IACrC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,sBAAsB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3D,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC"}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Effect, Queue, Redacted, Schema } from "effect";
|
|
2
|
-
import { fakeHttpService } from "@shivaedev/test-story/server/fakeHttpService.ts";
|
|
3
|
-
import { routeOutbound } from "@shivaedev/test-story/server/routeOutbound.ts";
|
|
4
|
-
import { Email } from "#Email.ts";
|
|
5
|
-
import { resendMailerLayer } from "#server/adapters/resendMailerLayer.ts";
|
|
6
|
-
|
|
7
|
-
export interface ReceivedEmail extends Email {
|
|
8
|
-
readonly idempotencyKey: string | undefined;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const inbox = Effect.fnUntraced(function* () {
|
|
12
|
-
const arrivals = yield* Queue.unbounded<ReceivedEmail>();
|
|
13
|
-
const emails: ReceivedEmail[] = [];
|
|
14
|
-
const decode = Schema.decodeUnknownSync(Schema.fromJsonString(Email));
|
|
15
|
-
const endpoint = yield* fakeHttpService({
|
|
16
|
-
routes: {
|
|
17
|
-
send: {
|
|
18
|
-
method: "POST",
|
|
19
|
-
path: "/emails",
|
|
20
|
-
reply: (request) => {
|
|
21
|
-
const email = { ...decode(request.body.toString()), idempotencyKey: request.headers["idempotency-key"]?.toString() };
|
|
22
|
-
emails.push(email);
|
|
23
|
-
Queue.offerUnsafe(arrivals, email);
|
|
24
|
-
return { body: { id: `email-${emails.length}` } };
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
yield* routeOutbound({ "api.resend.com": endpoint.url });
|
|
30
|
-
return {
|
|
31
|
-
emails: () => Effect.sync(() => [...emails]),
|
|
32
|
-
endpoint,
|
|
33
|
-
mailerLayer: resendMailerLayer({ apiKey: Redacted.make("re_story") }),
|
|
34
|
-
nextEmail: Queue.take(arrivals).pipe(Effect.timeout("10 seconds")),
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
export function emailLink(email: Email): string {
|
|
39
|
-
const link = email.text.match(/https?:\/\/[^\s<>]+/u)?.[0];
|
|
40
|
-
if (link === undefined) {
|
|
41
|
-
throw new Error("The delivered email has no link in its text");
|
|
42
|
-
}
|
|
43
|
-
return link;
|
|
44
|
-
}
|