@beignet/core 0.0.1 → 0.0.3

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 (287) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +202 -8
  3. package/dist/application/index.d.ts +93 -9
  4. package/dist/application/index.d.ts.map +1 -1
  5. package/dist/application/index.js +11 -11
  6. package/dist/application/index.js.map +1 -1
  7. package/dist/client/client.d.ts +73 -12
  8. package/dist/client/client.d.ts.map +1 -1
  9. package/dist/client/client.js +37 -12
  10. package/dist/client/client.js.map +1 -1
  11. package/dist/client/index.d.ts +12 -0
  12. package/dist/client/index.d.ts.map +1 -1
  13. package/dist/client/index.js +6 -0
  14. package/dist/client/index.js.map +1 -1
  15. package/dist/client/types.d.ts +69 -8
  16. package/dist/client/types.d.ts.map +1 -1
  17. package/dist/config/index.d.ts +84 -0
  18. package/dist/config/index.d.ts.map +1 -1
  19. package/dist/config/index.js +36 -0
  20. package/dist/config/index.js.map +1 -1
  21. package/dist/contracts/contract-builder.d.ts +49 -22
  22. package/dist/contracts/contract-builder.d.ts.map +1 -1
  23. package/dist/contracts/contract-builder.js +48 -21
  24. package/dist/contracts/contract-builder.js.map +1 -1
  25. package/dist/contracts/contract-group.d.ts +35 -19
  26. package/dist/contracts/contract-group.d.ts.map +1 -1
  27. package/dist/contracts/contract-group.js +35 -19
  28. package/dist/contracts/contract-group.js.map +1 -1
  29. package/dist/contracts/contract-like.d.ts +4 -4
  30. package/dist/contracts/contract-like.d.ts.map +1 -1
  31. package/dist/contracts/contract-like.js +2 -1
  32. package/dist/contracts/contract-like.js.map +1 -1
  33. package/dist/contracts/index.d.ts +28 -0
  34. package/dist/contracts/index.d.ts.map +1 -1
  35. package/dist/contracts/index.js +12 -0
  36. package/dist/contracts/index.js.map +1 -1
  37. package/dist/contracts/openapi-meta.d.ts +8 -8
  38. package/dist/contracts/openapi-meta.d.ts.map +1 -1
  39. package/dist/contracts/path-template.d.ts +27 -0
  40. package/dist/contracts/path-template.d.ts.map +1 -1
  41. package/dist/contracts/path-template.js +6 -0
  42. package/dist/contracts/path-template.js.map +1 -1
  43. package/dist/contracts/types.d.ts +104 -10
  44. package/dist/contracts/types.d.ts.map +1 -1
  45. package/dist/contracts/types.js +15 -0
  46. package/dist/contracts/types.js.map +1 -1
  47. package/dist/contracts/utils.d.ts +6 -0
  48. package/dist/contracts/utils.d.ts.map +1 -1
  49. package/dist/contracts/utils.js +6 -0
  50. package/dist/contracts/utils.js.map +1 -1
  51. package/dist/domain/entity.d.ts +22 -11
  52. package/dist/domain/entity.d.ts.map +1 -1
  53. package/dist/domain/entity.js +5 -1
  54. package/dist/domain/entity.js.map +1 -1
  55. package/dist/domain/events.d.ts +5 -2
  56. package/dist/domain/events.d.ts.map +1 -1
  57. package/dist/domain/events.js +4 -1
  58. package/dist/domain/events.js.map +1 -1
  59. package/dist/domain/value-object.d.ts +19 -9
  60. package/dist/domain/value-object.d.ts.map +1 -1
  61. package/dist/domain/value-object.js +5 -1
  62. package/dist/domain/value-object.js.map +1 -1
  63. package/dist/errors/catalog.d.ts +40 -16
  64. package/dist/errors/catalog.d.ts.map +1 -1
  65. package/dist/errors/catalog.js +18 -7
  66. package/dist/errors/catalog.js.map +1 -1
  67. package/dist/errors/response.d.ts +16 -4
  68. package/dist/errors/response.d.ts.map +1 -1
  69. package/dist/errors/response.js +3 -3
  70. package/dist/errors/response.js.map +1 -1
  71. package/dist/errors/validation.d.ts +10 -1
  72. package/dist/errors/validation.d.ts.map +1 -1
  73. package/dist/errors/validation.js +3 -0
  74. package/dist/errors/validation.js.map +1 -1
  75. package/dist/events/index.d.ts +133 -0
  76. package/dist/events/index.d.ts.map +1 -1
  77. package/dist/events/index.js +30 -0
  78. package/dist/events/index.js.map +1 -1
  79. package/dist/idempotency/index.d.ts +355 -0
  80. package/dist/idempotency/index.d.ts.map +1 -0
  81. package/dist/idempotency/index.js +360 -0
  82. package/dist/idempotency/index.js.map +1 -0
  83. package/dist/jobs/index.d.ts +248 -4
  84. package/dist/jobs/index.d.ts.map +1 -1
  85. package/dist/jobs/index.js +183 -1
  86. package/dist/jobs/index.js.map +1 -1
  87. package/dist/mail/index.d.ts +149 -0
  88. package/dist/mail/index.d.ts.map +1 -1
  89. package/dist/mail/index.js +30 -0
  90. package/dist/mail/index.js.map +1 -1
  91. package/dist/notifications/index.d.ts +369 -0
  92. package/dist/notifications/index.d.ts.map +1 -0
  93. package/dist/notifications/index.js +310 -0
  94. package/dist/notifications/index.js.map +1 -0
  95. package/dist/openapi/index.d.ts +132 -16
  96. package/dist/openapi/index.d.ts.map +1 -1
  97. package/dist/openapi/index.js +1 -1
  98. package/dist/openapi/index.js.map +1 -1
  99. package/dist/outbox/index.d.ts +474 -0
  100. package/dist/outbox/index.d.ts.map +1 -0
  101. package/dist/outbox/index.js +538 -0
  102. package/dist/outbox/index.js.map +1 -0
  103. package/dist/pagination/index.d.ts +166 -0
  104. package/dist/pagination/index.d.ts.map +1 -0
  105. package/dist/pagination/index.js +96 -0
  106. package/dist/pagination/index.js.map +1 -0
  107. package/dist/ports/audit.d.ts +271 -0
  108. package/dist/ports/audit.d.ts.map +1 -1
  109. package/dist/ports/audit.js +128 -0
  110. package/dist/ports/audit.js.map +1 -1
  111. package/dist/ports/auth.d.ts +70 -0
  112. package/dist/ports/auth.d.ts.map +1 -1
  113. package/dist/ports/auth.js +30 -0
  114. package/dist/ports/auth.js.map +1 -1
  115. package/dist/ports/cache.d.ts +41 -0
  116. package/dist/ports/cache.d.ts.map +1 -1
  117. package/dist/ports/cache.js +10 -0
  118. package/dist/ports/cache.js.map +1 -1
  119. package/dist/ports/clock.d.ts +38 -0
  120. package/dist/ports/clock.d.ts.map +1 -1
  121. package/dist/ports/clock.js +20 -0
  122. package/dist/ports/clock.js.map +1 -1
  123. package/dist/ports/id-generator.d.ts +37 -0
  124. package/dist/ports/id-generator.d.ts.map +1 -1
  125. package/dist/ports/id-generator.js +22 -0
  126. package/dist/ports/id-generator.js.map +1 -1
  127. package/dist/ports/index.d.ts +83 -0
  128. package/dist/ports/index.d.ts.map +1 -1
  129. package/dist/ports/index.js +41 -5
  130. package/dist/ports/index.js.map +1 -1
  131. package/dist/ports/logger.d.ts +56 -0
  132. package/dist/ports/logger.d.ts.map +1 -1
  133. package/dist/ports/logger.js +17 -0
  134. package/dist/ports/logger.js.map +1 -1
  135. package/dist/ports/policy.d.ts +132 -0
  136. package/dist/ports/policy.d.ts.map +1 -1
  137. package/dist/ports/policy.js +45 -0
  138. package/dist/ports/policy.js.map +1 -1
  139. package/dist/ports/rate-limit.d.ts +25 -0
  140. package/dist/ports/rate-limit.d.ts.map +1 -1
  141. package/dist/ports/rate-limit.js +10 -0
  142. package/dist/ports/rate-limit.js.map +1 -1
  143. package/dist/ports/redaction.d.ts +101 -0
  144. package/dist/ports/redaction.d.ts.map +1 -1
  145. package/dist/ports/redaction.js +59 -0
  146. package/dist/ports/redaction.js.map +1 -1
  147. package/dist/ports/storage.d.ts +100 -0
  148. package/dist/ports/storage.d.ts.map +1 -1
  149. package/dist/ports/storage.js +10 -0
  150. package/dist/ports/storage.js.map +1 -1
  151. package/dist/ports/testing.d.ts +47 -0
  152. package/dist/ports/testing.d.ts.map +1 -1
  153. package/dist/ports/testing.js +23 -0
  154. package/dist/ports/testing.js.map +1 -1
  155. package/dist/ports/unit-of-work.d.ts +60 -3
  156. package/dist/ports/unit-of-work.d.ts.map +1 -1
  157. package/dist/ports/unit-of-work.js +11 -2
  158. package/dist/ports/unit-of-work.js.map +1 -1
  159. package/dist/providers/instrumentation.d.ts +205 -1
  160. package/dist/providers/instrumentation.d.ts.map +1 -1
  161. package/dist/providers/instrumentation.js +14 -0
  162. package/dist/providers/instrumentation.js.map +1 -1
  163. package/dist/providers/provider.d.ts +14 -1
  164. package/dist/providers/provider.d.ts.map +1 -1
  165. package/dist/providers/provider.js.map +1 -1
  166. package/dist/schedules/index.d.ts +246 -0
  167. package/dist/schedules/index.d.ts.map +1 -1
  168. package/dist/schedules/index.js +27 -0
  169. package/dist/schedules/index.js.map +1 -1
  170. package/dist/server/health.d.ts +14 -5
  171. package/dist/server/health.d.ts.map +1 -1
  172. package/dist/server/health.js +5 -2
  173. package/dist/server/health.js.map +1 -1
  174. package/dist/server/hooks/auth.d.ts +68 -26
  175. package/dist/server/hooks/auth.d.ts.map +1 -1
  176. package/dist/server/hooks/auth.js +44 -55
  177. package/dist/server/hooks/auth.js.map +1 -1
  178. package/dist/server/hooks/cors.d.ts +27 -0
  179. package/dist/server/hooks/cors.d.ts.map +1 -1
  180. package/dist/server/hooks/cors.js +12 -0
  181. package/dist/server/hooks/cors.js.map +1 -1
  182. package/dist/server/hooks/errors.d.ts +15 -6
  183. package/dist/server/hooks/errors.d.ts.map +1 -1
  184. package/dist/server/hooks/errors.js.map +1 -1
  185. package/dist/server/hooks/index.d.ts +4 -1
  186. package/dist/server/hooks/index.d.ts.map +1 -1
  187. package/dist/server/hooks/index.js +3 -0
  188. package/dist/server/hooks/index.js.map +1 -1
  189. package/dist/server/hooks/logging.d.ts +36 -0
  190. package/dist/server/hooks/logging.d.ts.map +1 -1
  191. package/dist/server/hooks/logging.js +6 -0
  192. package/dist/server/hooks/logging.js.map +1 -1
  193. package/dist/server/hooks/rate-limit.d.ts +33 -0
  194. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  195. package/dist/server/hooks/rate-limit.js +11 -0
  196. package/dist/server/hooks/rate-limit.js.map +1 -1
  197. package/dist/server/http.d.ts +222 -0
  198. package/dist/server/http.d.ts.map +1 -1
  199. package/dist/server/http.js +20 -1
  200. package/dist/server/http.js.map +1 -1
  201. package/dist/server/index.d.ts +19 -1
  202. package/dist/server/index.d.ts.map +1 -1
  203. package/dist/server/index.js +7 -1
  204. package/dist/server/index.js.map +1 -1
  205. package/dist/server/openapi.d.ts +5 -3
  206. package/dist/server/openapi.d.ts.map +1 -1
  207. package/dist/server/openapi.js +4 -2
  208. package/dist/server/openapi.js.map +1 -1
  209. package/dist/server/providers/loadProviderConfig.d.ts +9 -0
  210. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
  211. package/dist/server/providers/loadProviderConfig.js +9 -0
  212. package/dist/server/providers/loadProviderConfig.js.map +1 -1
  213. package/dist/server/server.d.ts +159 -19
  214. package/dist/server/server.d.ts.map +1 -1
  215. package/dist/server/server.js +72 -31
  216. package/dist/server/server.js.map +1 -1
  217. package/dist/testing/index.d.ts +171 -0
  218. package/dist/testing/index.d.ts.map +1 -0
  219. package/dist/testing/index.js +127 -0
  220. package/dist/testing/index.js.map +1 -0
  221. package/dist/uploads/client.d.ts +278 -0
  222. package/dist/uploads/client.d.ts.map +1 -0
  223. package/dist/uploads/client.js +428 -0
  224. package/dist/uploads/client.js.map +1 -0
  225. package/dist/uploads/index.d.ts +361 -0
  226. package/dist/uploads/index.d.ts.map +1 -0
  227. package/dist/uploads/index.js +543 -0
  228. package/dist/uploads/index.js.map +1 -0
  229. package/package.json +31 -2
  230. package/src/application/index.ts +85 -22
  231. package/src/client/client.ts +73 -12
  232. package/src/client/index.ts +12 -0
  233. package/src/client/types.ts +70 -9
  234. package/src/config/index.ts +86 -0
  235. package/src/contracts/contract-builder.ts +49 -22
  236. package/src/contracts/contract-group.ts +35 -19
  237. package/src/contracts/contract-like.ts +4 -4
  238. package/src/contracts/index.ts +28 -1
  239. package/src/contracts/openapi-meta.ts +8 -8
  240. package/src/contracts/path-template.ts +27 -0
  241. package/src/contracts/types.ts +111 -10
  242. package/src/contracts/utils.ts +6 -0
  243. package/src/domain/entity.ts +22 -11
  244. package/src/domain/events.ts +5 -2
  245. package/src/domain/value-object.ts +19 -9
  246. package/src/errors/catalog.ts +40 -16
  247. package/src/errors/response.ts +16 -4
  248. package/src/errors/validation.ts +10 -1
  249. package/src/events/index.ts +134 -0
  250. package/src/idempotency/index.ts +767 -0
  251. package/src/jobs/index.ts +437 -5
  252. package/src/mail/index.ts +149 -0
  253. package/src/notifications/index.ts +771 -0
  254. package/src/openapi/index.ts +133 -16
  255. package/src/outbox/index.ts +1104 -0
  256. package/src/pagination/index.ts +278 -0
  257. package/src/ports/audit.ts +271 -0
  258. package/src/ports/auth.ts +70 -0
  259. package/src/ports/cache.ts +41 -0
  260. package/src/ports/clock.ts +38 -0
  261. package/src/ports/id-generator.ts +37 -0
  262. package/src/ports/index.ts +106 -11
  263. package/src/ports/logger.ts +56 -0
  264. package/src/ports/policy.ts +133 -0
  265. package/src/ports/rate-limit.ts +25 -0
  266. package/src/ports/redaction.ts +101 -0
  267. package/src/ports/storage.ts +100 -0
  268. package/src/ports/testing.ts +47 -0
  269. package/src/ports/unit-of-work.ts +60 -3
  270. package/src/providers/instrumentation.ts +211 -1
  271. package/src/providers/provider.ts +14 -1
  272. package/src/schedules/index.ts +247 -0
  273. package/src/server/health.ts +14 -5
  274. package/src/server/hooks/auth.ts +105 -120
  275. package/src/server/hooks/cors.ts +27 -0
  276. package/src/server/hooks/errors.ts +15 -6
  277. package/src/server/hooks/index.ts +4 -5
  278. package/src/server/hooks/logging.ts +36 -0
  279. package/src/server/hooks/rate-limit.ts +33 -0
  280. package/src/server/http.ts +249 -1
  281. package/src/server/index.ts +19 -1
  282. package/src/server/openapi.ts +5 -3
  283. package/src/server/providers/loadProviderConfig.ts +9 -0
  284. package/src/server/server.ts +296 -30
  285. package/src/testing/index.ts +348 -0
  286. package/src/uploads/client.ts +861 -0
  287. package/src/uploads/index.ts +1067 -0
