@vllnt/convex-email 0.1.0-canary.63aca6b

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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/dist/client/index.d.ts +174 -0
  4. package/dist/client/index.d.ts.map +1 -0
  5. package/dist/client/index.js +151 -0
  6. package/dist/client/index.js.map +1 -0
  7. package/dist/client/types.d.ts +84 -0
  8. package/dist/client/types.d.ts.map +1 -0
  9. package/dist/client/types.js +3 -0
  10. package/dist/client/types.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +40 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +110 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +121 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +78 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/convex.config.d.ts +3 -0
  28. package/dist/component/convex.config.d.ts.map +1 -0
  29. package/dist/component/convex.config.js +7 -0
  30. package/dist/component/convex.config.js.map +1 -0
  31. package/dist/component/crons.d.ts +16 -0
  32. package/dist/component/crons.d.ts.map +1 -0
  33. package/dist/component/crons.js +19 -0
  34. package/dist/component/crons.js.map +1 -0
  35. package/dist/component/mutations.d.ts +95 -0
  36. package/dist/component/mutations.d.ts.map +1 -0
  37. package/dist/component/mutations.js +243 -0
  38. package/dist/component/mutations.js.map +1 -0
  39. package/dist/component/queries.d.ts +59 -0
  40. package/dist/component/queries.d.ts.map +1 -0
  41. package/dist/component/queries.js +61 -0
  42. package/dist/component/queries.js.map +1 -0
  43. package/dist/component/schema.d.ts +54 -0
  44. package/dist/component/schema.d.ts.map +1 -0
  45. package/dist/component/schema.js +40 -0
  46. package/dist/component/schema.js.map +1 -0
  47. package/dist/component/validators.d.ts +54 -0
  48. package/dist/component/validators.d.ts.map +1 -0
  49. package/dist/component/validators.js +40 -0
  50. package/dist/component/validators.js.map +1 -0
  51. package/dist/jmap/index.d.ts +22 -0
  52. package/dist/jmap/index.d.ts.map +1 -0
  53. package/dist/jmap/index.js +21 -0
  54. package/dist/jmap/index.js.map +1 -0
  55. package/dist/jmap/send.d.ts +125 -0
  56. package/dist/jmap/send.d.ts.map +1 -0
  57. package/dist/jmap/send.js +418 -0
  58. package/dist/jmap/send.js.map +1 -0
  59. package/dist/jmap/types.d.ts +107 -0
  60. package/dist/jmap/types.d.ts.map +1 -0
  61. package/dist/jmap/types.js +16 -0
  62. package/dist/jmap/types.js.map +1 -0
  63. package/dist/shared.d.ts +32 -0
  64. package/dist/shared.d.ts.map +1 -0
  65. package/dist/shared.js +33 -0
  66. package/dist/shared.js.map +1 -0
  67. package/dist/smtp/index.d.ts +22 -0
  68. package/dist/smtp/index.d.ts.map +1 -0
  69. package/dist/smtp/index.js +21 -0
  70. package/dist/smtp/index.js.map +1 -0
  71. package/dist/smtp/send.d.ts +51 -0
  72. package/dist/smtp/send.d.ts.map +1 -0
  73. package/dist/smtp/send.js +124 -0
  74. package/dist/smtp/send.js.map +1 -0
  75. package/dist/smtp/transport.d.ts +43 -0
  76. package/dist/smtp/transport.d.ts.map +1 -0
  77. package/dist/smtp/transport.js +55 -0
  78. package/dist/smtp/transport.js.map +1 -0
  79. package/dist/smtp/types.d.ts +122 -0
  80. package/dist/smtp/types.d.ts.map +1 -0
  81. package/dist/smtp/types.js +9 -0
  82. package/dist/smtp/types.js.map +1 -0
  83. package/package.json +118 -0
  84. package/src/client/index.ts +312 -0
  85. package/src/client/types.ts +90 -0
  86. package/src/component/_generated/api.ts +56 -0
  87. package/src/component/_generated/component.ts +134 -0
  88. package/src/component/_generated/dataModel.ts +60 -0
  89. package/src/component/_generated/server.ts +156 -0
  90. package/src/component/convex.config.ts +9 -0
  91. package/src/component/crons.ts +23 -0
  92. package/src/component/mutations.ts +262 -0
  93. package/src/component/queries.ts +70 -0
  94. package/src/component/schema.ts +40 -0
  95. package/src/component/validators.ts +47 -0
  96. package/src/jmap/index.ts +39 -0
  97. package/src/jmap/send.test.ts +565 -0
  98. package/src/jmap/send.ts +502 -0
  99. package/src/jmap/types.ts +117 -0
  100. package/src/shared.ts +41 -0
  101. package/src/smtp/index.ts +30 -0
  102. package/src/smtp/send.test.ts +240 -0
  103. package/src/smtp/send.ts +154 -0
  104. package/src/smtp/transport.ts +58 -0
  105. package/src/smtp/types.ts +124 -0
  106. package/src/test.ts +12 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bntvllnt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ <!-- Badges -->
