@c9up/rover 0.1.15 → 0.1.17

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 (84) hide show
  1. package/dist/Mail.d.ts +8 -4
  2. package/dist/Mail.d.ts.map +1 -1
  3. package/dist/Mail.js.map +1 -1
  4. package/dist/MessageBuilder.js +1 -1
  5. package/dist/MessageBuilder.js.map +1 -1
  6. package/dist/RoverProvider.d.ts +8 -0
  7. package/dist/RoverProvider.d.ts.map +1 -1
  8. package/dist/RoverProvider.js +32 -10
  9. package/dist/RoverProvider.js.map +1 -1
  10. package/dist/augmentations.d.ts +29 -0
  11. package/dist/augmentations.d.ts.map +1 -0
  12. package/dist/augmentations.js +18 -0
  13. package/dist/augmentations.js.map +1 -0
  14. package/dist/emitSafely.d.ts +18 -0
  15. package/dist/emitSafely.d.ts.map +1 -0
  16. package/dist/emitSafely.js +22 -0
  17. package/dist/emitSafely.js.map +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/queue/MemoryMailMessenger.d.ts.map +1 -1
  23. package/dist/queue/MemoryMailMessenger.js +14 -15
  24. package/dist/queue/MemoryMailMessenger.js.map +1 -1
  25. package/dist/retry.d.ts +10 -1
  26. package/dist/retry.d.ts.map +1 -1
  27. package/dist/retry.js +9 -0
  28. package/dist/retry.js.map +1 -1
  29. package/dist/services/main.d.ts +9 -0
  30. package/dist/services/main.d.ts.map +1 -1
  31. package/dist/services/main.js +11 -0
  32. package/dist/services/main.js.map +1 -1
  33. package/dist/templating/loadNapi.d.ts.map +1 -1
  34. package/dist/templating/loadNapi.js +7 -16
  35. package/dist/templating/loadNapi.js.map +1 -1
  36. package/dist/testing/FakeMail.d.ts +4 -2
  37. package/dist/testing/FakeMail.d.ts.map +1 -1
  38. package/dist/testing/FakeMail.js +4 -19
  39. package/dist/testing/FakeMail.js.map +1 -1
  40. package/dist/transports/BrevoTransport.js +6 -3
  41. package/dist/transports/BrevoTransport.js.map +1 -1
  42. package/dist/transports/SparkPostTransport.js +6 -3
  43. package/dist/transports/SparkPostTransport.js.map +1 -1
  44. package/dist/vendor/nativeBinary.d.ts +43 -0
  45. package/dist/vendor/nativeBinary.d.ts.map +1 -0
  46. package/dist/vendor/nativeBinary.js +99 -0
  47. package/dist/vendor/nativeBinary.js.map +1 -0
  48. package/dist/webhooks/context.d.ts +9 -1
  49. package/dist/webhooks/context.d.ts.map +1 -1
  50. package/dist/webhooks/mailgun.d.ts.map +1 -1
  51. package/dist/webhooks/mailgun.js +2 -1
  52. package/dist/webhooks/mailgun.js.map +1 -1
  53. package/dist/webhooks/resend.d.ts.map +1 -1
  54. package/dist/webhooks/resend.js +2 -1
  55. package/dist/webhooks/resend.js.map +1 -1
  56. package/dist/webhooks/sendgrid.d.ts.map +1 -1
  57. package/dist/webhooks/sendgrid.js +2 -1
  58. package/dist/webhooks/sendgrid.js.map +1 -1
  59. package/index.darwin-arm64.node +0 -0
  60. package/index.darwin-x64.node +0 -0
  61. package/index.linux-arm64-gnu.node +0 -0
  62. package/index.linux-x64-gnu.node +0 -0
  63. package/index.win32-x64-msvc.node +0 -0
  64. package/package.json +7 -3
  65. package/scripts/build-napi-types.mjs +4 -3
  66. package/scripts/generate-napi-types.mjs +9 -6
  67. package/src/Mail.ts +10 -6
  68. package/src/MessageBuilder.ts +1 -1
  69. package/src/RoverProvider.ts +38 -9
  70. package/src/augmentations.ts +33 -0
  71. package/src/emitSafely.ts +31 -0
  72. package/src/index.ts +2 -0
  73. package/src/queue/MemoryMailMessenger.ts +15 -14
  74. package/src/retry.ts +11 -2
  75. package/src/services/main.ts +12 -0
  76. package/src/templating/loadNapi.ts +11 -17
  77. package/src/testing/FakeMail.ts +12 -3
  78. package/src/transports/BrevoTransport.ts +6 -3
  79. package/src/transports/SparkPostTransport.ts +6 -3
  80. package/src/vendor/nativeBinary.ts +110 -0
  81. package/src/webhooks/context.ts +9 -1
  82. package/src/webhooks/mailgun.ts +2 -1
  83. package/src/webhooks/resend.ts +2 -1
  84. package/src/webhooks/sendgrid.ts +2 -1