@@ -0,0 +1,1104 @@
1
+ /**
2
+ * @beignet/core/outbox
3
+ *
4
+ * Durable outbox primitives for transactionally recording events and jobs that
5
+ * should be delivered after the owning database transaction commits.
6
+ */
7
+
8
+ import {
9
+ type EventPayloadDef,
10
+ type InferEventPayload,
11
+ parseEventPayload,
12
+ } from "../events";
13
+ import {
14
+ getJobRetryDelayMs,
15
+ getJobRetryMaxAttempts,
16
+ type InferJobPayload,
17
+ type JobDef,
18
+ parseJobPayload,
19
+ shouldRetryJob,
20
+ } from "../jobs";
21
+ import type { JobDispatcherPort } from "../ports/events";
22
+ import type {
23
+ BufferedDomainEventRecorder,
24
+ DomainEventRecorderPort,
25
+ } from "../ports/unit-of-work";
26
+ import {
27
+ createProviderInstrumentation,
28
+ type ProviderInstrumentationTarget,
29
+ } from "../providers";
30
+
31
+ /**
32
+ * Value or promise of that value.
33
+ */
34
+ export type MaybePromise<T> = T | Promise<T>;
35
+
36
+ /**
37
+ * Default lease duration for claimed outbox messages.
38
+ */
39
+ export const DEFAULT_OUTBOX_LEASE_MS = 30_000;
40
+ /**
41
+ * Default maximum delivery attempts before a message is dead-lettered.
42
+ */
43
+ export const DEFAULT_OUTBOX_MAX_ATTEMPTS = 3;
44
+
45
+ /**
46
+ * Message kinds supported by the Beignet outbox.
47
+ */
48
+ export type OutboxMessageKind = "event" | "job";
49
+
50
+ /**
51
+ * Delivery status for an outbox message.
52
+ */
53
+ export type OutboxMessageStatus =
54
+ | "pending"
55
+ | "claimed"
56
+ | "delivered"
57
+ | "deadLettered";
58
+
59
+ /**
60
+ * JSON-serializable value accepted by the outbox.
61
+ */
62
+ export type OutboxJsonValue =
63
+ | null
64
+ | string
65
+ | number
66
+ | boolean
67
+ | readonly OutboxJsonValue[]
68
+ | { readonly [key: string]: OutboxJsonValue };
69
+
70
+ /**
71
+ * JSON object accepted by outbox payload helpers.
72
+ */
73
+ export type OutboxJsonObject = { readonly [key: string]: OutboxJsonValue };
74
+
75
+ /**
76
+ * Serialized delivery error stored on failed messages.
77
+ */
78
+ export interface OutboxErrorInfo {
79
+ /**
80
+ * Error name when available.
81
+ */
82
+ name?: string;
83
+ /**
84
+ * Error message.
85
+ */
86
+ message: string;
87
+ /**
88
+ * Error stack when available.
89
+ */
90
+ stack?: string;
91
+ }
92
+
93
+ /**
94
+ * Input for enqueueing a raw outbox message.
95
+ */
96
+ export interface OutboxEnqueueInput {
97
+ /**
98
+ * Optional caller-provided message ID.
99
+ */
100
+ id?: string;
101
+ /**
102
+ * Message kind.
103
+ */
104
+ kind: OutboxMessageKind;
105
+ /**
106
+ * Event or job name.
107
+ */
108
+ name: string;
109
+ /**
110
+ * JSON-serializable payload.
111
+ */
112
+ payload: OutboxJsonValue;
113
+ /**
114
+ * Earliest time the message may be claimed.
115
+ */
116
+ availableAt?: Date;
117
+ /**
118
+ * Maximum delivery attempts before dead-lettering.
119
+ */
120
+ maxAttempts?: number;
121
+ }
122
+
123
+ /**
124
+ * Durable outbox message record.
125
+ */
126
+ export interface OutboxMessage {
127
+ /**
128
+ * Stable message ID.
129
+ */
130
+ id: string;
131
+ /**
132
+ * Message kind.
133
+ */
134
+ kind: OutboxMessageKind;
135
+ /**
136
+ * Event or job name.
137
+ */
138
+ name: string;
139
+ /**
140
+ * JSON-serializable payload.
141
+ */
142
+ payload: OutboxJsonValue;
143
+ /**
144
+ * Current delivery status.
145
+ */
146
+ status: OutboxMessageStatus;
147
+ /**
148
+ * Number of claim attempts.
149
+ */
150
+ attempts: number;
151
+ /**
152
+ * Maximum delivery attempts before dead-lettering.
153
+ */
154
+ maxAttempts: number;
155
+ /**
156
+ * Earliest time the message may be claimed.
157
+ */
158
+ availableAt: Date;
159
+ /**
160
+ * Last claim timestamp.
161
+ */
162
+ claimedAt: Date | null;
163
+ /**
164
+ * Lease expiration timestamp for claimed messages.
165
+ */
166
+ lockedUntil: Date | null;
167
+ /**
168
+ * Token required to mark a claimed message delivered or failed.
169
+ */
170
+ claimToken: string | null;
171
+ /**
172
+ * Delivery timestamp.
173
+ */
174
+ deliveredAt: Date | null;
175
+ /**
176
+ * Last delivery error.
177
+ */
178
+ lastError: OutboxErrorInfo | null;
179
+ /**
180
+ * Creation timestamp.
181
+ */
182
+ createdAt: Date;
183
+ /**
184
+ * Last update timestamp.
185
+ */
186
+ updatedAt: Date;
187
+ }
188
+
189
+ /**
190
+ * Message returned from a successful outbox claim.
191
+ */
192
+ export interface ClaimedOutboxMessage
193
+ extends Omit<
194
+ OutboxMessage,
195
+ "claimToken" | "claimedAt" | "lockedUntil" | "status"
196
+ > {
197
+ status: "claimed";
198
+ claimToken: string;
199
+ claimedAt: Date;
200
+ lockedUntil: Date;
201
+ }
202
+
203
+ /**
204
+ * Options for leasing a batch of pending outbox messages for delivery.
205
+ */
206
+ export interface OutboxClaimBatchOptions {
207
+ /**
208
+ * Maximum messages to claim in one batch.
209
+ */
210
+ limit: number;
211
+ /**
212
+ * Claim timestamp.
213
+ */
214
+ now?: Date;
215
+ /**
216
+ * Lease duration in milliseconds.
217
+ */
218
+ leaseMs?: number;
219
+ }
220
+
221
+ /**
222
+ * Input for marking a claimed message delivered.
223
+ */
224
+ export interface OutboxMarkDeliveredInput {
225
+ /**
226
+ * Claimed message ID.
227
+ */
228
+ id: string;
229
+ /**
230
+ * Claim token returned by `claimBatch(...)`.
231
+ */
232
+ claimToken: string;
233
+ /**
234
+ * Delivery timestamp.
235
+ */
236
+ now?: Date;
237
+ }
238
+
239
+ /**
240
+ * Input for marking a claimed message failed.
241
+ */
242
+ export interface OutboxMarkFailedInput {
243
+ /**
244
+ * Claimed message ID.
245
+ */
246
+ id: string;
247
+ /**
248
+ * Claim token returned by `claimBatch(...)`.
249
+ */
250
+ claimToken: string;
251
+ /**
252
+ * Delivery error.
253
+ */
254
+ error?: unknown;
255
+ /**
256
+ * Next time the message may be claimed.
257
+ */
258
+ retryAt?: Date;
259
+ /**
260
+ * Whether this failure should dead-letter the message.
261
+ */
262
+ deadLetter?: boolean;
263
+ /**
264
+ * Failure timestamp.
265
+ */
266
+ now?: Date;
267
+ }
268
+
269
+ /**
270
+ * App-facing outbox storage port.
271
+ *
272
+ * Durable adapters should claim messages atomically and require `claimToken`
273
+ * for delivery/failure updates.
274
+ */
275
+ export interface OutboxPort {
276
+ /**
277
+ * Enqueue a new pending message.
278
+ */
279
+ enqueue(input: OutboxEnqueueInput): Promise<OutboxMessage>;
280
+ /**
281
+ * Atomically claim eligible messages for one worker.
282
+ */
283
+ claimBatch(options: OutboxClaimBatchOptions): Promise<ClaimedOutboxMessage[]>;
284
+ /**
285
+ * Mark a claimed message delivered.
286
+ */
287
+ markDelivered(input: OutboxMarkDeliveredInput): Promise<void>;
288
+ /**
289
+ * Mark a claimed message failed, retryable, or dead-lettered.
290
+ */
291
+ markFailed(input: OutboxMarkFailedInput): Promise<void>;
292
+ }
293
+
294
+ /**
295
+ * In-memory outbox for tests and local examples.
296
+ */
297
+ export interface MemoryOutboxPort extends OutboxPort {
298
+ /**
299
+ * Current message snapshots.
300
+ */
301
+ readonly messages: readonly OutboxMessage[];
302
+ /**
303
+ * Remove all messages.
304
+ */
305
+ clear(): void;
306
+ }
307
+
308
+ /**
309
+ * Options for `createOutboxMessage(...)`.
310
+ */
311
+ export interface CreateOutboxMessageOptions {
312
+ /**
313
+ * Generated message ID override.
314
+ */
315
+ id?: string;
316
+ /**
317
+ * Timestamp used for created/updated/available dates.
318
+ */
319
+ now?: Date;
320
+ }
321
+
322
+ /**
323
+ * Options for typed event/job enqueue helpers.
324
+ */
325
+ export interface EnqueueTypedOutboxOptions {
326
+ /**
327
+ * Optional caller-provided message ID.
328
+ */
329
+ id?: string;
330
+ /**
331
+ * Earliest time the message may be claimed.
332
+ */
333
+ availableAt?: Date;
334
+ /**
335
+ * Maximum delivery attempts before dead-lettering.
336
+ */
337
+ maxAttempts?: number;
338
+ }
339
+
340
+ /**
341
+ * Registry of definitions that `drainOutbox(...)` can deliver.
342
+ */
343
+ export interface OutboxRegistry {
344
+ /**
345
+ * Event definitions keyed by event name.
346
+ */
347
+ readonly events: ReadonlyMap<string, EventPayloadDef>;
348
+ /**
349
+ * Job definitions keyed by job name.
350
+ */
351
+ readonly jobs: ReadonlyMap<string, JobDef>;
352
+ }
353
+
354
+ /**
355
+ * Input for defining an outbox registry.
356
+ */
357
+ export interface DefineOutboxRegistryInput {
358
+ /**
359
+ * Events that may be delivered from the outbox.
360
+ */
361
+ events?: readonly EventPayloadDef[];
362
+ /**
363
+ * Jobs that may be delivered from the outbox.
364
+ */
365
+ jobs?: readonly JobDef[];
366
+ }
367
+
368
+ /**
369
+ * Options for draining one outbox batch.
370
+ */
371
+ export interface DrainOutboxOptions {
372
+ /**
373
+ * Outbox storage port.
374
+ */
375
+ outbox: OutboxPort;
376
+ /**
377
+ * Registry used to resolve message names to event/job definitions.
378
+ */
379
+ registry: OutboxRegistry;
380
+ /**
381
+ * Event bus used for event messages.
382
+ */
383
+ eventBus?: {
384
+ publish<E extends EventPayloadDef>(
385
+ event: E,
386
+ payload: InferEventPayload<E>,
387
+ ): MaybePromise<void>;
388
+ };
389
+ /**
390
+ * Job dispatcher used for job messages.
391
+ */
392
+ jobs?: JobDispatcherPort;
393
+ /**
394
+ * Maximum messages to claim in one drain pass.
395
+ */
396
+ batchSize?: number;
397
+ /**
398
+ * Timestamp used for claiming and state updates.
399
+ */
400
+ now?: Date;
401
+ /**
402
+ * Claim lease duration in milliseconds.
403
+ */
404
+ leaseMs?: number;
405
+ /**
406
+ * Retry delay in milliseconds or function for per-message delay.
407
+ */
408
+ retryDelayMs?:
409
+ | number
410
+ | ((args: {
411
+ message: ClaimedOutboxMessage;
412
+ error: unknown;
413
+ now: Date;
414
+ }) => number);
415
+ /**
416
+ * Optional instrumentation target for retry and dead-letter visibility.
417
+ */
418
+ instrumentation?: ProviderInstrumentationTarget;
419
+ /**
420
+ * Observer called when delivery fails. Observer failures are ignored so the
421
+ * original delivery failure still controls retry/dead-letter behavior.
422
+ */
423
+ onError?: (
424
+ error: unknown,
425
+ message: ClaimedOutboxMessage,
426
+ ) => MaybePromise<void>;
427
+ }
428
+
429
+ /**
430
+ * Summary returned from one `drainOutbox(...)` pass.
431
+ */
432
+ export interface DrainOutboxResult {
433
+ /**
434
+ * Messages claimed in this batch.
435
+ */
436
+ claimed: number;
437
+ /**
438
+ * Messages delivered successfully.
439
+ */
440
+ delivered: number;
441
+ /**
442
+ * Messages scheduled for retry.
443
+ */
444
+ retried: number;
445
+ /**
446
+ * Messages moved to dead letter state.
447
+ */
448
+ deadLettered: number;
449
+ }
450
+
451
+ /**
452
+ * Error thrown when an outbox payload is not JSON serializable.
453
+ */
454
+ export class OutboxSerializationError extends Error {
455
+ constructor(message: string) {
456
+ super(message);
457
+ this.name = "OutboxSerializationError";
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Error thrown when an outbox message cannot be resolved through the registry.
463
+ */
464
+ export class OutboxRegistryError extends Error {
465
+ constructor(message: string) {
466
+ super(message);
467
+ this.name = "OutboxRegistryError";
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Error thrown when a claimed message cannot be updated with the supplied token.
473
+ */
474
+ export class OutboxClaimError extends Error {
475
+ /**
476
+ * Message ID involved in the claim error.
477
+ */
478
+ readonly id: string;
479
+
480
+ constructor(args: { id: string; message: string }) {
481
+ super(args.message);
482
+ this.name = "OutboxClaimError";
483
+ this.id = args.id;
484
+ }
485
+ }
486
+
487
+ function assertNonEmptyString(name: string, value: string): void {
488
+ if (typeof value !== "string" || value.trim().length === 0) {
489
+ throw new Error(`${name} must be a non-empty string`);
490
+ }
491
+ }
492
+
493
+ function assertPositiveInteger(name: string, value: number): void {
494
+ if (!Number.isInteger(value) || value <= 0) {
495
+ throw new Error(`${name} must be a positive integer`);
496
+ }
497
+ }
498
+
499
+ function cloneDate(value: Date): Date {
500
+ return new Date(value.getTime());
501
+ }
502
+
503
+ function createId(): string {
504
+ if (!globalThis.crypto?.randomUUID) {
505
+ throw new Error("crypto.randomUUID is required to create outbox IDs.");
506
+ }
507
+
508
+ return globalThis.crypto.randomUUID();
509
+ }
510
+
511
+ function assertJsonValue(
512
+ value: unknown,
513
+ path: readonly string[] = [],
514
+ seen: WeakSet<object> = new WeakSet(),
515
+ ): OutboxJsonValue {
516
+ const label = path.length > 0 ? path.join(".") : "payload";
517
+
518
+ if (value === null) return null;
519
+
520
+ if (
521
+ typeof value === "string" ||
522
+ typeof value === "boolean" ||
523
+ typeof value === "number"
524
+ ) {
525
+ if (typeof value === "number" && !Number.isFinite(value)) {
526
+ throw new OutboxSerializationError(
527
+ `Outbox ${label} must be a finite number.`,
528
+ );
529
+ }
530
+ return value;
531
+ }
532
+
533
+ if (Array.isArray(value)) {
534
+ if (seen.has(value)) {
535
+ throw new OutboxSerializationError(
536
+ `Outbox ${label} must be JSON serializable. Circular references are not supported.`,
537
+ );
538
+ }
539
+ seen.add(value);
540
+ try {
541
+ return value.map((item, index) =>
542
+ assertJsonValue(item, [...path, String(index)], seen),
543
+ );
544
+ } finally {
545
+ seen.delete(value);
546
+ }
547
+ }
548
+
549
+ if (typeof value === "object") {
550
+ if (value instanceof Date) {
551
+ throw new OutboxSerializationError(
552
+ `Outbox ${label} must be JSON serializable. Convert Date values to strings before enqueueing.`,
553
+ );
554
+ }
555
+ if (seen.has(value)) {
556
+ throw new OutboxSerializationError(
557
+ `Outbox ${label} must be JSON serializable. Circular references are not supported.`,
558
+ );
559
+ }
560
+
561
+ seen.add(value);
562
+ try {
563
+ const record = value as Record<string, unknown>;
564
+ const output: Record<string, OutboxJsonValue> = {};
565
+ for (const key of Object.keys(record)) {
566
+ const child = record[key];
567
+ if (child === undefined) {
568
+ throw new OutboxSerializationError(
569
+ `Outbox ${[...path, key].join(".")} cannot be undefined.`,
570
+ );
571
+ }
572
+ output[key] = assertJsonValue(child, [...path, key], seen);
573
+ }
574
+ return output;
575
+ } finally {
576
+ seen.delete(value);
577
+ }
578
+ }
579
+
580
+ throw new OutboxSerializationError(
581
+ `Outbox ${label} must be JSON serializable. Received ${typeof value}.`,
582
+ );
583
+ }
584
+
585
+ /**
586
+ * Convert an unknown value to an outbox-safe JSON value.
587
+ *
588
+ * Dates, undefined values, functions, non-finite numbers, symbols, and circular
589
+ * references are rejected so durable adapters can store the payload safely.
590
+ */
591
+ export function toOutboxJsonValue(value: unknown): OutboxJsonValue {
592
+ const jsonValue = assertJsonValue(value);
593
+ return JSON.parse(JSON.stringify(jsonValue)) as OutboxJsonValue;
594
+ }
595
+
596
+ /**
597
+ * Serialize an unknown delivery error into outbox error metadata.
598
+ */
599
+ export function serializeOutboxError(error: unknown): OutboxErrorInfo {
600
+ if (error instanceof Error) {
601
+ return {
602
+ name: error.name,
603
+ message: error.message,
604
+ stack: error.stack,
605
+ };
606
+ }
607
+
608
+ if (typeof error === "string") {
609
+ return { message: error };
610
+ }
611
+
612
+ return { message: "Unknown outbox delivery error" };
613
+ }
614
+
615
+ function copyMessage(message: OutboxMessage): OutboxMessage {
616
+ return {
617
+ ...message,
618
+ availableAt: cloneDate(message.availableAt),
619
+ claimedAt: message.claimedAt ? cloneDate(message.claimedAt) : null,
620
+ lockedUntil: message.lockedUntil ? cloneDate(message.lockedUntil) : null,
621
+ deliveredAt: message.deliveredAt ? cloneDate(message.deliveredAt) : null,
622
+ createdAt: cloneDate(message.createdAt),
623
+ updatedAt: cloneDate(message.updatedAt),
624
+ lastError: message.lastError ? { ...message.lastError } : null,
625
+ };
626
+ }
627
+
628
+ function toClaimedMessage(message: OutboxMessage): ClaimedOutboxMessage {
629
+ if (
630
+ message.status !== "claimed" ||
631
+ !message.claimToken ||
632
+ !message.claimedAt ||
633
+ !message.lockedUntil
634
+ ) {
635
+ throw new OutboxClaimError({
636
+ id: message.id,
637
+ message: `Outbox message "${message.id}" is not claimed.`,
638
+ });
639
+ }
640
+
641
+ return {
642
+ ...copyMessage(message),
643
+ status: "claimed",
644
+ claimToken: message.claimToken,
645
+ claimedAt: cloneDate(message.claimedAt),
646
+ lockedUntil: cloneDate(message.lockedUntil),
647
+ };
648
+ }
649
+
650
+ /**
651
+ * Create a validated pending outbox message.
652
+ */
653
+ export function createOutboxMessage(
654
+ input: OutboxEnqueueInput,
655
+ options: CreateOutboxMessageOptions = {},
656
+ ): OutboxMessage {
657
+ assertNonEmptyString("kind", input.kind);
658
+ assertNonEmptyString("name", input.name);
659
+ if (input.id !== undefined) assertNonEmptyString("id", input.id);
660
+ if (input.maxAttempts !== undefined) {
661
+ assertPositiveInteger("maxAttempts", input.maxAttempts);
662
+ }
663
+
664
+ const now = options.now ?? new Date();
665
+ return {
666
+ id: options.id ?? input.id ?? createId(),
667
+ kind: input.kind,
668
+ name: input.name,
669
+ payload: toOutboxJsonValue(input.payload),
670
+ status: "pending",
671
+ attempts: 0,
672
+ maxAttempts: input.maxAttempts ?? DEFAULT_OUTBOX_MAX_ATTEMPTS,
673
+ availableAt: input.availableAt
674
+ ? cloneDate(input.availableAt)
675
+ : cloneDate(now),
676
+ claimedAt: null,
677
+ lockedUntil: null,
678
+ claimToken: null,
679
+ deliveredAt: null,
680
+ lastError: null,
681
+ createdAt: cloneDate(now),
682
+ updatedAt: cloneDate(now),
683
+ };
684
+ }
685
+
686
+ function isEligible(message: OutboxMessage, now: Date): boolean {
687
+ if (message.status === "pending") {
688
+ return message.availableAt.getTime() <= now.getTime();
689
+ }
690
+
691
+ return (
692
+ message.status === "claimed" &&
693
+ message.lockedUntil !== null &&
694
+ message.lockedUntil.getTime() <= now.getTime()
695
+ );
696
+ }
697
+
698
+ /**
699
+ * Create an in-memory outbox for tests and local examples.
700
+ *
701
+ * The memory outbox is process-local and not durable.
702
+ */
703
+ export function createMemoryOutbox(): MemoryOutboxPort {
704
+ const messages = new Map<string, OutboxMessage>();
705
+
706
+ function getClaimedOrThrow(id: string, claimToken: string): OutboxMessage {
707
+ const message = messages.get(id);
708
+ if (!message) {
709
+ throw new OutboxClaimError({
710
+ id,
711
+ message: `Outbox message "${id}" does not exist.`,
712
+ });
713
+ }
714
+ if (message.status !== "claimed" || message.claimToken !== claimToken) {
715
+ throw new OutboxClaimError({
716
+ id,
717
+ message: `Outbox message "${id}" is not claimed by this worker.`,
718
+ });
719
+ }
720
+ return message;
721
+ }
722
+
723
+ return {
724
+ get messages() {
725
+ return [...messages.values()].map(copyMessage);
726
+ },
727
+
728
+ async enqueue(input) {
729
+ const message = createOutboxMessage(input);
730
+ if (messages.has(message.id)) {
731
+ throw new Error(`Outbox message "${message.id}" already exists.`);
732
+ }
733
+ messages.set(message.id, message);
734
+ return copyMessage(message);
735
+ },
736
+
737
+ async claimBatch(options) {
738
+ assertPositiveInteger("limit", options.limit);
739
+ const now = options.now ?? new Date();
740
+ const leaseMs = options.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
741
+ assertPositiveInteger("leaseMs", leaseMs);
742
+ const lockedUntil = new Date(now.getTime() + leaseMs);
743
+ const claimed: ClaimedOutboxMessage[] = [];
744
+
745
+ const eligible = [...messages.values()]
746
+ .filter((message) => isEligible(message, now))
747
+ .sort((a, b) => {
748
+ const available = a.availableAt.getTime() - b.availableAt.getTime();
749
+ if (available !== 0) return available;
750
+ return a.createdAt.getTime() - b.createdAt.getTime();
751
+ })
752
+ .slice(0, options.limit);
753
+
754
+ for (const message of eligible) {
755
+ message.status = "claimed";
756
+ message.attempts += 1;
757
+ message.claimToken = createId();
758
+ message.claimedAt = cloneDate(now);
759
+ message.lockedUntil = cloneDate(lockedUntil);
760
+ message.updatedAt = cloneDate(now);
761
+ claimed.push(toClaimedMessage(message));
762
+ }
763
+
764
+ return claimed;
765
+ },
766
+
767
+ async markDelivered(input) {
768
+ assertNonEmptyString("id", input.id);
769
+ assertNonEmptyString("claimToken", input.claimToken);
770
+ const message = getClaimedOrThrow(input.id, input.claimToken);
771
+ const now = input.now ?? new Date();
772
+
773
+ message.status = "delivered";
774
+ message.deliveredAt = cloneDate(now);
775
+ message.claimToken = null;
776
+ message.claimedAt = null;
777
+ message.lockedUntil = null;
778
+ message.updatedAt = cloneDate(now);
779
+ },
780
+
781
+ async markFailed(input) {
782
+ assertNonEmptyString("id", input.id);
783
+ assertNonEmptyString("claimToken", input.claimToken);
784
+ const message = getClaimedOrThrow(input.id, input.claimToken);
785
+ const now = input.now ?? new Date();
786
+
787
+ message.status = input.deadLetter ? "deadLettered" : "pending";
788
+ message.lastError = serializeOutboxError(input.error);
789
+ message.availableAt = input.retryAt
790
+ ? cloneDate(input.retryAt)
791
+ : cloneDate(now);
792
+ message.claimToken = null;
793
+ message.claimedAt = null;
794
+ message.lockedUntil = null;
795
+ message.updatedAt = cloneDate(now);
796
+ },
797
+
798
+ clear() {
799
+ messages.clear();
800
+ },
801
+ };
802
+ }
803
+
804
+ function mapDefinitions<T extends { name: string }>(
805
+ kind: string,
806
+ defs: readonly T[],
807
+ ): ReadonlyMap<string, T> {
808
+ const map = new Map<string, T>();
809
+ for (const def of defs) {
810
+ if (map.has(def.name)) {
811
+ throw new OutboxRegistryError(
812
+ `Duplicate ${kind} definition "${def.name}" in outbox registry.`,
813
+ );
814
+ }
815
+ map.set(def.name, def);
816
+ }
817
+ return map;
818
+ }
819
+
820
+ /**
821
+ * Define the events and jobs that an outbox drain worker can deliver.
822
+ *
823
+ * Duplicate names throw because message delivery resolves by name.
824
+ */
825
+ export function defineOutboxRegistry(
826
+ input: DefineOutboxRegistryInput,
827
+ ): OutboxRegistry {
828
+ return {
829
+ events: mapDefinitions("event", input.events ?? []),
830
+ jobs: mapDefinitions("job", input.jobs ?? []),
831
+ };
832
+ }
833
+
834
+ /**
835
+ * Validate an event payload and enqueue it as an outbox message.
836
+ */
837
+ export async function enqueueEvent<E extends EventPayloadDef>(
838
+ outbox: OutboxPort,
839
+ event: E,
840
+ payload: InferEventPayload<E>,
841
+ options: EnqueueTypedOutboxOptions = {},
842
+ ): Promise<OutboxMessage> {
843
+ const parsed = await parseEventPayload(event, payload);
844
+ return outbox.enqueue({
845
+ id: options.id,
846
+ kind: "event",
847
+ name: event.name,
848
+ payload: toOutboxJsonValue(parsed),
849
+ availableAt: options.availableAt,
850
+ maxAttempts: options.maxAttempts,
851
+ });
852
+ }
853
+
854
+ /**
855
+ * Validate a job payload and enqueue it as an outbox message.
856
+ */
857
+ export async function enqueueJob<J extends JobDef>(
858
+ outbox: OutboxPort,
859
+ job: J,
860
+ payload: InferJobPayload<J>,
861
+ options: EnqueueTypedOutboxOptions = {},
862
+ ): Promise<OutboxMessage> {
863
+ const parsed = await parseJobPayload(job, payload);
864
+ return outbox.enqueue({
865
+ id: options.id,
866
+ kind: "job",
867
+ name: job.name,
868
+ payload: toOutboxJsonValue(parsed),
869
+ availableAt: options.availableAt,
870
+ maxAttempts: options.maxAttempts ?? getJobRetryMaxAttempts(job.retry),
871
+ });
872
+ }
873
+
874
+ /**
875
+ * Create a domain event recorder that writes events to the outbox.
876
+ */
877
+ export function createOutboxEventRecorder(
878
+ outbox: OutboxPort,
879
+ options: EnqueueTypedOutboxOptions = {},
880
+ ): BufferedDomainEventRecorder {
881
+ return {
882
+ async record(event, payload) {
883
+ await enqueueEvent(outbox, event, payload, options);
884
+ },
885
+ entries() {
886
+ return [];
887
+ },
888
+ clear() {},
889
+ async flush() {},
890
+ };
891
+ }
892
+
893
+ /**
894
+ * Create a job dispatcher that writes jobs to the outbox.
895
+ */
896
+ export function createOutboxJobDispatcher(
897
+ outbox: OutboxPort,
898
+ options: EnqueueTypedOutboxOptions = {},
899
+ ): JobDispatcherPort {
900
+ return {
901
+ async dispatch(job, payload) {
902
+ await enqueueJob(outbox, job, payload, options);
903
+ },
904
+ };
905
+ }
906
+
907
+ function resolveRetryDelayMs(
908
+ options: DrainOutboxOptions,
909
+ message: ClaimedOutboxMessage,
910
+ error: unknown,
911
+ now: Date,
912
+ ): number {
913
+ if (typeof options.retryDelayMs === "function") {
914
+ const delay = options.retryDelayMs({ message, error, now });
915
+ assertPositiveInteger("retryDelayMs", delay);
916
+ return delay;
917
+ }
918
+
919
+ if (options.retryDelayMs !== undefined) {
920
+ assertPositiveInteger("retryDelayMs", options.retryDelayMs);
921
+ return options.retryDelayMs;
922
+ }
923
+
924
+ if (message.kind === "job") {
925
+ const job = options.registry.jobs.get(message.name);
926
+ if (job?.retry) {
927
+ return getJobRetryDelayMs(job.retry, {
928
+ attempt: message.attempts,
929
+ error,
930
+ jobName: message.name,
931
+ });
932
+ }
933
+ }
934
+
935
+ return Math.min(60_000, 1000 * 2 ** Math.max(0, message.attempts - 1));
936
+ }
937
+
938
+ function shouldRetryOutboxMessage(
939
+ options: DrainOutboxOptions,
940
+ message: ClaimedOutboxMessage,
941
+ error: unknown,
942
+ ): boolean {
943
+ if (message.kind !== "job") {
944
+ return message.attempts < message.maxAttempts;
945
+ }
946
+
947
+ const job = options.registry.jobs.get(message.name);
948
+ return shouldRetryJob(job?.retry, {
949
+ attempt: message.attempts,
950
+ error,
951
+ jobName: message.name,
952
+ maxAttempts: message.maxAttempts,
953
+ });
954
+ }
955
+
956
+ async function deliverOutboxMessage(
957
+ options: DrainOutboxOptions,
958
+ message: ClaimedOutboxMessage,
959
+ ): Promise<void> {
960
+ if (message.kind === "event") {
961
+ if (!options.eventBus) {
962
+ throw new OutboxRegistryError(
963
+ `Cannot deliver event "${message.name}" without an event bus.`,
964
+ );
965
+ }
966
+
967
+ const event = options.registry.events.get(message.name);
968
+ if (!event) {
969
+ throw new OutboxRegistryError(
970
+ `Outbox registry does not include event "${message.name}".`,
971
+ );
972
+ }
973
+
974
+ const payload = await parseEventPayload(event, message.payload);
975
+ await options.eventBus.publish(event, payload);
976
+ return;
977
+ }
978
+
979
+ if (!options.jobs) {
980
+ throw new OutboxRegistryError(
981
+ `Cannot deliver job "${message.name}" without a job dispatcher.`,
982
+ );
983
+ }
984
+
985
+ const job = options.registry.jobs.get(message.name);
986
+ if (!job) {
987
+ throw new OutboxRegistryError(
988
+ `Outbox registry does not include job "${message.name}".`,
989
+ );
990
+ }
991
+
992
+ const payload = await parseJobPayload(job, message.payload);
993
+ await options.jobs.dispatch(job, payload);
994
+ }
995
+
996
+ /**
997
+ * Claim and deliver one batch of outbox messages.
998
+ *
999
+ * This does not loop forever; production workers should call it on their own
1000
+ * polling cadence. Event and job messages require matching registry entries.
1001
+ * Failed messages are retried with backoff until `maxAttempts`, then
1002
+ * dead-lettered.
1003
+ */
1004
+ export async function drainOutbox(
1005
+ options: DrainOutboxOptions,
1006
+ ): Promise<DrainOutboxResult> {
1007
+ const batchSize = options.batchSize ?? 100;
1008
+ assertPositiveInteger("batchSize", batchSize);
1009
+ const instrumentation = createProviderInstrumentation(
1010
+ options.instrumentation,
1011
+ {
1012
+ providerName: "outbox",
1013
+ watcher: "jobs",
1014
+ },
1015
+ );
1016
+
1017
+ const now = options.now ?? new Date();
1018
+ const messages = await options.outbox.claimBatch({
1019
+ limit: batchSize,
1020
+ now,
1021
+ leaseMs: options.leaseMs,
1022
+ });
1023
+ const result: DrainOutboxResult = {
1024
+ claimed: messages.length,
1025
+ delivered: 0,
1026
+ retried: 0,
1027
+ deadLettered: 0,
1028
+ };
1029
+
1030
+ for (const message of messages) {
1031
+ try {
1032
+ await deliverOutboxMessage(options, message);
1033
+ await options.outbox.markDelivered({
1034
+ id: message.id,
1035
+ claimToken: message.claimToken,
1036
+ now,
1037
+ });
1038
+ result.delivered += 1;
1039
+ } catch (error) {
1040
+ try {
1041
+ await options.onError?.(error, message);
1042
+ } catch {
1043
+ // Preserve the delivery failure path so the message is retried or
1044
+ // dead-lettered even if the observer fails.
1045
+ }
1046
+ const shouldRetry = shouldRetryOutboxMessage(options, message, error);
1047
+ const deadLetter = !shouldRetry;
1048
+ const retryDelayMs = deadLetter
1049
+ ? 0
1050
+ : resolveRetryDelayMs(options, message, error, now);
1051
+ await options.outbox.markFailed({
1052
+ id: message.id,
1053
+ claimToken: message.claimToken,
1054
+ error,
1055
+ deadLetter,
1056
+ now,
1057
+ retryAt: deadLetter
1058
+ ? undefined
1059
+ : new Date(now.getTime() + retryDelayMs),
1060
+ });
1061
+
1062
+ if (deadLetter) {
1063
+ if (message.kind === "job") {
1064
+ instrumentation.record({
1065
+ type: "job",
1066
+ jobName: message.name,
1067
+ status: "deadLettered",
1068
+ details: {
1069
+ attempt: message.attempts,
1070
+ maxAttempts: message.maxAttempts,
1071
+ messageId: message.id,
1072
+ error: serializeOutboxError(error),
1073
+ },
1074
+ });
1075
+ }
1076
+ result.deadLettered += 1;
1077
+ } else {
1078
+ if (message.kind === "job") {
1079
+ instrumentation.record({
1080
+ type: "job",
1081
+ jobName: message.name,
1082
+ status: "retryScheduled",
1083
+ details: {
1084
+ attempt: message.attempts,
1085
+ maxAttempts: message.maxAttempts,
1086
+ messageId: message.id,
1087
+ retryDelayMs,
1088
+ retryAt: new Date(now.getTime() + retryDelayMs).toISOString(),
1089
+ error: serializeOutboxError(error),
1090
+ },
1091
+ });
1092
+ }
1093
+ result.retried += 1;
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ return result;
1099
+ }
1100
+
1101
+ /**
1102
+ * Domain event recorder port re-exported for outbox integrations.
1103
+ */
1104
+ export type { DomainEventRecorderPort };