2
+ [![convex-component](https://img.shields.io/badge/convex-component-EE342F.svg)](https://www.convex.dev/components)
3
+ [![npm](https://img.shields.io/npm/v/@vllnt/convex-email.svg)](https://www.npmjs.com/package/@vllnt/convex-email)
4
+ [![CI](https://github.com/vllnt/convex-email/actions/workflows/ci.yml/badge.svg)](https://github.com/vllnt/convex-email/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/@vllnt/convex-email.svg)](./LICENSE)
6
+
7
+ # @vllnt/convex-email
8
+
9
+ A durable, transport-agnostic outbound transactional email queue, as a Convex component — the host enqueues a message, its own transport sends it, and clients poll the delivery status.
10
+
11
+ ```ts
12
+ const email = new Email(components.email);
13
+ await email.enqueue(ctx, messageId, to, from, "smtp", { idempotencyKey });
14
+ await email.markSending(ctx, messageId); // host sends, then:
15
+ await email.markSent(ctx, messageId, { providerId }); // or markFailed → auto-retry
16
+ ```
17
+
18
+ ## Features
19
+
20
+ - **Enqueue-and-return** — insert a `queued` message, get its id back; the caller never blocks on the send.
21
+ - **Host-driven transport** — the host claims (`markSending`), sends, and reports (`markSent`/`markFailed`); the component never reaches a provider.
22
+ - **Optional generic SMTP adapter** — `@vllnt/convex-email/smtp` sends through any SMTP server; `nodemailer` is an optional peer dep.
23
+ - **Optional generic JMAP adapter** — `@vllnt/convex-email/jmap` sends over any JMAP server's HTTP API (Stalwart, Fastmail, Cyrus) from a plain action — `fetch`-based, no `"use node"`, no extra dep.
24
+ - **Idempotent enqueue** — an `idempotencyKey` dedups a re-enqueue so a retry can't double-send.
25
+ - **Retry with budget** — `markFailed` re-queues until `attempts` hit `maxAttempts`, then lands in terminal `failed`.
26
+ - **Terminal states are final** — a late or duplicate delivery callback can never overwrite a recorded outcome.
27
+ - **Poll or subscribe** — `get` / `listByStatus` update live in a reactive Convex query.
28
+ - **Server-sourced time, typed opaque payload, bounded prune cron, mount-safe.**
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pnpm add @vllnt/convex-email
34
+ ```
35
+
36
+ Peer dependency: `convex@^1.41.0`. The queue core has zero third-party runtime deps. `nodemailer@^8.0.4` is an optional peer dep — only for the SMTP transport. The JMAP transport (`@vllnt/convex-email/jmap`) is `fetch`-based and needs no extra dependency.
37
+
38
+ ## Usage
39
+
40
+ ```ts
41
+ // convex/convex.config.ts
42
+ import { defineApp } from "convex/server";
43
+ import email from "@vllnt/convex-email/convex.config";
44
+
45
+ const app = defineApp();
46
+ app.use(email);
47
+ export default app;
48
+ ```
49
+
50
+ ```ts
51
+ // convex/notify.ts — host owns auth AND the transport; pass opaque refs in.
52
+ import { components } from "./_generated/api";
53
+ import { mutation, internalAction } from "./_generated/server";
54
+ import { v } from "convex/values";
55
+ import { Email } from "@vllnt/convex-email";
56
+
57
+ const email = new Email<{ subject: string; html: string }>(components.email, {
58
+ payloadValidator: v.object({ subject: v.string(), html: v.string() }).parse,
59
+ });
60
+
61
+ // 1) Enqueue (records intent only) and schedule the send.
62
+ export const sendWelcome = mutation({
63
+ args: { userId: v.string(), to: v.string() },
64
+ handler: async (ctx, { userId, to }) => {
65
+ const messageId = crypto.randomUUID();
66
+ await email.enqueue(ctx, messageId, to, "no-reply@app.com", "jmap", {
67
+ payload: { subject: "Welcome", html: "<p>Hello</p>" },
68
+ idempotencyKey: `welcome:${userId}`, // a retry never double-sends
69
+ });
70
+ await ctx.scheduler.runAfter(0, internal.notify.flush, { messageId });
71
+ return { messageId };
72
+ },
73
+ });
74
+
75
+ // 2) The host's transport sender claims, dispatches, and reports the outcome.
76
+ export const flush = internalAction({
77
+ args: { messageId: v.string() },
78
+ handler: async (ctx, { messageId }) => {
79
+ await email.markSending(ctx, messageId);
80
+ try {
81
+ const providerId = await sendOverJmap(messageId); // your transport
82
+ await email.markSent(ctx, messageId, { providerId });
83
+ } catch (e) {
84
+ const { retried } = await email.markFailed(ctx, messageId, { error: String(e) });
85
+ if (retried) await ctx.scheduler.runAfter(backoffMs(), internal.notify.flush, { messageId });
86
+ }
87
+ },
88
+ });
89
+ ```
90
+
91
+ ## API Reference
92
+
93
+ | Method | Kind | Result |
94
+ |--------|------|--------|
95
+ | `enqueue(ctx, messageId, to, from, transport, opts?)` | mutation | `{ messageId, deduplicated }` |
96
+ | `markSending(ctx, messageId)` | mutation | `{ attempts }` |
97
+ | `markSent(ctx, messageId, opts?)` | mutation | `null` |
98
+ | `markFailed(ctx, messageId, opts?)` | mutation | `{ status, retried }` |
99
+ | `get(ctx, messageId)` | query | `MessageView \| null` |
100
+ | `listByStatus(ctx, status, paginationOpts)` | query | `PaginationResult<MessageView>` |
101
+ | `prune(ctx, opts?)` | mutation | `number` |
102
+
103
+ Full reference: [docs/API.md](docs/API.md).
104
+
105
+ ## SMTP transport (optional)
106
+
107
+ A generic SMTP adapter ships at `@vllnt/convex-email/smtp` — it sends through any SMTP server (Stalwart, Postfix, any relay). The real send runs in the host's own `"use node"` action; the component never sends.
108
+
109
+ ```ts
110
+ import { createSmtpSender } from "@vllnt/convex-email/smtp";
111
+
112
+ const send = createSmtpSender({
113
+ host: process.env.SMTP_HOST!,
114
+ port: 465,
115
+ secure: true,
116
+ auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },
117
+ });
118
+ ```
119
+
120
+ Full queue-to-SMTP wiring in [docs/API.md](docs/API.md).
121
+
122
+ ## JMAP transport (optional)
123
+
124
+ A generic JMAP adapter ships at `@vllnt/convex-email/jmap` — it sends over any JMAP server's HTTP API (Stalwart, Fastmail, Cyrus, Apache James). JMAP is HTTP, so it runs in a **plain Convex action** — no `"use node"`, no `nodemailer`, zero extra deps. It's a protocol, not a vendor: `./jmap`, never `./stalwart`.
125
+
126
+ ```ts
127
+ import { createJmapSender, discoverJmapSession } from "@vllnt/convex-email/jmap";
128
+
129
+ // Resolve account / identity / Sent mailbox once, then bind a sender over your `fetch`.
130
+ const config = await discoverJmapSession((u, i) => fetch(u, i), {
131
+ sessionUrl: "https://mail.example.com/.well-known/jmap", // e.g. a Stalwart host
132
+ token: process.env.JMAP_TOKEN!,
133
+ from: "no-reply@app.com",
134
+ });
135
+ const send = createJmapSender(config, (u, i) => fetch(u, i));
136
+ ```
137
+
138
+ To send some mail over SMTP and some over Stalwart's HTTP (JMAP), keep **one queue** and route per message by its stored `transport` tag (a `senders` map keyed by `"smtp"` / `"jmap"`). Full JMAP wiring + routing in [docs/API.md](docs/API.md).
139
+
140
+ ## Security
141
+
142
+ - Auth-agnostic and provider-neutral — the host gates access and drives the transport; the component never authenticates or contacts a provider.
143
+ - Tables are sandboxed (reached only via exported functions); the stored `payload` is opaque.
144
+ - Terminal states are final, enqueue is idempotent, and time is server-sourced.
145
+
146
+ See [docs/API.md](docs/API.md).
147
+
148
+ ## Testing
149
+
150
+ ```bash
151
+ pnpm test # single run
152
+ pnpm test:coverage # enforced 100% on covered files
153
+ ```
154
+
155
+ Tests run against the real component runtime via `convex-test` (`@edge-runtime/vm`), not mocks.
156
+
157
+ ## Contributing
158
+
159
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
160
+
161
+ ## Author
162
+
163
+ Built by [bntvllnt](https://github.com/bntvllnt) · [bntvllnt.com](https://bntvllnt.com) · [X @bntvllnt](https://x.com/bntvllnt)
164
+
165
+ Part of the [@vllnt](https://github.com/vllnt) Convex component fleet — [vllnt.com](https://vllnt.com)
166
+
167
+ If this is useful, [sponsor the work](https://github.com/sponsors/bntvllnt).
168
+
169
+ ## License
170
+
171
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,174 @@
1
+ import type { FunctionArgs, FunctionReference, FunctionReturnType, PaginationOptions, PaginationResult } from "convex/server";
2
+ import type { EmailOptions, EnqueueOptions, EnqueueResult, MarkFailedResult, MessageStatus, MessageView, Parser } from "./types.js";
3
+ /**
4
+ * The component's raw message view, before the client narrows the opaque host
5
+ * payload. `payload` is `unknown` here; the {@link Email} client runs the host
6
+ * validator over it at its typed boundary.
7
+ */
8
+ type RawView = {
9
+ messageId: string;
10
+ to: string;
11
+ from: string;
12
+ transport: string;
13
+ status: MessageStatus;
14
+ payload?: unknown;
15
+ subjectRef?: string;
16
+ idempotencyKey?: string;
17
+ providerId?: string;
18
+ attempts: number;
19
+ maxAttempts: number;
20
+ error?: string;
21
+ createdAt: number;
22
+ updatedAt: number;
23
+ };
24
+ /**
25
+ * The email component's function references, as exposed on the host via
26
+ * `components.email`. The host's stored `payload` is opaque here (`unknown`); the
27
+ * {@link Email} client narrows it at its own typed boundary.
28
+ */
29
+ export interface EmailComponent {
30
+ mutations: {
31
+ enqueue: FunctionReference<"mutation", "internal", {
32
+ messageId: string;
33
+ to: string;
34
+ from: string;
35
+ transport: string;
36
+ payload?: unknown;
37
+ subjectRef?: string;
38
+ idempotencyKey?: string;
39
+ maxAttempts: number;
40
+ }, {
41
+ messageId: string;
42
+ deduplicated: boolean;
43
+ }>;
44
+ markSending: FunctionReference<"mutation", "internal", {
45
+ messageId: string;
46
+ }, {
47
+ attempts: number;
48
+ }>;
49
+ markSent: FunctionReference<"mutation", "internal", {
50
+ messageId: string;
51
+ providerId?: string;
52
+ }, null>;
53
+ markFailed: FunctionReference<"mutation", "internal", {
54
+ messageId: string;
55
+ error?: string;
56
+ }, {
57
+ status: "queued" | "failed";
58
+ retried: boolean;
59
+ }>;
60
+ prune: FunctionReference<"mutation", "internal", {
61
+ before?: number;
62
+ batch: number;
63
+ }, number>;
64
+ };
65
+ queries: {
66
+ get: FunctionReference<"query", "internal", {
67
+ messageId: string;
68
+ }, RawView | null>;
69
+ listByStatus: FunctionReference<"query", "internal", {
70
+ status: MessageStatus;
71
+ paginationOpts: PaginationOptions;
72
+ }, PaginationResult<RawView>>;
73
+ };
74
+ }
75
+ interface RunQueryCtx {
76
+ runQuery<Q extends FunctionReference<"query", "internal">>(reference: Q, args: FunctionArgs<Q>): Promise<FunctionReturnType<Q>>;
77
+ }
78
+ interface RunMutationCtx {
79
+ runMutation<M extends FunctionReference<"mutation", "internal">>(reference: M, args: FunctionArgs<M>): Promise<FunctionReturnType<M>>;
80
+ }
81
+ /**
82
+ * Consumer-facing client for the durable, transport-agnostic outbound email
83
+ * queue. A host mutation enqueues a message with `enqueue` and gets its id back;
84
+ * the host's own transport sender (JMAP, an HTTP API, an SMTP-relay shim — never
85
+ * a vendor baked into this component) claims it (`markSending`), dispatches it,
86
+ * and reports the outcome (`markSent` / `markFailed`); `markFailed` retries until
87
+ * the attempt budget is spent. The component records intent and status — it never
88
+ * calls a provider. The host owns meaning and auth: it passes opaque `to`/`from`
89
+ * addresses, a `transport` adapter name, and an opaque `payload` the component
90
+ * stores without inspecting. Pass `payloadValidator` to narrow that opaque data
91
+ * to `TPayload` at the boundary — there is no unchecked cast.
92
+ *
93
+ * @typeParam TPayload - The host's rendered message payload type (defaults to `unknown`).
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const email = new Email(components.email, {
98
+ * payloadValidator: v.object({ subject: v.string(), html: v.string() }).parse,
99
+ * });
100
+ * const { messageId } = await email.enqueue(ctx, id, "user@x.com", "no-reply@app.com", "jmap", {
101
+ * payload: { subject: "Hi", html: "<p>Welcome</p>" },
102
+ * idempotencyKey: `welcome:${userId}`,
103
+ * });
104
+ * // ... the host's transport sender:
105
+ * await email.markSending(ctx, messageId);
106
+ * await email.markSent(ctx, messageId, { providerId: "jmap-123" });
107
+ * // ... clients poll:
108
+ * const msg = await email.get(ctx, messageId); // typed payload
109
+ * ```
110
+ */
111
+ export declare class Email<TPayload = unknown> {
112
+ private readonly component;
113
+ private readonly payloadValidator;
114
+ private readonly maxAttempts;
115
+ constructor(component: EmailComponent, options?: EmailOptions<TPayload>);
116
+ /** Narrow an opaque value through the host parser; pass `undefined` and an unset parser through. */
117
+ private parse;
118
+ /** Project a raw component view into the typed, validated client view. */
119
+ private view;
120
+ /**
121
+ * Enqueue an outbound message and return its id immediately. `messageId` is
122
+ * host-supplied and must be unique; `to`/`from` are opaque addresses;
123
+ * `transport` names the host-configured adapter. `opts.payload` is opaque host
124
+ * data validated against `payloadValidator` before storage. When
125
+ * `opts.idempotencyKey` matches an existing message the existing id is returned
126
+ * (`deduplicated: true`) and no new row is inserted. The message starts
127
+ * `queued`.
128
+ */
129
+ enqueue(ctx: RunMutationCtx, messageId: string, to: string, from: string, transport: string, opts?: EnqueueOptions<TPayload>): Promise<EnqueueResult>;
130
+ /**
131
+ * Claim a `queued` message for a send attempt (move it to `sending`, increment
132
+ * `attempts`). Returns the new attempt count. Rejects a missing id, a terminal
133
+ * message, and an already-`sending` message (claimed by another sender).
134
+ */
135
+ markSending(ctx: RunMutationCtx, messageId: string): Promise<{
136
+ attempts: number;
137
+ }>;
138
+ /**
139
+ * Record a successful send — the message moves to terminal `sent`, recording
140
+ * the transport's `opts.providerId`. Idempotent against a replayed callback.
141
+ * Rejects a missing id and an already-`failed` message.
142
+ */
143
+ markSent(ctx: RunMutationCtx, messageId: string, opts?: {
144
+ providerId?: string;
145
+ }): Promise<null>;
146
+ /**
147
+ * Record a failed send attempt, recording `opts.error`. The message returns to
148
+ * `queued` for another attempt while attempts remain, or lands in terminal
149
+ * `failed` once exhausted (see the returned `retried` flag). Rejects a missing
150
+ * id and an already-terminal message.
151
+ */
152
+ markFailed(ctx: RunMutationCtx, messageId: string, opts?: {
153
+ error?: string;
154
+ }): Promise<MarkFailedResult>;
155
+ /** The current envelope for `messageId`, or `null` if no such message is held. */
156
+ get(ctx: RunQueryCtx, messageId: string): Promise<MessageView<TPayload> | null>;
157
+ /**
158
+ * Page messages in one `status`, oldest first. Returns the standard Convex
159
+ * pagination envelope with each row narrowed to the typed view.
160
+ */
161
+ listByStatus(ctx: RunQueryCtx, status: MessageStatus, paginationOpts: PaginationOptions): Promise<PaginationResult<MessageView<TPayload>>>;
162
+ /**
163
+ * Delete terminal messages whose `updatedAt < before` in bounded batches,
164
+ * oldest first. `before` defaults to the server clock; `batch` caps each pass
165
+ * and the sweep self-reschedules until the tail is clean. Returns the count
166
+ * removed in the first pass. The built-in daily cron drives this automatically.
167
+ */
168
+ prune(ctx: RunMutationCtx, opts?: {
169
+ before?: number;
170
+ batch?: number;
171
+ }): Promise<number>;
172
+ }
173
+ export type { EmailOptions, EnqueueOptions, EnqueueResult, MarkFailedResult, MessageStatus, MessageView, Parser, };
174
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,151 @@
1
+ import { DEFAULT_MAX_ATTEMPTS, DEFAULT_PRUNE_BATCH } from "../shared.js";
2
+ /**
3
+ * Consumer-facing client for the durable, transport-agnostic outbound email
4
+ * queue. A host mutation enqueues a message with `enqueue` and gets its id back;
5
+ * the host's own transport sender (JMAP, an HTTP API, an SMTP-relay shim — never
6
+ * a vendor baked into this component) claims it (`markSending`), dispatches it,
7
+ * and reports the outcome (`markSent` / `markFailed`); `markFailed` retries until
8
+ * the attempt budget is spent. The component records intent and status — it never
9
+ * calls a provider. The host owns meaning and auth: it passes opaque `to`/`from`
10
+ * addresses, a `transport` adapter name, and an opaque `payload` the component
11
+ * stores without inspecting. Pass `payloadValidator` to narrow that opaque data
12
+ * to `TPayload` at the boundary — there is no unchecked cast.
13
+ *
14
+ * @typeParam TPayload - The host's rendered message payload type (defaults to `unknown`).
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const email = new Email(components.email, {
19
+ * payloadValidator: v.object({ subject: v.string(), html: v.string() }).parse,
20
+ * });
21
+ * const { messageId } = await email.enqueue(ctx, id, "user@x.com", "no-reply@app.com", "jmap", {
22
+ * payload: { subject: "Hi", html: "<p>Welcome</p>" },
23
+ * idempotencyKey: `welcome:${userId}`,
24
+ * });
25
+ * // ... the host's transport sender:
26
+ * await email.markSending(ctx, messageId);
27
+ * await email.markSent(ctx, messageId, { providerId: "jmap-123" });
28
+ * // ... clients poll:
29
+ * const msg = await email.get(ctx, messageId); // typed payload
30
+ * ```
31
+ */
32
+ export class Email {
33
+ component;
34
+ payloadValidator;
35
+ maxAttempts;
36
+ constructor(component, options = {}) {
37
+ this.component = component;
38
+ this.payloadValidator = options.payloadValidator;
39
+ this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
40
+ }
41
+ /** Narrow an opaque value through the host parser; pass `undefined` and an unset parser through. */
42
+ parse(value) {
43
+ if (value === undefined) {
44
+ return undefined;
45
+ }
46
+ if (this.payloadValidator === undefined) {
47
+ return value;
48
+ }
49
+ return this.payloadValidator(value);
50
+ }
51
+ /** Project a raw component view into the typed, validated client view. */
52
+ view(raw) {
53
+ return {
54
+ messageId: raw.messageId,
55
+ to: raw.to,
56
+ from: raw.from,
57
+ transport: raw.transport,
58
+ status: raw.status,
59
+ payload: this.parse(raw.payload),
60
+ subjectRef: raw.subjectRef,
61
+ idempotencyKey: raw.idempotencyKey,
62
+ providerId: raw.providerId,
63
+ attempts: raw.attempts,
64
+ maxAttempts: raw.maxAttempts,
65
+ error: raw.error,
66
+ createdAt: raw.createdAt,
67
+ updatedAt: raw.updatedAt,
68
+ };
69
+ }
70
+ /**
71
+ * Enqueue an outbound message and return its id immediately. `messageId` is
72
+ * host-supplied and must be unique; `to`/`from` are opaque addresses;
73
+ * `transport` names the host-configured adapter. `opts.payload` is opaque host
74
+ * data validated against `payloadValidator` before storage. When
75
+ * `opts.idempotencyKey` matches an existing message the existing id is returned
76
+ * (`deduplicated: true`) and no new row is inserted. The message starts
77
+ * `queued`.
78
+ */
79
+ enqueue(ctx, messageId, to, from, transport, opts = {}) {
80
+ return ctx.runMutation(this.component.mutations.enqueue, {
81
+ messageId,
82
+ to,
83
+ from,
84
+ transport,
85
+ payload: opts.payload === undefined ? undefined : this.parse(opts.payload),
86
+ subjectRef: opts.subjectRef,
87
+ idempotencyKey: opts.idempotencyKey,
88
+ maxAttempts: opts.maxAttempts ?? this.maxAttempts,
89
+ });
90
+ }
91
+ /**
92
+ * Claim a `queued` message for a send attempt (move it to `sending`, increment
93
+ * `attempts`). Returns the new attempt count. Rejects a missing id, a terminal
94
+ * message, and an already-`sending` message (claimed by another sender).
95
+ */
96
+ markSending(ctx, messageId) {
97
+ return ctx.runMutation(this.component.mutations.markSending, { messageId });
98
+ }
99
+ /**
100
+ * Record a successful send — the message moves to terminal `sent`, recording
101
+ * the transport's `opts.providerId`. Idempotent against a replayed callback.
102
+ * Rejects a missing id and an already-`failed` message.
103
+ */
104
+ markSent(ctx, messageId, opts = {}) {
105
+ return ctx.runMutation(this.component.mutations.markSent, {
106
+ messageId,
107
+ providerId: opts.providerId,
108
+ });
109
+ }
110
+ /**
111
+ * Record a failed send attempt, recording `opts.error`. The message returns to
112
+ * `queued` for another attempt while attempts remain, or lands in terminal
113
+ * `failed` once exhausted (see the returned `retried` flag). Rejects a missing
114
+ * id and an already-terminal message.
115
+ */
116
+ markFailed(ctx, messageId, opts = {}) {
117
+ return ctx.runMutation(this.component.mutations.markFailed, {
118
+ messageId,
119
+ error: opts.error,
120
+ });
121
+ }
122
+ /** The current envelope for `messageId`, or `null` if no such message is held. */
123
+ async get(ctx, messageId) {
124
+ const raw = await ctx.runQuery(this.component.queries.get, { messageId });
125
+ return raw === null ? null : this.view(raw);
126
+ }
127
+ /**
128
+ * Page messages in one `status`, oldest first. Returns the standard Convex
129
+ * pagination envelope with each row narrowed to the typed view.
130
+ */
131
+ async listByStatus(ctx, status, paginationOpts) {
132
+ const result = await ctx.runQuery(this.component.queries.listByStatus, {
133
+ status,
134
+ paginationOpts,
135
+ });
136
+ return { ...result, page: result.page.map((raw) => this.view(raw)) };
137
+ }
138
+ /**
139
+ * Delete terminal messages whose `updatedAt < before` in bounded batches,
140
+ * oldest first. `before` defaults to the server clock; `batch` caps each pass
141
+ * and the sweep self-reschedules until the tail is clean. Returns the count
142
+ * removed in the first pass. The built-in daily cron drives this automatically.
143
+ */
144
+ prune(ctx, opts = {}) {
145
+ return ctx.runMutation(this.component.mutations.prune, {
146
+ before: opts.before,
147
+ batch: opts.batch ?? DEFAULT_PRUNE_BATCH,
148
+ });
149
+ }
150
+ }
151
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ /** Public TypeScript surface for the email client. */
2
+ /** The four lifecycle states a message moves through. */
3
+ export type MessageStatus = "queued" | "sending" | "sent" | "failed";
4
+ /**
5
+ * Validates and narrows the opaque stored `payload` to a host type `T` at the
6
+ * client boundary. Receives the raw value the component returned (`unknown`) and
7
+ * MUST return a typed `T` or throw. A `convex/values` validator's `.parse` (or a
8
+ * Zod `.parse`) fits directly; omit it to keep the value unvalidated.
9
+ *
10
+ * @typeParam T - The host's stored message `payload` type.
11
+ */
12
+ export type Parser<T> = (value: unknown) => T;
13
+ /** The public envelope returned by {@link Email.get}. */
14
+ export interface MessageView<TPayload = unknown> {
15
+ /** The host-supplied id naming this message. */
16
+ messageId: string;
17
+ /** The opaque destination address. */
18
+ to: string;
19
+ /** The opaque sender address. */
20
+ from: string;
21
+ /** The host-configured transport adapter this message is routed through. */
22
+ transport: string;
23
+ /** The current lifecycle status. */
24
+ status: MessageStatus;
25
+ /** The opaque rendered message payload (narrowed if a `payloadValidator` is set). */
26
+ payload?: TPayload;
27
+ /** An opaque host ref for subject-centric listing (e.g. the addressee subject). */
28
+ subjectRef?: string;
29
+ /** The dedup key, if the message was enqueued with one. */
30
+ idempotencyKey?: string;
31
+ /** The transport's own message handle, recorded once `sent`. */
32
+ providerId?: string;
33
+ /** The number of send attempts made so far. */
34
+ attempts: number;
35
+ /** The maximum send attempts before a failure is terminal. */
36
+ maxAttempts: number;
37
+ /** The host-supplied failure reason from the last failed attempt. */
38
+ error?: string;
39
+ /** Absolute ms timestamp the message was enqueued. */
40
+ createdAt: number;
41
+ /** Absolute ms timestamp of the last transition. */
42
+ updatedAt: number;
43
+ }
44
+ /** The result of {@link Email.enqueue}. */
45
+ export interface EnqueueResult {
46
+ /** The id of the queued message — the existing one when `deduplicated`. */
47
+ messageId: string;
48
+ /** True when an existing `idempotencyKey` matched and no new row was inserted. */
49
+ deduplicated: boolean;
50
+ }
51
+ /** Per-call options for {@link Email.enqueue}. */
52
+ export interface EnqueueOptions<TPayload> {
53
+ /** The opaque rendered message payload (validated against `payloadValidator` before storage). */
54
+ payload?: TPayload;
55
+ /** An opaque host ref recorded for subject-centric listing. */
56
+ subjectRef?: string;
57
+ /** A dedup key — a second enqueue carrying the same key returns the existing message. */
58
+ idempotencyKey?: string;
59
+ /** Override the client-level `maxAttempts` for this message. */
60
+ maxAttempts?: number;
61
+ }
62
+ /** The result of {@link Email.markFailed}. */
63
+ export interface MarkFailedResult {
64
+ /** `queued` when the message was re-queued for retry, `failed` when terminal. */
65
+ status: "queued" | "failed";
66
+ /** True when the message was re-queued (attempts remained), false when terminal. */
67
+ retried: boolean;
68
+ }
69
+ /** Construction options for the {@link Email} client. */
70
+ export interface EmailOptions<TPayload> {
71
+ /**
72
+ * Validates/narrows a stored `payload` to `TPayload` at the boundary — applied
73
+ * to the `payload` passed into `enqueue` (before storage) and the `payload`
74
+ * returned by `get` / `listByStatus`. Throws on a mismatch. Omit to leave the
75
+ * payload unvalidated.
76
+ */
77
+ payloadValidator?: Parser<TPayload>;
78
+ /**
79
+ * The default maximum send attempts before a `markFailed` is terminal. Per-call
80
+ * `enqueue` `maxAttempts` overrides it. Defaults to `DEFAULT_MAX_ATTEMPTS` (5).
81
+ */
82
+ maxAttempts?: number;
83
+ }
84
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,sDAAsD;AAEtD,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAErE;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,CAAC;AAE9C,yDAAyD;AACzD,MAAM,WAAW,WAAW,CAAC,QAAQ,GAAG,OAAO;IAC7C,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,MAAM,EAAE,aAAa,CAAC;IACtB,qFAAqF;IACrF,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,2CAA2C;AAC3C,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,kDAAkD;AAClD,MAAM,WAAW,cAAc,CAAC,QAAQ;IACtC,iGAAiG;IACjG,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,oFAAoF;IACpF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,yDAAyD;AACzD,MAAM,WAAW,YAAY,CAAC,QAAQ;IACpC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACpC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB"}
@@ -0,0 +1,3 @@
1
+ /** Public TypeScript surface for the email client. */
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,sDAAsD"}