@@ -1,5 +1,6 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import { createPublicKey, verify } from "node:crypto";
3
+ import { emitSafely } from "../emitSafely.js";
3
4
  const EVENT_MAP = {
4
5
  delivered: "mail.delivered",
5
6
  bounce: "mail.bounced",
@@ -69,7 +70,7 @@ export function createSendGridWebhookHandler(options) {
69
70
  if (!mapped)
70
71
  continue;
71
72
  try {
72
- emitter.emit(mapped, {
73
+ emitSafely(emitter, mapped, {
73
74
  messageId: ev.sg_message_id ?? "",
74
75
  to: ev.email ?? "",
75
76
  reason: ev.reason,
@@ -1 +1 @@
1
- {"version":3,"file":"sendgrid.js","sourceRoot":"","sources":["../../src/webhooks/sendgrid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AA0BtD,MAAM,SAAS,GAA2B;IACzC,SAAS,EAAE,gBAAgB;IAC3B,MAAM,EAAE,cAAc;IACtB,OAAO,EAAE,aAAa;IACtB,QAAQ,EAAE,aAAa;IACvB,OAAO,EAAE,aAAa;CACtB,CAAC;AAEF,MAAM,UAAU,4BAA4B,CAC3C,OAA+B;IAE/B,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACd,+EAA+E,CAC/E,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAChC,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,GAAG,CAAC;IAEnD,OAAO,KAAK,EAAE,GAAuB,EAAE,KAAK,EAAiB,EAAE;QAC9D,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QACzD,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CACnC,wCAAwC,CACxC,CAAC;QACF,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CACnC,wCAAwC,CACxC,CAAC;QACF,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE,CAAC;YAC9B,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAC9D,OAAO;QACR,CAAC;QACD,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YACjC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAC9D,OAAO;QACR,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QACrC,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,GAAG,SAAS,CAAC,GAAG,aAAa,EAAE,CAAC;YACtD,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;YAC5D,OAAO;QACR,CAAC;QACD,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC;YACrC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC;YAC9B,GAAG;SACH,CAAC,CAAC;QACH,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACR,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAC9D,OAAO;QACR,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;QACxD,IAAI,CAAC,EAAE,EAAE,CAAC;YACT,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAC9D,OAAO;QACR,CAAC;QAED,IAAI,MAAuB,CAAC;QAC5B,IAAI,CAAC;YACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAoB,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACR,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;YACzD,OAAO;QACR,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAC;YACjE,OAAO;QACR,CAAC;QACD,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;YACzB,IAAI,CAAC,EAAE,CAAC,KAAK;gBAAE,SAAS;YACxB,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;YACnC,IAAI,CAAC,MAAM;gBAAE,SAAS;YACtB,IAAI,CAAC;gBACJ,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE;oBACpB,SAAS,EAAE,EAAE,CAAC,aAAa,IAAI,EAAE;oBACjC,EAAE,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE;oBAClB,MAAM,EAAE,EAAE,CAAC,MAAM;oBACjB,SAAS,EAAE,kBAAkB,CAAC,EAAE,CAAC,SAAS,CAAC;iBAC3C,CAAC,CAAC;YACJ,CAAC;YAAC,MAAM,CAAC;gBACR,wDAAwD;YACzD,CAAC;QACF,CAAC;QACD,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,GAAW;IACzB,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC9C,IAAI,UAAU,CAAC,QAAQ,CAAC,kBAAkB,CAAC;QAAE,OAAO,UAAU,CAAC;IAC/D,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1C,OAAO,+BAA+B,GAAG,4BAA4B,CAAC;AACvE,CAAC;AAED,SAAS,kBAAkB,CAAC,EAAsB;IACjD,qEAAqE;IACrE,kEAAkE;IAClE,mEAAmE;IACnE,8CAA8C;IAC9C,IAAI,EAAE,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC;IAChE,IAAI,EAAE,GAAG,IAAI;QAAE,OAAO,EAAE,CAAC;IACzB,OAAO,EAAE,GAAG,IAAI,CAAC;AAClB,CAAC"}
1
+ {"version":3,"file":"sendgrid.js","sourceRoot":"","sources":["../../src/webhooks/sendgrid.ts"],"names":[],"mappings":"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"}
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/rover",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Mail transport for Ream — SMTP, log, and pluggable transports",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -68,9 +68,12 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@biomejs/biome": "^2.4.10",
71
+ "@c9up/bay": "^0.2.0",
71
72
  "@c9up/helix": "^0.2.0",
73
+ "@c9up/ream": "^0.2.0",
72
74
  "@types/node": "*",
73
75
  "@types/nodemailer": "^7.0.0",
76
+ "@vitest/coverage-v8": "^4.1.2",
74
77
  "typescript": "^6.0.2",
75
78
  "vitest": "^4.1.2"
76
79
  },
@@ -106,8 +109,9 @@
106
109
  "build:napi": "cargo build --release -p rover-template-engine-napi && pnpm build:napi-types && node scripts/copy-napi.mjs",
107
110
  "build:napi-types": "node scripts/build-napi-types.mjs",
108
111
  "test": "vitest run",
109
- "lint": "biome check src/",
112
+ "lint": "biome check src/ tests/",
110
113
  "test:coverage": "vitest run --coverage",
111
- "typecheck": "tsc --noEmit"
114
+ "typecheck": "tsc --noEmit",
115
+ "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json"
112
116
  }
113
117
  }
@@ -7,7 +7,8 @@
7
7
  * cargo a `/tmp/...` path the native proc-macro cannot write to, so the
8
8
  * type-def file comes back empty and the build fails for no visible reason.
9
9
  *
10
- * One crate at a time on purpose: napi-derive APPENDS to `TYPE_DEF_TMP_PATH`
10
+ * One crate at a time on purpose: with napi-derive 2 the writes all went to a
11
+ * single `TYPE_DEF_TMP_PATH` and a parallel build interleaved them
11
12
  * while cargo compiles, and a parallel build interleaves the writes —
12
13
  * definitions go missing, silently, and the generated file comes out short.
13
14
  */
@@ -28,11 +29,11 @@ const scratch = mkdtempSync(join(tmpdir(), 'napi-types-'))
28
29
  try {
29
30
  const lines = []
30
31
  for (const crate of CRATES) {
31
- const perCrate = join(scratch, `${crate}.jsonl`)
32
+ const perCrate = join(scratch, crate)
32
33
  writeFileSync(perCrate, '')
33
34
  execFileSync('cargo', ['build', '-p', crate], {
34
35
  cwd: packageRoot,
35
- env: { ...process.env, TYPE_DEF_TMP_PATH: perCrate },
36
+ env: { ...process.env, NAPI_TYPE_DEF_TMP_FOLDER: scratch },
36
37
  stdio: ['ignore', 'ignore', 'inherit'],
37
38
  })
38
39
  const emitted = readFileSync(perCrate, 'utf8').split('\n').filter(Boolean)
@@ -130,12 +130,15 @@ for (const fn of fns) {
130
130
  // napi-derive emits the whole declaration for a function, unlike a struct
131
131
  // where `def` holds only the members.
132
132
  const declaration = fn.def.trim()
133
- out.push(
134
- declaration.startsWith('export declare function')
135
- ? `${declaration};`
136
- : `export declare function ${fn.name}${declaration};`,
137
- '',
138
- )
133
+ // napi-derive 3 emits a bare `function name(...)` where 2 emitted the
134
+ // signature after the name; concatenating onto the former produced
135
+ // `function xfunction x(...)`.
136
+ const rendered = declaration.startsWith('export declare function')
137
+ ? declaration
138
+ : declaration.startsWith('function ')
139
+ ? `export declare ${declaration}`
140
+ : `export declare function ${fn.name}${declaration}`
141
+ out.push(`${rendered};`, '')
139
142
  }
140
143
 
141
144
  const stale = Object.keys(CALLBACK_REFINEMENTS).filter((k) => !used.has(k))
package/src/Mail.ts CHANGED
@@ -72,7 +72,11 @@ export interface MailTransport {
72
72
  * Rover never hard-imports the event bus.
73
73
  */
74
74
  export interface EmitterLike {
75
- emit(event: string, data: unknown): void;
75
+ /**
76
+ * `unknown`, not `void`: a `void` return ACCEPTS a promise-returning
77
+ * function, which is how an Adonis emitter's rejection went unnoticed.
78
+ */
79
+ emit(event: string, data: unknown): unknown;
76
80
  }
77
81
 
78
82
  /**
@@ -147,7 +151,7 @@ export interface MailConfig {
147
151
  */
148
152
  mailers?: Record<
149
153
  string,
150
- { transport: string; retry?: RetryConfig; [key: string]: unknown }
154
+ { transport: string; retry?: Partial<RetryConfig>; [key: string]: unknown }
151
155
  >;
152
156
  /**
153
157
  * Rover's older spelling of `mailers`. Still accepted and identical in
@@ -155,14 +159,14 @@ export interface MailConfig {
155
159
  */
156
160
  transports?: Record<
157
161
  string,
158
- { transport: string; retry?: RetryConfig; [key: string]: unknown }
162
+ { transport: string; retry?: Partial<RetryConfig>; [key: string]: unknown }
159
163
  >;
160
164
  /** Root directory for `htmlView(path, data)` template lookups. Default: `"resources/views/emails"`. */
161
165
  viewsRoot?: string;
162
166
  /** Optional Bay queue tuning for `sendLater()`. */
163
167
  queue?: { name?: string; maxAttempts?: number };
164
168
  /** Process-wide retry defaults. Overridden per-mailer via `mailers[name].retry`. */
165
- retry?: RetryConfig;
169
+ retry?: Partial<RetryConfig>;
166
170
  }
167
171
 
168
172
  /**
@@ -393,8 +397,8 @@ export class Mail {
393
397
  #memoryMessenger: MemoryMailMessenger;
394
398
  #queueName: string;
395
399
  #queueMaxAttempts: number;
396
- #globalRetry: RetryConfig | undefined;
397
- #transportRetry: Map<string, RetryConfig> = new Map();
400
+ #globalRetry: Partial<RetryConfig> | undefined;
401
+ #transportRetry: Map<string, Partial<RetryConfig>> = new Map();
398
402
  #hooks: MailHooks;
399
403
  #emitter: EmitterLike | null;
400
404
  /** Per-instance template root, threaded into each render so concurrent Mails with different roots don't clobber the shared global. */
@@ -228,7 +228,7 @@ function listUrls(value: ListHeader | ListHeader[] | ListHeader[][]): string[] {
228
228
  function titleCaseKey(key: string): string {
229
229
  return key
230
230
  .split("-")
231
- .map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part))
231
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
232
232
  .join("-");
233
233
  }
234
234
 
@@ -1,7 +1,8 @@
1
+ import "./augmentations.js";
1
2
  import type { EmitterLike, MailConfig } from "./Mail.js";
2
3
  import { Mail } from "./Mail.js";
3
4
  import type { BayQueueLike } from "./queue/MailJob.js";
4
- import { setMail } from "./services/main.js";
5
+ import { clearMail, getMail, setMail } from "./services/main.js";
5
6
 
6
7
  /**
7
8
  * Duck-typed slice of the host's IoC container — rover does NOT import
@@ -10,6 +11,12 @@ import { setMail } from "./services/main.js";
10
11
  interface RoverContainer {
11
12
  singleton(token: unknown, factory: () => unknown): void;
12
13
  resolve<T = unknown>(token: unknown): Promise<T>;
14
+ /**
15
+ * Optional reader — present on ream's real container. Asked before resolving
16
+ * an OPTIONAL peer, so "not installed" is answered by a lookup rather than
17
+ * inferred from whatever resolving it threw.
18
+ */
19
+ has?(token: unknown): boolean;
13
20
  }
14
21
  interface RoverConfigStore {
15
22
  get<T = unknown>(key: string): T | undefined;
@@ -20,6 +27,9 @@ export interface RoverAppContext {
20
27
  }
21
28
 
22
29
  export default class RoverProvider {
30
+ /** The Mail this provider bound, so shutdown only clears its own. */
31
+ #mail: Mail | undefined;
32
+
23
33
  constructor(protected app: RoverAppContext) {}
24
34
 
25
35
  register() {
@@ -43,19 +53,33 @@ export default class RoverProvider {
43
53
  },
44
54
  );
45
55
  });
46
- this.app.container.singleton("mail", () => {
47
- return this.app.container.resolve<Mail>(Mail);
48
- });
56
+ // Namespaced by the package that owns it, the way upstream namespaces
57
+ // `lucid.db`, `auth.manager` and `drive.manager` by theirs. The bare
58
+ // token stays bound beside it: it is what every existing
59
+ // `container.make(...)` asks for, and a token is not worth breaking an
60
+ // application over.
61
+ const mail = (): Promise<Mail> => this.app.container.resolve<Mail>(Mail);
62
+ this.app.container.singleton("rover.mail", mail);
63
+ this.app.container.singleton("mail", mail);
49
64
  }
50
65
 
51
66
  async boot() {
52
67
  // Populate the `@c9up/rover/services/main` singleton with the
53
68
  // container-resolved Mail instance so apps can
54
69
  // `import mail from '@c9up/rover/services/main'` from anywhere.
55
- setMail(await this.app.container.resolve<Mail>(Mail));
70
+ const mail = await this.app.container.resolve<Mail>(Mail);
71
+ this.#mail = mail;
72
+ setMail(mail);
56
73
  }
57
74
 
58
- async shutdown() {}
75
+ async shutdown() {
76
+ // Release the module-level singleton, but only while it is still ours: a
77
+ // stopped application left a dead Mail reachable through
78
+ // `services/main`, and with two applications in one process the one
79
+ // shutting down must not clear a binding the other has since installed.
80
+ if (this.#mail !== undefined && getMail() === this.#mail) clearMail();
81
+ this.#mail = undefined;
82
+ }
59
83
  }
60
84
 
61
85
  /**
@@ -67,9 +91,14 @@ async function tryResolve<T>(
67
91
  app: RoverAppContext,
68
92
  token: string,
69
93
  ): Promise<T | undefined> {
70
- try {
71
- return await app.container.resolve<T>(token);
72
- } catch {
94
+ // ASK whether the token is bound, rather than catching whatever resolving it
95
+ // throws. Catching everything meant a registered binding whose factory
96
+ // failed — a bad queue config, a driver that cannot connect — read exactly
97
+ // like "bay is not installed": rover disabled the feature and said nothing,
98
+ // and mail queued nowhere. Absence is a `has()` answer; a factory that
99
+ // throws is a fault, and faults propagate.
100
+ if (typeof app.container.has === "function" && !app.container.has(token)) {
73
101
  return undefined;
74
102
  }
103
+ return await app.container.resolve<T>(token);
75
104
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Teach ream's `ContainerBindings` what `container.make(...)` returns for the
3
+ * tokens rover binds.
4
+ *
5
+ * ream declares that interface open on purpose: it registers its own entries
6
+ * and expects each package to contribute the ones it owns. Nothing filled
7
+ * these in, so resolving by the string token answered `unknown` and every call
8
+ * site had to assert a type it could not prove.
9
+ *
10
+ * Loaded from the package barrel, so importing Rover anywhere in the
11
+ * application is enough — nobody writes a `declare module` of their own.
12
+ *
13
+ * Type-only, and ream stays an OPTIONAL peer: nothing here reaches a runtime
14
+ * import, and a `declare module` for a specifier that does not resolve is
15
+ * simply inert.
16
+ */
17
+
18
+ // Referenced so the augmentation below resolves the module it augments.
19
+ import type {} from "@c9up/ream/types";
20
+
21
+ import type { Mail } from "./Mail.js";
22
+
23
+ declare module "@c9up/ream/types" {
24
+ interface ContainerBindings {
25
+ /** The mailer, bound by `RoverProvider`. */
26
+ "rover.mail": Mail;
27
+ /**
28
+ * The same binding under the name it had before the token carried its
29
+ * package. Kept bound so an existing `container.make(...)` resolves.
30
+ */
31
+ mail: Mail;
32
+ }
33
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Emit an event without letting the listener come back.
3
+ *
4
+ * `@adonisjs/events` declares `emit(): Promise<void>` and rethrows when a
5
+ * listener fails and the application registered no error handler. Nothing here
6
+ * awaits an event — a webhook has already been acknowledged, a queued mail has
7
+ * already been sent or already failed — so that rejection had nowhere to go
8
+ * and ended the process over a notification.
9
+ *
10
+ * The emitter is invoked INSIDE the async function rather than before it:
11
+ * `Promise.resolve(emit(...))` runs `emit` first, so a synchronous throw would
12
+ * still escape. The call itself stays synchronous, because an async body runs
13
+ * to its first `await` — only the failure handling is deferred.
14
+ */
15
+ export function emitSafely(
16
+ emitter:
17
+ | { emit: (event: string, data: unknown) => unknown }
18
+ | null
19
+ | undefined,
20
+ event: string,
21
+ data: unknown,
22
+ ): void {
23
+ if (emitter == null) return;
24
+ void (async () => emitter.emit(event, data))().catch((error: unknown) => {
25
+ process.stderr.write(
26
+ `[rover] '${event}' listener failed: ${
27
+ error instanceof Error ? error.message : String(error)
28
+ }\n`,
29
+ );
30
+ });
31
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  * for each of them, so each has to be there when the config is read.
8
8
  */
9
9
 
10
+ import "./augmentations.js";
11
+
10
12
  export type { MailAddress } from "./BaseMail.js";
11
13
  export { BaseMail } from "./BaseMail.js";
12
14
  export type { TransportDescriptor } from "./config.js";
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { emitSafely } from "../emitSafely.js";
2
3
  import type { EmitterLike, MailMessage } from "../Mail.js";
3
4
  import type { MailDispatcher } from "./MailJob.js";
4
5
 
@@ -24,20 +25,20 @@ export class MemoryMailMessenger {
24
25
  this.#dispatcher
25
26
  .dispatchMessage(message, transport)
26
27
  .catch((error: unknown) => {
27
- if (!this.#emitter) return;
28
- try {
29
- this.#emitter.emit("queued:mail:error", {
30
- jobId,
31
- transportName: transport,
32
- error:
33
- error instanceof Error
34
- ? { message: error.message }
35
- : { message: String(error) },
36
- });
37
- } catch {
38
- // Event bus failure ≠ mail failure — swallow so the microtask
39
- // never rejects.
40
- }
28
+ // The try/catch here only ever caught a listener that threw
29
+ // SYNCHRONOUSLY. An Adonis emitter's `emit` is async and
30
+ // rethrows when a listener fails and no error handler is
31
+ // registered, so an async one rejected inside a microtask
32
+ // nobody awaits — the failure notification taking the process
33
+ // down after the mail had already failed.
34
+ emitSafely(this.#emitter, "queued:mail:error", {
35
+ jobId,
36
+ transportName: transport,
37
+ error:
38
+ error instanceof Error
39
+ ? { message: error.message }
40
+ : { message: String(error) },
41
+ });
41
42
  });
42
43
  });
43
44
  return jobId;
package/src/retry.ts CHANGED
@@ -83,9 +83,18 @@ export function computeBackoffMs(
83
83
  return Math.min(config.maxDelayMs, Math.max(50, raw));
84
84
  }
85
85
 
86
+ /**
87
+ * Global and per-transport overrides, merged over the defaults.
88
+ *
89
+ * `Partial`, because that is what the function does: it spreads whatever it is
90
+ * given over {@link DEFAULT_RETRY_CONFIG}, so `{ maxAttempts: 1 }` is a
91
+ * complete answer. Declared as the full `RetryConfig`, a config file writing
92
+ * exactly that was a type error for doing the supported thing — and every
93
+ * caller had to spell `baseDelayMs` and `factor` it did not want to change.
94
+ */
86
95
  export function resolveRetryConfig(
87
- globalConfig: RetryConfig | undefined,
88
- transportConfig: RetryConfig | undefined,
96
+ globalConfig: Partial<RetryConfig> | undefined,
97
+ transportConfig: Partial<RetryConfig> | undefined,
89
98
  ): Required<RetryConfig> {
90
99
  // Skip entries whose value is `undefined` so a partial override doesn't
91
100
  // clobber a default or global value. Pre-spread filter is required because
@@ -25,6 +25,18 @@ export function getMail(): Mail | undefined {
25
25
  return instance;
26
26
  }
27
27
 
28
+ /**
29
+ * @internal Release the singleton, so a shut-down application does not leave a
30
+ * dead Mail reachable through `services/main`.
31
+ *
32
+ * The caller checks ownership first (`getMail() === mine`): with two
33
+ * applications in one process, the one shutting down must not clear a binding
34
+ * the other has since installed.
35
+ */
36
+ export function clearMail(): void {
37
+ instance = undefined;
38
+ }
39
+
28
40
  const mail: Mail = new Proxy({} as Mail, {
29
41
  get(_target, prop) {
30
42
  if (!instance) {
@@ -8,25 +8,21 @@
8
8
  // — every boundary is narrowed with a `Reflect.get` type guard.
9
9
 
10
10
  import { createRequire } from "node:module";
11
- import { arch, platform } from "node:process";
12
11
  import { fileURLToPath } from "node:url";
13
12
  import { RoverError } from "../RoverError.js";
14
-
15
- const SUFFIX_MAP: Readonly<Record<string, string>> = {
16
- "linux-x64": "linux-x64-gnu",
17
- "linux-arm64": "linux-arm64-gnu",
18
- "darwin-x64": "darwin-x64",
19
- "darwin-arm64": "darwin-arm64",
20
- "win32-x64": "win32-x64-msvc",
21
- };
13
+ import {
14
+ muslHint,
15
+ nativeBinarySuffix,
16
+ supportedTargets,
17
+ } from "../vendor/nativeBinary.js";
22
18
 
23
19
  function platformSuffix(): string {
24
- const key = `${platform}-${arch}`;
25
- const suffix = SUFFIX_MAP[key];
26
- if (typeof suffix !== "string") {
20
+ // The table is shared; the refusal is rover's, with rover's code on it.
21
+ const suffix = nativeBinarySuffix();
22
+ if (suffix === undefined) {
27
23
  throw new RoverError(
28
24
  "E_MAIL_TEMPLATE_NAPI_REQUIRED",
29
- `Unsupported platform/arch '${key}' for @c9up/rover native binary. Supported: ${Object.keys(SUFFIX_MAP).join(", ")}.`,
25
+ `No @c9up/rover native binary for this platform. Supported: ${supportedTargets().join(", ")}.`,
30
26
  {
31
27
  hint: "Build the native binary on a supported platform with 'pnpm --filter @c9up/rover build:napi'.",
32
28
  },
@@ -83,12 +79,10 @@ export function getNative(): NativeExports {
83
79
  // The prebuilt linux binaries target glibc (`-gnu`). On musl hosts (Alpine
84
80
  // containers) the `-gnu` binary fails to dlopen with a libc symbol error —
85
81
  // surface that explicitly rather than only pointing at the build step.
86
- const muslHint = suffix.endsWith("-gnu")
87
- ? " If you are on Alpine/musl, note the prebuilt binaries target glibc (musl is not a supported target)."
88
- : "";
82
+ const musl = muslHint();
89
83
  throw new RoverError(
90
84
  "E_MAIL_TEMPLATE_NAPI_REQUIRED",
91
- `@c9up/rover native binary 'index.${suffix}.node' not found or failed to load near ${here} — run 'pnpm --filter @c9up/rover build:napi' to build it.${muslHint} Cause: ${causeMessage}`,
85
+ `@c9up/rover native binary 'index.${suffix}.node' not found or failed to load near ${here} — run 'pnpm --filter @c9up/rover build:napi' to build it.${musl} Cause: ${causeMessage}`,
92
86
  {
93
87
  hint: "Run 'pnpm --filter @c9up/rover build:napi' to compile the native template engine.",
94
88
  },
@@ -1,4 +1,4 @@
1
- import type { Plugin } from "@c9up/helix";
1
+ import type { PluginApi } from "@c9up/helix";
2
2
  import { BaseMail } from "../BaseMail.js";
3
3
  import type { MailMessage, MailSendOutcome, MailTransport } from "../Mail.js";
4
4
 
@@ -340,8 +340,17 @@ export interface FakeableMailer {
340
340
  * frame) and torn down when that test ends — so capture never leaks between
341
341
  * tests. Uses the manager's own `fake()`/`restore()` (the Adonis pattern).
342
342
  */
343
- export function mailFake(mailer: FakeableMailer): Plugin {
344
- return (api) => {
343
+ /** The slice of helix's `PluginApi` this plugin uses. */
344
+ export type MailPluginHost = Pick<PluginApi, "context">;
345
+
346
+ export function mailFake(
347
+ mailer: FakeableMailer,
348
+ ): (api: MailPluginHost) => void {
349
+ // Typed by what it touches, not by the whole `PluginApi`: a test wiring the
350
+ // plugin had to build every field of it — `config`, `cliArgs`, `runner`,
351
+ // `emitter`, `cleanup` — to exercise a function that reads one. Same shape
352
+ // `@c9up/helix-plugin-ream` uses for its own host.
353
+ return (api: MailPluginHost) => {
345
354
  api.context.getter("mail", (ctx) => {
346
355
  const fake = mailer.fake();
347
356
  ctx.cleanup(() => mailer.restore());
@@ -165,9 +165,12 @@ export class BrevoTransport implements MailTransport {
165
165
  function parseContact(input: string): BrevoContact {
166
166
  const s = stripCrlf(input).trim();
167
167
  const match = s.match(/^(.*)<([^>]+)>\s*$/);
168
- if (match) {
169
- const email = match[2].trim();
170
- const name = match[1].trim().replace(/^"|"$/g, "").trim();
168
+ // Both groups are required by the pattern, so a match carries both —
169
+ // named rather than indexed, which is what says so.
170
+ const [, rawName, rawEmail] = match ?? [];
171
+ if (rawEmail !== undefined) {
172
+ const email = rawEmail.trim();
173
+ const name = (rawName ?? "").trim().replace(/^"|"$/g, "").trim();
171
174
  return name ? { email, name } : { email };
172
175
  }
173
176
  return { email: s };
@@ -177,9 +177,12 @@ export class SparkPostTransport implements MailTransport {
177
177
  function parseAddress(input: string): { email: string; name?: string } {
178
178
  const s = stripCrlf(input).trim();
179
179
  const match = s.match(/^(.*)<([^>]+)>\s*$/);
180
- if (match) {
181
- const email = match[2].trim();
182
- const name = match[1].trim().replace(/^"|"$/g, "").trim();
180
+ // Both groups are required by the pattern, so a match carries both —
181
+ // named rather than indexed, which is what says so.
182
+ const [, rawName, rawEmail] = match ?? [];
183
+ if (rawEmail !== undefined) {
184
+ const email = rawEmail.trim();
185
+ const name = (rawName ?? "").trim().replace(/^"|"$/g, "").trim();
183
186
  return name ? { email, name } : { email };
184
187
  }
185
188
  return { email: s };