@classytic/arc 2.8.0 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/{BaseController-CpMfCXdn.mjs → BaseController-DAGGc5Xn.mjs} +76 -25
- package/dist/{EventTransport-n1KBxC_N.d.mts → EventTransport-CLXJUzyT.d.mts} +37 -1
- package/dist/{ResourceRegistry-BOtJuRCs.mjs → ResourceRegistry-Dtcojmu8.mjs} +14 -2
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/index.mjs +1 -1
- package/dist/{adapters-BxGgSHjj.mjs → adapters-BBqAVvPK.mjs} +11 -0
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +3 -3
- package/dist/{betterAuthOpenApi-CHCIuA-p.mjs → betterAuthOpenApi-C5lDyRH2.mjs} +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +4 -4
- package/dist/{core-BfrfxNqO.mjs → core-CrLDuqoT.mjs} +1 -1
- package/dist/{createActionRouter-CbkIAaGh.mjs → createActionRouter-Df1BuawX.mjs} +87 -21
- package/dist/{createApp-Cy8eUNKQ.mjs → createApp-p2OThysU.mjs} +2 -2
- package/dist/{defineResource-CovBXvTB.mjs → defineResource-CqeUltrW.mjs} +19 -7
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/dynamic/index.d.mts +1 -1
- package/dist/dynamic/index.mjs +1 -1
- package/dist/{errorHandler-BW08lEiy.mjs → errorHandler-Cw34h_om.mjs} +1 -1
- package/dist/{errorHandler-BeN-ERN7.d.mts → errorHandler-DJ7OAB2V.d.mts} +1 -1
- package/dist/{eventPlugin-CAOWMQS8.d.mts → eventPlugin-Cdjwo0Gv.d.mts} +1 -1
- package/dist/{eventPlugin-x4jo3sG0.mjs → eventPlugin-XijlQmlL.mjs} +19 -1
- package/dist/events/index.d.mts +399 -28
- package/dist/events/index.mjs +345 -29
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/{index-BpMhrFgn.d.mts → index-0zj73o2U.d.mts} +1 -1
- package/dist/{index-qct60lnl.d.mts → index-DadoLP51.d.mts} +35 -3
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +7 -7
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/{interface-IJqN3pXK.d.mts → interface-CS6d7HiB.d.mts} +549 -107
- package/dist/{openapi-AYLVjqVe.mjs → openapi-q6rNKfZy.mjs} +49 -2
- package/dist/org/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.mjs +3 -3
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/index.d.mts +3 -3
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/{redis-stream-CF1lrKVk.d.mts → redis-stream-BgrYzpeq.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +1 -1
- package/dist/{resourceToTools-C_1SMiCz.mjs → resourceToTools-DNNWnZtx.mjs} +193 -63
- package/dist/rpc/index.mjs +1 -1
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-gUxAIZHp.d.mts → types-BlOuKTPw.d.mts} +4 -4
- package/dist/{types-Ct0PUUSp.d.mts → types-D3b7hA00.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -14
- package/dist/utils/index.mjs +5 -5
- package/dist/{utils-B-l6410F.mjs → utils-7sJ8X83I.mjs} +1 -13
- package/package.json +4 -3
- /package/dist/{circuitBreaker-l18oRgL5.mjs → circuitBreaker-cmi5XDv5.mjs} +0 -0
- /package/dist/{errors-Cg58SLNi.mjs → errors-BF2bIOIS.mjs} +0 -0
- /package/dist/{requestContext-xHIKedG6.mjs → requestContext-DYvHl113.mjs} +0 -0
- /package/dist/{schemaConverter-Y5EejTnJ.mjs → schemaConverter-OxfCshus.mjs} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
|
-
import { p as isArcError } from "./errors-
|
|
2
|
+
import { p as isArcError } from "./errors-BF2bIOIS.mjs";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
//#region src/plugins/errorHandler.ts
|
|
5
5
|
var errorHandler_exports = /* @__PURE__ */ __exportAll({ errorHandlerPlugin: () => errorHandlerPlugin });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-
|
|
1
|
+
import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-CLXJUzyT.mjs";
|
|
2
2
|
import { FastifyPluginAsync } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/events/defineEvent.d.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
|
-
import { t as requestContext } from "./requestContext-
|
|
2
|
+
import { t as requestContext } from "./requestContext-DYvHl113.mjs";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
//#region src/events/EventTransport.ts
|
|
5
5
|
/**
|
|
@@ -33,6 +33,24 @@ var MemoryEventTransport = class {
|
|
|
33
33
|
this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Reference `publishMany` implementation — delegates to `publish()` in order.
|
|
38
|
+
*
|
|
39
|
+
* Production transports (Kafka, Redis pipeline, SQS batch) should override
|
|
40
|
+
* this with a single batched network call. Memory transport has nothing to
|
|
41
|
+
* batch, so we just loop — the loop still returns a proper result map so
|
|
42
|
+
* `EventOutbox.relay` can exercise the batched code path in tests.
|
|
43
|
+
*/
|
|
44
|
+
async publishMany(events) {
|
|
45
|
+
const results = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const event of events) try {
|
|
47
|
+
await this.publish(event);
|
|
48
|
+
results.set(event.meta.id, null);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
36
54
|
async subscribe(pattern, handler) {
|
|
37
55
|
if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
|
|
38
56
|
this.handlers.get(pattern)?.add(handler);
|
package/dist/events/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as MemoryEventTransport, i as EventTransport, n as EventHandler, o as MemoryEventTransportOptions, r as EventLogger, s as
|
|
2
|
-
import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-
|
|
1
|
+
import { a as MemoryEventTransport, c as createEvent, i as EventTransport, n as EventHandler, o as MemoryEventTransportOptions, r as EventLogger, s as PublishManyResult, t as DomainEvent } from "../EventTransport-CLXJUzyT.mjs";
|
|
2
|
+
import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-Cdjwo0Gv.mjs";
|
|
3
3
|
import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
|
|
4
|
-
import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-
|
|
4
|
+
import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-BgrYzpeq.mjs";
|
|
5
5
|
|
|
6
6
|
//#region src/events/eventTypes.d.ts
|
|
7
7
|
/**
|
|
@@ -51,68 +51,439 @@ declare const CACHE_EVENTS: Readonly<{
|
|
|
51
51
|
type CacheEvent = (typeof CACHE_EVENTS)[keyof typeof CACHE_EVENTS];
|
|
52
52
|
//#endregion
|
|
53
53
|
//#region src/events/outbox.d.ts
|
|
54
|
+
/**
|
|
55
|
+
* **Terminology (v2.8.1+):**
|
|
56
|
+
*
|
|
57
|
+
* Arc's outbox uses **`delivered`** as the canonical term for "event has been
|
|
58
|
+
* successfully published to the transport and marked by `acknowledge()`".
|
|
59
|
+
*
|
|
60
|
+
* - **`acknowledge()`** is the operation that transitions an event to delivered
|
|
61
|
+
* - **`delivered`** is the resulting state
|
|
62
|
+
* - **`deliveredAt`** is the timestamp column/field in every reference implementation
|
|
63
|
+
*
|
|
64
|
+
* Store authors should use `deliveredAt` for their timestamp field. Older
|
|
65
|
+
* drafts of these docs used `acknowledgedAt` — that is a deprecated alias and
|
|
66
|
+
* should not be used in new code.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Options passed to {@link OutboxStore.save} for richer write semantics.
|
|
70
|
+
* Stores may ignore fields they don't support — contract is best-effort.
|
|
71
|
+
*/
|
|
72
|
+
interface OutboxWriteOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Host-provided DB session/transaction handle for atomic writes.
|
|
75
|
+
* Typed as `unknown` so stores can accept any backend (mongoose session,
|
|
76
|
+
* pg client, Prisma tx, etc.) without Arc taking a peer dep.
|
|
77
|
+
*/
|
|
78
|
+
readonly session?: unknown;
|
|
79
|
+
/** Earliest time the event should be visible to relay workers (for delayed publishing) */
|
|
80
|
+
readonly visibleAt?: Date;
|
|
81
|
+
/** Idempotency key — stores that support it should dedupe on this */
|
|
82
|
+
readonly dedupeKey?: string;
|
|
83
|
+
/** Partition/routing key for sharded transports (Kafka, Redis Streams) */
|
|
84
|
+
readonly partitionKey?: string;
|
|
85
|
+
/** Arbitrary headers propagated to the transport layer */
|
|
86
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Options for {@link OutboxStore.claimPending} — lease-based work claim.
|
|
90
|
+
* Supports safe multi-worker relay: each worker atomically claims a batch
|
|
91
|
+
* with a lease TTL, preventing duplicate publishes.
|
|
92
|
+
*/
|
|
93
|
+
interface OutboxClaimOptions {
|
|
94
|
+
/** Max events to claim (default: batchSize) */
|
|
95
|
+
readonly limit?: number;
|
|
96
|
+
/** Unique identifier for the claiming worker */
|
|
97
|
+
readonly consumerId?: string;
|
|
98
|
+
/** Lease duration in ms — claim is released automatically after this */
|
|
99
|
+
readonly leaseMs?: number;
|
|
100
|
+
/** Only claim events of these types (optional filter) */
|
|
101
|
+
readonly types?: readonly string[];
|
|
102
|
+
}
|
|
103
|
+
/** Options for {@link OutboxStore.acknowledge} */
|
|
104
|
+
interface OutboxAcknowledgeOptions {
|
|
105
|
+
/** Worker identifier — stores may enforce "only owner can ack" */
|
|
106
|
+
readonly consumerId?: string;
|
|
107
|
+
}
|
|
108
|
+
/** Options for {@link OutboxStore.fail} */
|
|
109
|
+
interface OutboxFailOptions {
|
|
110
|
+
/** Worker identifier — stores may enforce "only owner can fail" */
|
|
111
|
+
readonly consumerId?: string;
|
|
112
|
+
/** Schedule retry for a later time (implements backoff) */
|
|
113
|
+
readonly retryAt?: Date;
|
|
114
|
+
/** Move the event to dead-letter state (no further retries) */
|
|
115
|
+
readonly deadLetter?: boolean;
|
|
116
|
+
}
|
|
117
|
+
/** Normalized error info passed to {@link OutboxStore.fail} */
|
|
118
|
+
interface OutboxErrorInfo {
|
|
119
|
+
readonly message: string;
|
|
120
|
+
readonly code?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Thrown by a store when `acknowledge` / `fail` is called by a consumer that
|
|
124
|
+
* does not own the event's current lease.
|
|
125
|
+
*
|
|
126
|
+
* Stores that enforce lease ownership MUST throw this (not silently return)
|
|
127
|
+
* so {@link EventOutbox} can detect the mismatch and avoid over-counting
|
|
128
|
+
* successful deliveries. {@link EventOutbox.relay} catches it and reports
|
|
129
|
+
* via {@link EventOutboxOptions.onError} instead of counting as relayed.
|
|
130
|
+
*/
|
|
131
|
+
declare class OutboxOwnershipError extends Error {
|
|
132
|
+
readonly eventId: string;
|
|
133
|
+
readonly attemptedBy: string;
|
|
134
|
+
readonly currentOwner: string | null;
|
|
135
|
+
constructor(eventId: string, attemptedBy: string, currentOwner: string | null);
|
|
136
|
+
}
|
|
137
|
+
/** Thrown by {@link EventOutbox.store} when an event is missing `type` or `meta.id`. */
|
|
138
|
+
declare class InvalidOutboxEventError extends Error {
|
|
139
|
+
constructor(reason: string);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Durable storage contract for the transactional outbox pattern.
|
|
143
|
+
*
|
|
144
|
+
* **Required methods**: `save`, `getPending`, `acknowledge`.
|
|
145
|
+
*
|
|
146
|
+
* **Optional capabilities** — stores opt-in to richer semantics:
|
|
147
|
+
* - `claimPending` — lease-based work claim for multi-worker relay
|
|
148
|
+
* - `fail` — failure tracking with retry scheduling and dead-letter
|
|
149
|
+
* - `purge` — acknowledged event cleanup
|
|
150
|
+
*
|
|
151
|
+
* Arc's {@link EventOutbox} detects capabilities at runtime and degrades
|
|
152
|
+
* gracefully for legacy stores that only implement the required methods.
|
|
153
|
+
*
|
|
154
|
+
* ## Required semantics
|
|
155
|
+
*
|
|
156
|
+
* Implementations MUST honor these contracts — `EventOutbox` depends on them
|
|
157
|
+
* for correctness of at-least-once delivery:
|
|
158
|
+
*
|
|
159
|
+
* 1. **`save` must reject invalid events.** If `event.type` or `event.meta.id`
|
|
160
|
+
* is missing/empty, throw — do not persist. `EventOutbox.store()` validates
|
|
161
|
+
* first, but stores should defend against direct-save code paths.
|
|
162
|
+
*
|
|
163
|
+
* 2. **`claimPending` must be atomic.** Two workers calling `claimPending`
|
|
164
|
+
* concurrently must never receive the same event. Stores backed by SQL/Mongo
|
|
165
|
+
* should use `SELECT ... FOR UPDATE SKIP LOCKED` or `findOneAndUpdate` with
|
|
166
|
+
* `{ leaseOwner: null }` as the match condition.
|
|
167
|
+
*
|
|
168
|
+
* 3. **`acknowledge` / `fail` must throw {@link OutboxOwnershipError} on
|
|
169
|
+
* ownership mismatch.** When `options.consumerId` is provided and does not
|
|
170
|
+
* match the current lease owner, the method MUST throw — never silently
|
|
171
|
+
* no-op. `EventOutbox.relay` relies on this signal to avoid counting
|
|
172
|
+
* hijacked events as successfully relayed.
|
|
173
|
+
*
|
|
174
|
+
* 4. **`acknowledge` on an unknown `eventId` is a no-op.** This keeps relay
|
|
175
|
+
* idempotent when the store has been purged or the event was manually
|
|
176
|
+
* removed. Do NOT throw `OutboxOwnershipError` in this case.
|
|
177
|
+
*
|
|
178
|
+
* 5. **`fail` must deterministically update lease/visibility.** On success,
|
|
179
|
+
* the event MUST become re-claimable (either immediately or at `retryAt`),
|
|
180
|
+
* or transition to dead-letter state. The lease owner should be cleared.
|
|
181
|
+
*
|
|
182
|
+
* 6. **Malformed events must never be returned by `getPending`/`claimPending`.**
|
|
183
|
+
* If the store's underlying storage has corrupt rows, the store is
|
|
184
|
+
* responsible for quarantining them (e.g., direct DB delete, DLQ).
|
|
185
|
+
*/
|
|
54
186
|
interface OutboxStore {
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Save event to outbox (typically called within a business transaction).
|
|
189
|
+
*
|
|
190
|
+
* MUST reject events missing `type` or `meta.id` — throw rather than persist.
|
|
191
|
+
*
|
|
192
|
+
* @param event - Event to persist
|
|
193
|
+
* @param options - Optional write metadata (session, visibleAt, dedupeKey, etc.)
|
|
194
|
+
*/
|
|
195
|
+
save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* Get pending (unrelayed) events, ordered FIFO.
|
|
198
|
+
*
|
|
199
|
+
* This is the legacy/simple pull API. Multi-worker deployments should
|
|
200
|
+
* prefer {@link OutboxStore.claimPending} to avoid duplicate publishes.
|
|
201
|
+
*
|
|
202
|
+
* Events returned by this method MUST be well-formed (valid `type` and
|
|
203
|
+
* `meta.id`). Corrupt rows must be quarantined by the store, not exposed
|
|
204
|
+
* to the relay loop.
|
|
205
|
+
*/
|
|
58
206
|
getPending(limit: number): Promise<DomainEvent[]>;
|
|
59
|
-
/** Mark event as successfully relayed */
|
|
60
|
-
acknowledge(eventId: string): Promise<void>;
|
|
61
207
|
/**
|
|
62
|
-
*
|
|
208
|
+
* Atomically claim pending events with a lease.
|
|
209
|
+
*
|
|
210
|
+
* When implemented, {@link EventOutbox.relay} prefers this over `getPending`
|
|
211
|
+
* so multi-worker relay is safe: each worker holds an exclusive lease on
|
|
212
|
+
* its batch, and stale leases are automatically recovered.
|
|
213
|
+
*
|
|
214
|
+
* **Atomicity is mandatory**: two concurrent callers must never receive
|
|
215
|
+
* overlapping events. Use `SELECT ... FOR UPDATE SKIP LOCKED` (SQL) or
|
|
216
|
+
* a compound condition on `findOneAndUpdate` (Mongo).
|
|
217
|
+
*/
|
|
218
|
+
claimPending?(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
|
|
219
|
+
/**
|
|
220
|
+
* Mark event as successfully relayed.
|
|
221
|
+
*
|
|
222
|
+
* **Ownership contract**: If `options.consumerId` is provided and does not
|
|
223
|
+
* match the current lease owner, this method MUST throw
|
|
224
|
+
* {@link OutboxOwnershipError}. Unknown `eventId` is a no-op.
|
|
225
|
+
*
|
|
226
|
+
* @param eventId - Event ID (from `meta.id`)
|
|
227
|
+
* @param options - Optional ack metadata (consumerId for lease enforcement)
|
|
228
|
+
* @throws {@link OutboxOwnershipError} on ownership mismatch
|
|
229
|
+
*/
|
|
230
|
+
acknowledge(eventId: string, options?: OutboxAcknowledgeOptions): Promise<void>;
|
|
231
|
+
/**
|
|
232
|
+
* Record a relay failure. When implemented, {@link EventOutbox.relay} calls
|
|
233
|
+
* this instead of stopping the batch — enables retry scheduling and DLQ.
|
|
234
|
+
*
|
|
235
|
+
* **Ownership contract**: Same as `acknowledge` — MUST throw
|
|
236
|
+
* {@link OutboxOwnershipError} on mismatch. After a successful call, the
|
|
237
|
+
* lease owner MUST be cleared and the event MUST be re-claimable (at
|
|
238
|
+
* `retryAt` if provided) or transitioned to dead-letter.
|
|
239
|
+
*
|
|
240
|
+
* @throws {@link OutboxOwnershipError} on ownership mismatch
|
|
241
|
+
*/
|
|
242
|
+
fail?(eventId: string, error: OutboxErrorInfo, options?: OutboxFailOptions): Promise<void>;
|
|
243
|
+
/**
|
|
244
|
+
* Purge old **delivered** events (optional, DB-agnostic contract).
|
|
245
|
+
*
|
|
246
|
+
* Cleanup is scoped to events in the `delivered` state — events still
|
|
247
|
+
* pending, failed, or dead-lettered MUST NOT be removed by purge.
|
|
63
248
|
*
|
|
64
249
|
* Arc does **not** ship a concrete implementation — your store owns the
|
|
65
250
|
* cleanup strategy that fits your database:
|
|
66
251
|
*
|
|
67
|
-
* - **MongoDB:** TTL index on `
|
|
68
|
-
* - **SQL:** Scheduled `DELETE FROM outbox WHERE
|
|
69
|
-
* - **Redis:** Key expiry (`EXPIRE`) on
|
|
252
|
+
* - **MongoDB:** TTL index on `deliveredAt` (automatic, zero-code)
|
|
253
|
+
* - **SQL:** Scheduled `DELETE FROM outbox WHERE status = 'delivered' AND delivered_at < :cutoff`
|
|
254
|
+
* - **Redis:** Key expiry (`EXPIRE`) on delivered entries
|
|
70
255
|
*
|
|
71
256
|
* Called by {@link EventOutbox.purge}. If not implemented, cleanup is
|
|
72
257
|
* entirely the app's responsibility via native DB tools.
|
|
73
258
|
*
|
|
74
|
-
* @param olderThanMs - Remove events
|
|
259
|
+
* @param olderThanMs - Remove events delivered more than this many ms ago
|
|
75
260
|
* @returns Number of purged events
|
|
76
261
|
*/
|
|
77
262
|
purge?(olderThanMs: number): Promise<number>;
|
|
78
263
|
}
|
|
264
|
+
/** Reason codes passed to {@link EventOutboxOptions.onError}. */
|
|
265
|
+
type OutboxRelayErrorKind = "publish_failed" | "acknowledge_failed" | "fail_failed" | "ownership_mismatch" | "malformed_event";
|
|
266
|
+
/**
|
|
267
|
+
* Rich per-batch outcome returned by {@link EventOutbox.relayBatch}.
|
|
268
|
+
*
|
|
269
|
+
* Useful for operational dashboards, alerting thresholds, and test assertions.
|
|
270
|
+
* The simpler {@link EventOutbox.relay} returns just the `relayed` count for
|
|
271
|
+
* backward compatibility.
|
|
272
|
+
*/
|
|
273
|
+
interface RelayResult {
|
|
274
|
+
/** Number of events successfully published AND acknowledged */
|
|
275
|
+
readonly relayed: number;
|
|
276
|
+
/** Number of events claimed and attempted in this batch */
|
|
277
|
+
readonly attempted: number;
|
|
278
|
+
/** Number of publish failures (transport rejected the event) */
|
|
279
|
+
readonly publishFailed: number;
|
|
280
|
+
/** Number of acknowledge failures after successful publish (at-least-once replay risk) */
|
|
281
|
+
readonly ackFailed: number;
|
|
282
|
+
/** Number of ownership mismatches (our lease expired mid-flight) */
|
|
283
|
+
readonly ownershipMismatches: number;
|
|
284
|
+
/** Number of malformed events encountered (aborts the batch) */
|
|
285
|
+
readonly malformed: number;
|
|
286
|
+
/** Number of fail() calls that themselves threw (store bugs / contention) */
|
|
287
|
+
readonly failHookErrors: number;
|
|
288
|
+
/** Whether `publishMany` was used (true) or per-event `publish` (false) */
|
|
289
|
+
readonly usedPublishMany: boolean;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Called by {@link EventOutbox.relay} when a non-fatal error occurs during
|
|
293
|
+
* a batch. Used for logging and metrics. Must not throw.
|
|
294
|
+
*/
|
|
295
|
+
type OutboxRelayErrorHandler = (info: {
|
|
296
|
+
readonly kind: OutboxRelayErrorKind;
|
|
297
|
+
readonly event?: DomainEvent;
|
|
298
|
+
readonly error: Error;
|
|
299
|
+
}) => void;
|
|
79
300
|
interface EventOutboxOptions {
|
|
80
301
|
/** Outbox store for persistence */
|
|
81
|
-
store: OutboxStore;
|
|
302
|
+
readonly store: OutboxStore;
|
|
82
303
|
/** Transport to relay events to (optional — can relay later) */
|
|
83
|
-
transport?: EventTransport;
|
|
304
|
+
readonly transport?: EventTransport;
|
|
84
305
|
/** Max events per relay batch (default: 100) */
|
|
85
|
-
batchSize?: number;
|
|
306
|
+
readonly batchSize?: number;
|
|
307
|
+
/**
|
|
308
|
+
* Unique identifier for this relay worker. Used when the store supports
|
|
309
|
+
* `claimPending`/`fail` to enforce lease ownership. Defaults to a random ID.
|
|
310
|
+
*/
|
|
311
|
+
readonly consumerId?: string;
|
|
312
|
+
/**
|
|
313
|
+
* Lease duration in ms for claimed events. Only used when the store
|
|
314
|
+
* supports `claimPending`. Default: 30 seconds.
|
|
315
|
+
*/
|
|
316
|
+
readonly leaseMs?: number;
|
|
317
|
+
/**
|
|
318
|
+
* Callback for non-fatal errors during relay: publish failures,
|
|
319
|
+
* ownership mismatches, ack/fail errors, malformed events. Use this for
|
|
320
|
+
* logging and metrics. Must not throw — exceptions are swallowed.
|
|
321
|
+
*/
|
|
322
|
+
readonly onError?: OutboxRelayErrorHandler;
|
|
323
|
+
/**
|
|
324
|
+
* Enable {@link EventTransport.publishMany} when the transport implements it.
|
|
325
|
+
* Default: `true`. Set to `false` to force per-event `publish()` — useful
|
|
326
|
+
* for transports where strict event-order observability matters more than
|
|
327
|
+
* throughput, or to debug batch-specific issues.
|
|
328
|
+
*/
|
|
329
|
+
readonly usePublishMany?: boolean;
|
|
86
330
|
}
|
|
87
331
|
declare class EventOutbox {
|
|
88
332
|
private readonly _store;
|
|
89
333
|
private readonly _transport?;
|
|
90
334
|
private readonly _batchSize;
|
|
335
|
+
private readonly _consumerId;
|
|
336
|
+
private readonly _leaseMs;
|
|
337
|
+
private readonly _onError?;
|
|
338
|
+
private readonly _usePublishMany;
|
|
91
339
|
constructor(opts: EventOutboxOptions);
|
|
92
|
-
/**
|
|
93
|
-
|
|
340
|
+
/** Unique consumer ID used for lease ownership when the store supports claims */
|
|
341
|
+
get consumerId(): string;
|
|
342
|
+
/**
|
|
343
|
+
* Store event in outbox.
|
|
344
|
+
*
|
|
345
|
+
* Validates that `event.type` and `event.meta.id` are present — throws
|
|
346
|
+
* {@link InvalidOutboxEventError} otherwise, so corrupt rows can never
|
|
347
|
+
* be persisted via this API.
|
|
348
|
+
*
|
|
349
|
+
* Pass `options.session` to participate in a host-managed DB transaction
|
|
350
|
+
* (store must support session-aware writes). Other options (`visibleAt`,
|
|
351
|
+
* `dedupeKey`, `partitionKey`, `headers`) are forwarded to stores that
|
|
352
|
+
* implement them and ignored otherwise.
|
|
353
|
+
*/
|
|
354
|
+
store(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
|
|
355
|
+
private _reportError;
|
|
94
356
|
/**
|
|
95
|
-
* Relay pending events to transport
|
|
357
|
+
* Relay pending events to transport and return the number of successful
|
|
358
|
+
* publish+acknowledge pairs.
|
|
96
359
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
360
|
+
* For richer observability (per-kind counts, publishMany detection, etc.)
|
|
361
|
+
* use {@link relayBatch} which returns a {@link RelayResult}. This method
|
|
362
|
+
* is the backward-compatible shortcut that returns just the count.
|
|
99
363
|
*
|
|
100
|
-
* @returns Number of successfully published
|
|
364
|
+
* @returns Number of successfully published AND acknowledged events
|
|
101
365
|
*/
|
|
102
366
|
relay(): Promise<number>;
|
|
103
367
|
/**
|
|
104
|
-
*
|
|
368
|
+
* Relay a batch of pending events to the transport and return a rich
|
|
369
|
+
* {@link RelayResult} describing the outcome of each event.
|
|
370
|
+
*
|
|
371
|
+
* Behavior summary:
|
|
372
|
+
*
|
|
373
|
+
* - **Claim path**: uses {@link OutboxStore.claimPending} when the store
|
|
374
|
+
* supports it (safe for multi-worker relay) or falls back to
|
|
375
|
+
* {@link OutboxStore.getPending} (single-worker only).
|
|
376
|
+
*
|
|
377
|
+
* - **Publish path**: if the transport implements
|
|
378
|
+
* {@link EventTransport.publishMany} and `usePublishMany` is not disabled,
|
|
379
|
+
* the entire batch is sent in one call. Otherwise each event is published
|
|
380
|
+
* individually. Either way, per-event outcomes are tracked.
|
|
381
|
+
*
|
|
382
|
+
* - **Failure path**: if the store implements `fail`, per-event failures
|
|
383
|
+
* are reported via `store.fail(...)` and the batch continues. Without
|
|
384
|
+
* `fail`, the batch stops on the first failure (legacy behavior).
|
|
385
|
+
*
|
|
386
|
+
* - **Malformed events**: events missing `type` or `meta.id` abort the
|
|
387
|
+
* batch — a well-behaved store must never return them (see
|
|
388
|
+
* {@link OutboxStore} semantics #6). The error is reported via `onError`.
|
|
389
|
+
*
|
|
390
|
+
* - **Ownership mismatches**: if `acknowledge`/`fail` throws
|
|
391
|
+
* {@link OutboxOwnershipError} (our lease expired and another worker
|
|
392
|
+
* claimed the event), the event is NOT counted as relayed. The other
|
|
393
|
+
* worker will re-publish — at-least-once semantics preserved.
|
|
394
|
+
*
|
|
395
|
+
* @returns Per-kind outcome counts for the batch
|
|
396
|
+
*/
|
|
397
|
+
relayBatch(): Promise<RelayResult>;
|
|
398
|
+
/**
|
|
399
|
+
* Purge old **delivered** events from the outbox store.
|
|
105
400
|
* Delegates to `store.purge()` if implemented; no-op otherwise.
|
|
106
|
-
* @param olderThanMs - Remove events
|
|
401
|
+
* @param olderThanMs - Remove events delivered more than this many ms ago (default: 7 days)
|
|
107
402
|
* @returns Number of purged events, or 0 if store doesn't support purge
|
|
108
403
|
*/
|
|
109
404
|
purge(olderThanMs?: number): Promise<number>;
|
|
110
405
|
}
|
|
406
|
+
interface MemoryEntry {
|
|
407
|
+
event: DomainEvent;
|
|
408
|
+
status: "pending" | "delivered" | "dead_letter";
|
|
409
|
+
attempts: number;
|
|
410
|
+
visibleAt: number;
|
|
411
|
+
leaseOwner: string | null;
|
|
412
|
+
leaseExpiresAt: number;
|
|
413
|
+
deliveredAt: number | null;
|
|
414
|
+
lastError: OutboxErrorInfo | null;
|
|
415
|
+
dedupeKey?: string;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* In-memory outbox store — reference implementation supporting the full
|
|
419
|
+
* capability set (claim/lease, fail/retry, dedupe, visibleAt).
|
|
420
|
+
*
|
|
421
|
+
* For dev/testing only. Production deployments should use a durable store
|
|
422
|
+
* backed by the app's database.
|
|
423
|
+
*/
|
|
111
424
|
declare class MemoryOutboxStore implements OutboxStore {
|
|
112
|
-
private
|
|
113
|
-
|
|
425
|
+
private readonly entries;
|
|
426
|
+
private readonly seenDedupeKeys;
|
|
427
|
+
save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
|
|
114
428
|
getPending(limit: number): Promise<DomainEvent[]>;
|
|
115
|
-
|
|
429
|
+
claimPending(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
|
|
430
|
+
acknowledge(eventId: string, options?: OutboxAcknowledgeOptions): Promise<void>;
|
|
431
|
+
fail(eventId: string, error: OutboxErrorInfo, options?: OutboxFailOptions): Promise<void>;
|
|
432
|
+
purge(olderThanMs: number): Promise<number>;
|
|
433
|
+
/** Test helper: inspect entry by id */
|
|
434
|
+
_getEntry(eventId: string): Readonly<MemoryEntry> | undefined;
|
|
116
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Options for {@link exponentialBackoff}.
|
|
438
|
+
*/
|
|
439
|
+
interface ExponentialBackoffOptions {
|
|
440
|
+
/** Current attempt count (1-indexed — first retry is attempt 1) */
|
|
441
|
+
readonly attempt: number;
|
|
442
|
+
/** Base delay in ms (first retry delay). Default: 1000 (1 second) */
|
|
443
|
+
readonly baseMs?: number;
|
|
444
|
+
/** Maximum delay in ms — caps exponential growth. Default: 60_000 (1 minute) */
|
|
445
|
+
readonly maxMs?: number;
|
|
446
|
+
/**
|
|
447
|
+
* Jitter factor [0–1]. The returned delay is multiplied by
|
|
448
|
+
* `1 + (random * jitter)` to spread retry bursts across workers.
|
|
449
|
+
* Default: 0.2 (±20%). Set to 0 to disable.
|
|
450
|
+
*/
|
|
451
|
+
readonly jitter?: number;
|
|
452
|
+
/** Reference time (for deterministic tests). Default: `Date.now()` */
|
|
453
|
+
readonly now?: number;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Compute a `retryAt` `Date` using exponential backoff with jitter.
|
|
457
|
+
*
|
|
458
|
+
* This is a convenience helper for store authors implementing
|
|
459
|
+
* {@link OutboxStore.fail}: call it to compute the retry visibility window
|
|
460
|
+
* based on the event's current attempt count.
|
|
461
|
+
*
|
|
462
|
+
* Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
|
|
463
|
+
*
|
|
464
|
+
* @example Basic usage inside a store's `fail()` method
|
|
465
|
+
* ```typescript
|
|
466
|
+
* async fail(eventId, error, options) {
|
|
467
|
+
* const entry = await this.findById(eventId);
|
|
468
|
+
* entry.attempts++;
|
|
469
|
+
* if (entry.attempts >= MAX_ATTEMPTS) {
|
|
470
|
+
* return this.deadLetter(eventId, error);
|
|
471
|
+
* }
|
|
472
|
+
* const retryAt = exponentialBackoff({ attempt: entry.attempts });
|
|
473
|
+
* entry.visibleAt = retryAt;
|
|
474
|
+
* await this.update(entry);
|
|
475
|
+
* }
|
|
476
|
+
* ```
|
|
477
|
+
*
|
|
478
|
+
* @example Tuning for a faster transport
|
|
479
|
+
* ```typescript
|
|
480
|
+
* exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
|
|
481
|
+
* // attempt=1 → ~250ms ±30%
|
|
482
|
+
* // attempt=2 → ~500ms ±30%
|
|
483
|
+
* // attempt=3 → ~1000ms ±30%
|
|
484
|
+
* // attempt=10 → capped at 10_000ms
|
|
485
|
+
* ```
|
|
486
|
+
*/
|
|
487
|
+
declare function exponentialBackoff(options: ExponentialBackoffOptions): Date;
|
|
117
488
|
//#endregion
|
|
118
|
-
export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type DomainEvent, type EventDefinitionInput, type EventDefinitionOutput, type EventHandler, type EventLogger, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventRegistryOptions, type EventSchema, type EventTransport, MemoryEventTransport, type MemoryEventTransportOptions, MemoryOutboxStore, type OutboxStore, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RetryOptions, type ValidationResult, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, withRetry };
|
|
489
|
+
export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type DomainEvent, type EventDefinitionInput, type EventDefinitionOutput, type EventHandler, type EventLogger, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventRegistryOptions, type EventSchema, type EventTransport, type ExponentialBackoffOptions, InvalidOutboxEventError, MemoryEventTransport, type MemoryEventTransportOptions, MemoryOutboxStore, type OutboxAcknowledgeOptions, type OutboxClaimOptions, type OutboxErrorInfo, type OutboxFailOptions, OutboxOwnershipError, type OutboxRelayErrorHandler, type OutboxRelayErrorKind, type OutboxStore, type OutboxWriteOptions, type PublishManyResult, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RelayResult, type RetryOptions, type ValidationResult, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, withRetry };
|