@classytic/arc 2.23.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseController-BQ8KNvWC.mjs → BaseController--M8EMzt0.mjs} +90 -21
- package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
- package/dist/actionPermissions-CAphcmUG.mjs +24 -0
- package/dist/audit/index.d.mts +32 -3
- package/dist/audit/index.mjs +7 -4
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +263 -259
- package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
- package/dist/cache/index.d.mts +41 -3
- package/dist/cache/index.mjs +33 -4
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/context/index.mjs +2 -64
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-DQ6rDTyH.mjs → core-EUNu0u07.mjs} +296 -11
- package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-B6GvAuAX.mjs} +3 -2
- package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
- package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
- package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
- package/dist/events/index.d.mts +57 -314
- package/dist/events/index.mjs +163 -91
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +24 -2
- package/dist/factory/index.d.mts +4 -5
- package/dist/factory/index.mjs +28 -8
- package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
- package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
- package/dist/headers-BvlQSDMB.mjs +45 -0
- package/dist/health-CB_m4S32.mjs +377 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +27 -3
- package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
- package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
- package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
- package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
- package/dist/{index-CC5ruDGh.d.mts → index-f4PTrk3B.d.mts} +37 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +5 -5
- package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/jobs/index.d.mts +2 -0
- package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
- package/dist/integrations/mcp/index.d.mts +4 -4
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +37 -30
- package/dist/integrations/webhooks.mjs +51 -62
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +41 -14
- package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
- package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cb7olw0j.mjs} +11 -9
- package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
- package/dist/middleware/index.d.mts +17 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/migrations/index.d.mts +94 -44
- package/dist/migrations/index.mjs +196 -44
- package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
- package/dist/{openapi-jSDx-VDv.mjs → openapi-DrLtog_B.mjs} +3 -3
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BN5G05rd.mjs} +3 -26
- package/dist/plugins/index.d.mts +6 -77
- package/dist/plugins/index.mjs +11 -95
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{index-Ba10fdKp.d.mts → preloadResources-0oe-JSSS.d.mts} +50 -50
- package/dist/{createApp-CjOeWJ9_.mjs → preloadResources-DfyngIN9.mjs} +233 -70
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +2 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
- package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
- package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
- package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
- package/dist/{realtime-I8OrfnMR.mjs → realtime-Cuw_deA-.mjs} +2 -2
- package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
- package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
- package/dist/registry/index.d.mts +2 -2
- package/dist/renewingLease-3cuQH7h8.mjs +44 -0
- package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
- package/dist/schedules-CjOd2MG9.mjs +113 -0
- package/dist/schemas/index.d.mts +67 -67
- package/dist/schemas/index.mjs +2 -3
- package/dist/scim/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +4 -4
- package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
- package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
- package/dist/testing/index.d.mts +4 -3
- package/dist/testing/index.mjs +2 -2
- package/dist/transactionContext-PNdoyLLm.mjs +64 -0
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
- package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
- package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
- package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
- package/dist/usage/index.d.mts +3 -52
- package/dist/usage/index.mjs +0 -0
- package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
- package/dist/utils/index.d.mts +58 -85
- package/dist/utils/index.mjs +4 -2
- package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
- package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
- package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
- package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
- package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
- package/package.json +526 -513
- package/skills/arc/references/api-reference.md +3 -1
- package/skills/arc/references/integrations.md +20 -11
- package/dist/QueryCache-Z1B38koh.d.mts +0 -40
- package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
- package/dist/health-ScyjIVo5.mjs +0 -202
- package/dist/requestId-5BhTVzBv.mjs +0 -113
- package/dist/types-XTApa8b6.mjs +0 -24
|
@@ -140,12 +140,41 @@ interface RetryOptions {
|
|
|
140
140
|
*/
|
|
141
141
|
transport?: Pick<EventTransport, "deadLetter">;
|
|
142
142
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
143
|
+
* DURABLE dead-letter persistence for exhausted events (custom store,
|
|
144
|
+
* `$deadLetter` publisher, ...). Treated exactly like
|
|
145
|
+
* {@link transport}.deadLetter: a failure follows {@link dlqFailureMode}
|
|
146
|
+
* — rethrow by default, so the transport redelivers instead of
|
|
147
|
+
* acknowledging an event that is neither processed nor persisted.
|
|
145
148
|
*
|
|
146
|
-
*
|
|
149
|
+
* This is what `eventPlugin({ deadLetterQueue })` wires. Use
|
|
150
|
+
* {@link onDead} for metrics/alerts whose failure must NOT affect
|
|
151
|
+
* acknowledgement.
|
|
152
|
+
*/
|
|
153
|
+
deadLetter?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;
|
|
154
|
+
/**
|
|
155
|
+
* OBSERVABILITY callback when all retries are exhausted (metrics,
|
|
156
|
+
* alerting, logging). Fires in addition to {@link transport} /
|
|
157
|
+
* {@link deadLetter} routing. Failures are logged and swallowed — this
|
|
158
|
+
* callback can never affect whether the event is acknowledged; durable
|
|
159
|
+
* persistence belongs in {@link deadLetter}.
|
|
147
160
|
*/
|
|
148
161
|
onDead?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* What to do when `transport.deadLetter()` ITSELF fails after retries are
|
|
164
|
+
* exhausted.
|
|
165
|
+
*
|
|
166
|
+
* - `'rethrow'` (default) — the wrapper throws, so the transport treats
|
|
167
|
+
* the handler as failed: an at-least-once transport (Redis Streams,
|
|
168
|
+
* SQS) keeps the original message pending and redelivers it. The only
|
|
169
|
+
* mode that cannot LOSE the event — returning normally would
|
|
170
|
+
* acknowledge a message that was neither processed nor persisted.
|
|
171
|
+
* - `'log-and-drop'` — log the DLQ failure and return normally,
|
|
172
|
+
* acknowledging the message. Only correct when losing the event is
|
|
173
|
+
* acceptable (metrics, notifications).
|
|
174
|
+
*
|
|
175
|
+
* @default 'rethrow'
|
|
176
|
+
*/
|
|
177
|
+
dlqFailureMode?: "rethrow" | "log-and-drop";
|
|
149
178
|
/**
|
|
150
179
|
* Optional name for logging + written into `DeadLetteredEvent.handlerName`.
|
|
151
180
|
*/
|
|
@@ -246,6 +275,12 @@ interface EventPluginOptions {
|
|
|
246
275
|
* Dead letter queue for events that exhaust all retries.
|
|
247
276
|
* Requires `retry` to be enabled. If `retry` is set but no custom `store`,
|
|
248
277
|
* failed events are published to the `$deadLetter` event type by default.
|
|
278
|
+
*
|
|
279
|
+
* The store is DURABLE persistence: if it throws, the wrapped handler
|
|
280
|
+
* rethrows (per `retry.dlqFailureMode`, default `'rethrow'`) so an
|
|
281
|
+
* at-least-once transport redelivers instead of acknowledging an event
|
|
282
|
+
* that was neither processed nor dead-lettered. Metrics/alerting belongs
|
|
283
|
+
* in `retry.onDead`, whose failures never affect acknowledgement.
|
|
249
284
|
*/
|
|
250
285
|
deadLetterQueue?: {
|
|
251
286
|
/** Custom store function. If omitted, publishes to '$deadLetter' event type. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { d as createDomainError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
-
import { n as requestContext } from "./requestContext-
|
|
4
|
+
import { n as requestContext } from "./requestContext-MrlJwECf.mjs";
|
|
5
5
|
import fp from "fastify-plugin";
|
|
6
6
|
import { createEvent, matchEventPattern } from "@classytic/primitives/events";
|
|
7
7
|
//#region src/events/EventTransport.ts
|
|
@@ -118,12 +118,13 @@ var MemoryEventTransport = class {
|
|
|
118
118
|
* to `unknown` for raw `subscribe(pattern, withRetry(...))` call sites.
|
|
119
119
|
*/
|
|
120
120
|
function withRetry(handler, options = {}) {
|
|
121
|
-
const { maxRetries = 3, backoffMs = 1e3, maxBackoffMs = 3e4, jitter = .1, transport, onDead, name, logger = console } = options;
|
|
121
|
+
const { maxRetries = 3, backoffMs = 1e3, maxBackoffMs = 3e4, jitter = .1, transport, deadLetter, onDead, dlqFailureMode = "rethrow", name, logger = console } = options;
|
|
122
122
|
const label = name ?? handler.name ?? "anonymous";
|
|
123
123
|
return async (event) => {
|
|
124
124
|
const errors = [];
|
|
125
125
|
let firstFailedAt;
|
|
126
126
|
let lastFailedAt;
|
|
127
|
+
let dlqPersistError;
|
|
127
128
|
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
128
129
|
await handler(event);
|
|
129
130
|
return;
|
|
@@ -160,13 +161,21 @@ function withRetry(handler, options = {}) {
|
|
|
160
161
|
await transport.deadLetter(dlq);
|
|
161
162
|
} catch (dlqErr) {
|
|
162
163
|
logger.error("[Arc Events] transport.deadLetter() failed:", dlqErr);
|
|
164
|
+
dlqPersistError = dlqErr instanceof Error ? dlqErr : new Error(String(dlqErr));
|
|
163
165
|
}
|
|
164
166
|
}
|
|
167
|
+
if (deadLetter) try {
|
|
168
|
+
await deadLetter(event, errors);
|
|
169
|
+
} catch (dlqErr) {
|
|
170
|
+
logger.error("[Arc Events] deadLetter() persistence failed:", dlqErr);
|
|
171
|
+
dlqPersistError ??= dlqErr instanceof Error ? dlqErr : new Error(String(dlqErr));
|
|
172
|
+
}
|
|
165
173
|
if (onDead) try {
|
|
166
174
|
await onDead(event, errors);
|
|
167
175
|
} catch (dlqErr) {
|
|
168
|
-
logger.error("[Arc Events]
|
|
176
|
+
logger.error("[Arc Events] onDead observability callback failed:", dlqErr);
|
|
169
177
|
}
|
|
178
|
+
if (dlqPersistError && dlqFailureMode === "rethrow") throw new Error(`[Arc Events] Handler '${label}' exhausted retries for ${event.type} AND dead-letter persistence failed — rethrowing so the transport redelivers instead of acknowledging a lost event (set dlqFailureMode: 'log-and-drop' to accept loss).`, { cause: dlqPersistError });
|
|
170
179
|
};
|
|
171
180
|
}
|
|
172
181
|
/**
|
|
@@ -279,13 +288,22 @@ const eventPlugin = async (fastify, opts = {}) => {
|
|
|
279
288
|
let wrappedHandler = handler;
|
|
280
289
|
if (retryOpts && pattern !== "$deadLetter") wrappedHandler = withRetry(handler, {
|
|
281
290
|
...retryOpts,
|
|
282
|
-
|
|
291
|
+
deadLetter: dlqOpts?.store ?? createDeadLetterPublisher(fastify.events),
|
|
283
292
|
logger: fastify.log
|
|
284
293
|
});
|
|
285
294
|
if (logEvents) fastify.log?.info?.({
|
|
286
295
|
pattern,
|
|
287
296
|
retry: !!retryOpts
|
|
288
297
|
}, "Subscribing to events");
|
|
298
|
+
if (typeof transport.subscribe !== "function") {
|
|
299
|
+
const error = /* @__PURE__ */ new Error(`[Arc Events] transport '${transport.name}' is publish-only (no subscribe) — cannot register a handler for '${pattern}'. Use MemoryEventTransport / RedisEventTransport, or a transport that implements subscribe().`);
|
|
300
|
+
fastify.log?.error?.({
|
|
301
|
+
transport: transport.name,
|
|
302
|
+
pattern
|
|
303
|
+
}, error.message);
|
|
304
|
+
if (!failOpen) throw error;
|
|
305
|
+
return () => {};
|
|
306
|
+
}
|
|
289
307
|
try {
|
|
290
308
|
return await transport.subscribe(pattern, wrappedHandler);
|
|
291
309
|
} catch (error) {
|
package/dist/events/index.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { a as EventTransport, i as EventLogger, n as DomainEvent, o as MemoryEventTransport, r as EventHandler, s as MemoryEventTransportOptions, t as DeadLetteredEvent } from "../EventTransport-QmY7A258.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-
|
|
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-Bg3cP2_u.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-uIaauNpZ.mjs";
|
|
5
|
+
import { OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxErrorInfo, OutboxFailOptions, OutboxFailurePolicy, OutboxStore, OutboxWriteOptions } from "@classytic/primitives/outbox";
|
|
5
6
|
import { RepositoryLike } from "@classytic/repo-core/adapter";
|
|
6
7
|
//#region src/events/eventTypes.d.ts
|
|
7
8
|
/**
|
|
@@ -82,279 +83,65 @@ declare class MemoryOutboxStore implements OutboxStore {
|
|
|
82
83
|
_getEntry(eventId: string): Readonly<MemoryEntry> | undefined;
|
|
83
84
|
}
|
|
84
85
|
//#endregion
|
|
85
|
-
//#region src/events/outbox.d.ts
|
|
86
|
+
//#region src/events/outbox/backoff.d.ts
|
|
86
87
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* Arc's outbox uses **`delivered`** as the canonical term for "event has been
|
|
90
|
-
* successfully published to the transport and marked by `acknowledge()`".
|
|
91
|
-
*
|
|
92
|
-
* - **`acknowledge()`** is the operation that transitions an event to delivered
|
|
93
|
-
* - **`delivered`** is the resulting state
|
|
94
|
-
* - **`deliveredAt`** is the timestamp column/field in every reference implementation
|
|
95
|
-
*
|
|
96
|
-
* Store authors should use `deliveredAt` for their timestamp field. Older
|
|
97
|
-
* drafts of these docs used `acknowledgedAt` — that is a deprecated alias and
|
|
98
|
-
* should not be used in new code.
|
|
99
|
-
*/
|
|
100
|
-
/**
|
|
101
|
-
* Options passed to {@link OutboxStore.save} for richer write semantics.
|
|
102
|
-
* Stores may ignore fields they don't support — contract is best-effort.
|
|
103
|
-
*/
|
|
104
|
-
interface OutboxWriteOptions {
|
|
105
|
-
/**
|
|
106
|
-
* Host-provided DB session/transaction handle for atomic writes.
|
|
107
|
-
* Typed as `unknown` so stores can accept any backend (mongoose session,
|
|
108
|
-
* pg client, Prisma tx, etc.) without Arc taking a peer dep.
|
|
109
|
-
*/
|
|
110
|
-
readonly session?: unknown;
|
|
111
|
-
/** Earliest time the event should be visible to relay workers (for delayed publishing) */
|
|
112
|
-
readonly visibleAt?: Date;
|
|
113
|
-
/** Idempotency key — stores that support it should dedupe on this */
|
|
114
|
-
readonly dedupeKey?: string;
|
|
115
|
-
/** Partition/routing key for sharded transports (Kafka, Redis Streams) */
|
|
116
|
-
readonly partitionKey?: string;
|
|
117
|
-
/** Arbitrary headers propagated to the transport layer */
|
|
118
|
-
readonly headers?: Readonly<Record<string, string>>;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Options for {@link OutboxStore.claimPending} — lease-based work claim.
|
|
122
|
-
* Supports safe multi-worker relay: each worker atomically claims a batch
|
|
123
|
-
* with a lease TTL, preventing duplicate publishes.
|
|
124
|
-
*/
|
|
125
|
-
interface OutboxClaimOptions {
|
|
126
|
-
/** Max events to claim (default: batchSize) */
|
|
127
|
-
readonly limit?: number;
|
|
128
|
-
/** Unique identifier for the claiming worker */
|
|
129
|
-
readonly consumerId?: string;
|
|
130
|
-
/** Lease duration in ms — claim is released automatically after this */
|
|
131
|
-
readonly leaseMs?: number;
|
|
132
|
-
/** Only claim events of these types (optional filter) */
|
|
133
|
-
readonly types?: readonly string[];
|
|
134
|
-
}
|
|
135
|
-
/** Options for {@link OutboxStore.acknowledge} */
|
|
136
|
-
interface OutboxAcknowledgeOptions {
|
|
137
|
-
/** Worker identifier — stores may enforce "only owner can ack" */
|
|
138
|
-
readonly consumerId?: string;
|
|
139
|
-
}
|
|
140
|
-
/** Options for {@link OutboxStore.fail} */
|
|
141
|
-
interface OutboxFailOptions {
|
|
142
|
-
/** Worker identifier — stores may enforce "only owner can fail" */
|
|
143
|
-
readonly consumerId?: string;
|
|
144
|
-
/** Schedule retry for a later time (implements backoff) */
|
|
145
|
-
readonly retryAt?: Date;
|
|
146
|
-
/** Move the event to dead-letter state (no further retries) */
|
|
147
|
-
readonly deadLetter?: boolean;
|
|
148
|
-
}
|
|
149
|
-
/** Normalized error info passed to {@link OutboxStore.fail} */
|
|
150
|
-
interface OutboxErrorInfo {
|
|
151
|
-
readonly message: string;
|
|
152
|
-
readonly code?: string;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Context passed to {@link OutboxFailurePolicy}.
|
|
88
|
+
* Exponential backoff for outbox failure policies — pure date math shared
|
|
89
|
+
* by `failurePolicy({ attempts })` style handlers.
|
|
156
90
|
*/
|
|
157
|
-
interface OutboxFailureContext {
|
|
158
|
-
/** The event whose delivery just failed */
|
|
159
|
-
readonly event: DomainEvent;
|
|
160
|
-
/** The error returned by the transport / handler */
|
|
161
|
-
readonly error: Error;
|
|
162
|
-
/**
|
|
163
|
-
* Attempt count including this failure (1 on the first fail).
|
|
164
|
-
*
|
|
165
|
-
* Tracked by {@link EventOutbox} in-process — accurate within a single
|
|
166
|
-
* relay process, resets on restart. For durable attempt counts, query
|
|
167
|
-
* your store directly inside the policy.
|
|
168
|
-
*/
|
|
169
|
-
readonly attempts: number;
|
|
170
|
-
}
|
|
171
91
|
/**
|
|
172
|
-
*
|
|
173
|
-
* relay calls `store.fail()` for a failed event.
|
|
92
|
+
* Options for {@link exponentialBackoff}.
|
|
174
93
|
*/
|
|
175
|
-
interface
|
|
176
|
-
/**
|
|
177
|
-
readonly
|
|
94
|
+
interface ExponentialBackoffOptions {
|
|
95
|
+
/** Current attempt count (1-indexed — first retry is attempt 1) */
|
|
96
|
+
readonly attempt: number;
|
|
97
|
+
/** Base delay in ms (first retry delay). Default: 1000 (1 second) */
|
|
98
|
+
readonly baseMs?: number;
|
|
99
|
+
/** Maximum delay in ms — caps exponential growth. Default: 60_000 (1 minute) */
|
|
100
|
+
readonly maxMs?: number;
|
|
178
101
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
102
|
+
* Jitter factor [0–1]. The returned delay is multiplied by
|
|
103
|
+
* `1 + (random * jitter)` to spread retry bursts across workers.
|
|
104
|
+
* Default: 0.2 (±20%). Set to 0 to disable.
|
|
181
105
|
*/
|
|
182
|
-
readonly
|
|
106
|
+
readonly jitter?: number;
|
|
107
|
+
/** Reference time (for deterministic tests). Default: `Date.now()` */
|
|
108
|
+
readonly now?: number;
|
|
183
109
|
}
|
|
184
110
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
111
|
+
* Compute a `retryAt` `Date` using exponential backoff with jitter.
|
|
112
|
+
*
|
|
113
|
+
* This is a convenience helper for store authors implementing
|
|
114
|
+
* {@link OutboxStore.fail}: call it to compute the retry visibility window
|
|
115
|
+
* based on the event's current attempt count.
|
|
187
116
|
*
|
|
188
|
-
*
|
|
117
|
+
* Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
|
|
118
|
+
*
|
|
119
|
+
* @example Basic usage inside a store's `fail()` method
|
|
189
120
|
* ```typescript
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
121
|
+
* async fail(eventId, error, options) {
|
|
122
|
+
* const entry = await this.findById(eventId);
|
|
123
|
+
* entry.attempts++;
|
|
124
|
+
* if (entry.attempts >= MAX_ATTEMPTS) {
|
|
125
|
+
* return this.deadLetter(eventId, error);
|
|
126
|
+
* }
|
|
127
|
+
* const retryAt = exponentialBackoff({ attempt: entry.attempts });
|
|
128
|
+
* entry.visibleAt = retryAt;
|
|
129
|
+
* await this.update(entry);
|
|
193
130
|
* }
|
|
194
131
|
* ```
|
|
195
132
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* Stores that enforce lease ownership MUST throw this (not silently return)
|
|
205
|
-
* so {@link EventOutbox} can detect the mismatch and avoid over-counting
|
|
206
|
-
* successful deliveries. {@link EventOutbox.relay} catches it and reports
|
|
207
|
-
* via {@link EventOutboxOptions.onError} instead of counting as relayed.
|
|
208
|
-
*/
|
|
209
|
-
declare class OutboxOwnershipError extends Error {
|
|
210
|
-
readonly eventId: string;
|
|
211
|
-
readonly attemptedBy: string;
|
|
212
|
-
readonly currentOwner: string | null;
|
|
213
|
-
constructor(eventId: string, attemptedBy: string, currentOwner: string | null);
|
|
214
|
-
}
|
|
215
|
-
/** Thrown by {@link EventOutbox.store} when an event is missing `type` or `meta.id`. */
|
|
216
|
-
declare class InvalidOutboxEventError extends Error {
|
|
217
|
-
constructor(reason: string);
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Durable storage contract for the transactional outbox pattern.
|
|
221
|
-
*
|
|
222
|
-
* **Required methods**: `save`, `getPending`, `acknowledge`.
|
|
223
|
-
*
|
|
224
|
-
* **Optional capabilities** — stores opt-in to richer semantics:
|
|
225
|
-
* - `claimPending` — lease-based work claim for multi-worker relay
|
|
226
|
-
* - `fail` — failure tracking with retry scheduling and dead-letter
|
|
227
|
-
* - `purge` — acknowledged event cleanup
|
|
228
|
-
*
|
|
229
|
-
* Arc's {@link EventOutbox} detects capabilities at runtime and degrades
|
|
230
|
-
* gracefully for legacy stores that only implement the required methods.
|
|
231
|
-
*
|
|
232
|
-
* ## Required semantics
|
|
233
|
-
*
|
|
234
|
-
* Implementations MUST honor these contracts — `EventOutbox` depends on them
|
|
235
|
-
* for correctness of at-least-once delivery:
|
|
236
|
-
*
|
|
237
|
-
* 1. **`save` must reject invalid events.** If `event.type` or `event.meta.id`
|
|
238
|
-
* is missing/empty, throw — do not persist. `EventOutbox.store()` validates
|
|
239
|
-
* first, but stores should defend against direct-save code paths.
|
|
240
|
-
*
|
|
241
|
-
* 2. **`claimPending` must be atomic.** Two workers calling `claimPending`
|
|
242
|
-
* concurrently must never receive the same event. Stores backed by SQL/Mongo
|
|
243
|
-
* should use `SELECT ... FOR UPDATE SKIP LOCKED` or `findOneAndUpdate` with
|
|
244
|
-
* `{ leaseOwner: null }` as the match condition.
|
|
245
|
-
*
|
|
246
|
-
* 3. **`acknowledge` / `fail` must throw {@link OutboxOwnershipError} on
|
|
247
|
-
* ownership mismatch.** When `options.consumerId` is provided and does not
|
|
248
|
-
* match the current lease owner, the method MUST throw — never silently
|
|
249
|
-
* no-op. `EventOutbox.relay` relies on this signal to avoid counting
|
|
250
|
-
* hijacked events as successfully relayed.
|
|
251
|
-
*
|
|
252
|
-
* 4. **`acknowledge` on an unknown `eventId` is a no-op.** This keeps relay
|
|
253
|
-
* idempotent when the store has been purged or the event was manually
|
|
254
|
-
* removed. Do NOT throw `OutboxOwnershipError` in this case.
|
|
255
|
-
*
|
|
256
|
-
* 5. **`fail` must deterministically update lease/visibility.** On success,
|
|
257
|
-
* the event MUST become re-claimable (either immediately or at `retryAt`),
|
|
258
|
-
* or transition to dead-letter state. The lease owner should be cleared.
|
|
259
|
-
*
|
|
260
|
-
* 6. **Malformed events must never be returned by `getPending`/`claimPending`.**
|
|
261
|
-
* If the store's underlying storage has corrupt rows, the store is
|
|
262
|
-
* responsible for quarantining them (e.g., direct DB delete, DLQ).
|
|
133
|
+
* @example Tuning for a faster transport
|
|
134
|
+
* ```typescript
|
|
135
|
+
* exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
|
|
136
|
+
* // attempt=1 → ~250ms ±30%
|
|
137
|
+
* // attempt=2 → ~500ms ±30%
|
|
138
|
+
* // attempt=3 → ~1000ms ±30%
|
|
139
|
+
* // attempt=10 → capped at 10_000ms
|
|
140
|
+
* ```
|
|
263
141
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
*
|
|
268
|
-
* MUST reject events missing `type` or `meta.id` — throw rather than persist.
|
|
269
|
-
*
|
|
270
|
-
* @param event - Event to persist
|
|
271
|
-
* @param options - Optional write metadata (session, visibleAt, dedupeKey, etc.)
|
|
272
|
-
*/
|
|
273
|
-
save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
|
|
274
|
-
/**
|
|
275
|
-
* Get pending (unrelayed) events, ordered FIFO.
|
|
276
|
-
*
|
|
277
|
-
* This is the legacy/simple pull API. Multi-worker deployments should
|
|
278
|
-
* prefer {@link OutboxStore.claimPending} to avoid duplicate publishes.
|
|
279
|
-
*
|
|
280
|
-
* Events returned by this method MUST be well-formed (valid `type` and
|
|
281
|
-
* `meta.id`). Corrupt rows must be quarantined by the store, not exposed
|
|
282
|
-
* to the relay loop.
|
|
283
|
-
*/
|
|
284
|
-
getPending(limit: number): Promise<DomainEvent[]>;
|
|
285
|
-
/**
|
|
286
|
-
* Atomically claim pending events with a lease.
|
|
287
|
-
*
|
|
288
|
-
* When implemented, {@link EventOutbox.relay} prefers this over `getPending`
|
|
289
|
-
* so multi-worker relay is safe: each worker holds an exclusive lease on
|
|
290
|
-
* its batch, and stale leases are automatically recovered.
|
|
291
|
-
*
|
|
292
|
-
* **Atomicity is mandatory**: two concurrent callers must never receive
|
|
293
|
-
* overlapping events. Use `SELECT ... FOR UPDATE SKIP LOCKED` (SQL) or
|
|
294
|
-
* a compound condition on `findOneAndUpdate` (Mongo).
|
|
295
|
-
*/
|
|
296
|
-
claimPending?(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
|
|
297
|
-
/**
|
|
298
|
-
* Mark event as successfully relayed.
|
|
299
|
-
*
|
|
300
|
-
* **Ownership contract**: If `options.consumerId` is provided and does not
|
|
301
|
-
* match the current lease owner, this method MUST throw
|
|
302
|
-
* {@link OutboxOwnershipError}. Unknown `eventId` is a no-op.
|
|
303
|
-
*
|
|
304
|
-
* @param eventId - Event ID (from `meta.id`)
|
|
305
|
-
* @param options - Optional ack metadata (consumerId for lease enforcement)
|
|
306
|
-
* @throws {@link OutboxOwnershipError} on ownership mismatch
|
|
307
|
-
*/
|
|
308
|
-
acknowledge(eventId: string, options?: OutboxAcknowledgeOptions): Promise<void>;
|
|
309
|
-
/**
|
|
310
|
-
* Record a relay failure. When implemented, {@link EventOutbox.relay} calls
|
|
311
|
-
* this instead of stopping the batch — enables retry scheduling and DLQ.
|
|
312
|
-
*
|
|
313
|
-
* **Ownership contract**: Same as `acknowledge` — MUST throw
|
|
314
|
-
* {@link OutboxOwnershipError} on mismatch. After a successful call, the
|
|
315
|
-
* lease owner MUST be cleared and the event MUST be re-claimable (at
|
|
316
|
-
* `retryAt` if provided) or transitioned to dead-letter.
|
|
317
|
-
*
|
|
318
|
-
* @throws {@link OutboxOwnershipError} on ownership mismatch
|
|
319
|
-
*/
|
|
320
|
-
fail?(eventId: string, error: OutboxErrorInfo, options?: OutboxFailOptions): Promise<void>;
|
|
321
|
-
/**
|
|
322
|
-
* Return events currently in dead-letter state as typed
|
|
323
|
-
* {@link DeadLetteredEvent} envelopes.
|
|
324
|
-
*
|
|
325
|
-
* Closes the loop between {@link OutboxStore.fail} (with `deadLetter: true`)
|
|
326
|
-
* and {@link import('./EventTransport.js').DeadLetteredEvent} — apps get the
|
|
327
|
-
* same shape out of the outbox that {@link import('./retry.js').withRetry}
|
|
328
|
-
* delivers to transports. No hand-building DLQ envelopes.
|
|
329
|
-
*
|
|
330
|
-
* Implementations MUST populate `error`, `attempts`, and both
|
|
331
|
-
* `firstFailedAt` / `lastFailedAt` from whatever they tracked during `fail()`
|
|
332
|
-
* calls. `handlerName` is optional — stores that don't track it can omit.
|
|
333
|
-
*
|
|
334
|
-
* @param limit - Max envelopes to return
|
|
335
|
-
*/
|
|
336
|
-
getDeadLettered?(limit: number): Promise<DeadLetteredEvent[]>;
|
|
337
|
-
/**
|
|
338
|
-
* Purge old **delivered** events (optional, DB-agnostic contract).
|
|
339
|
-
*
|
|
340
|
-
* Cleanup is scoped to events in the `delivered` state — events still
|
|
341
|
-
* pending, failed, or dead-lettered MUST NOT be removed by purge.
|
|
342
|
-
*
|
|
343
|
-
* Arc does **not** ship a concrete implementation — your store owns the
|
|
344
|
-
* cleanup strategy that fits your database:
|
|
345
|
-
*
|
|
346
|
-
* - **MongoDB:** TTL index on `deliveredAt` (automatic, zero-code)
|
|
347
|
-
* - **SQL:** Scheduled `DELETE FROM outbox WHERE status = 'delivered' AND delivered_at < :cutoff`
|
|
348
|
-
* - **Redis:** Key expiry (`EXPIRE`) on delivered entries
|
|
349
|
-
*
|
|
350
|
-
* Called by {@link EventOutbox.purge}. If not implemented, cleanup is
|
|
351
|
-
* entirely the app's responsibility via native DB tools.
|
|
352
|
-
*
|
|
353
|
-
* @param olderThanMs - Remove events delivered more than this many ms ago
|
|
354
|
-
* @returns Number of purged events
|
|
355
|
-
*/
|
|
356
|
-
purge?(olderThanMs: number): Promise<number>;
|
|
357
|
-
}
|
|
142
|
+
declare function exponentialBackoff(options: ExponentialBackoffOptions): Date;
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/events/outbox/relay.d.ts
|
|
358
145
|
/** Reason codes passed to {@link EventOutboxOptions.onError}. */
|
|
359
146
|
type OutboxRelayErrorKind = "publish_failed" | "acknowledge_failed" | "fail_failed" | "ownership_mismatch" | "malformed_event";
|
|
360
147
|
/**
|
|
@@ -387,6 +174,14 @@ interface RelayResult {
|
|
|
387
174
|
readonly deadLettered: number;
|
|
388
175
|
/** Whether `publishMany` was used (true) or per-event `publish` (false) */
|
|
389
176
|
readonly usedPublishMany: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Wall-clock ms spent claiming this batch from the store. With the
|
|
179
|
+
* portable repository adapter this is ~2 DB round trips; a persistently
|
|
180
|
+
* high value under low batch sizes points at store latency, not volume.
|
|
181
|
+
*/
|
|
182
|
+
readonly claimMs?: number;
|
|
183
|
+
/** Wall-clock ms spent publishing + acknowledging the batch. */
|
|
184
|
+
readonly publishMs?: number;
|
|
390
185
|
}
|
|
391
186
|
/**
|
|
392
187
|
* Called by {@link EventOutbox.relay} when a non-fatal error occurs during
|
|
@@ -598,58 +393,6 @@ declare class EventOutbox {
|
|
|
598
393
|
*/
|
|
599
394
|
purge(olderThanMs?: number): Promise<number>;
|
|
600
395
|
}
|
|
601
|
-
/**
|
|
602
|
-
* Options for {@link exponentialBackoff}.
|
|
603
|
-
*/
|
|
604
|
-
interface ExponentialBackoffOptions {
|
|
605
|
-
/** Current attempt count (1-indexed — first retry is attempt 1) */
|
|
606
|
-
readonly attempt: number;
|
|
607
|
-
/** Base delay in ms (first retry delay). Default: 1000 (1 second) */
|
|
608
|
-
readonly baseMs?: number;
|
|
609
|
-
/** Maximum delay in ms — caps exponential growth. Default: 60_000 (1 minute) */
|
|
610
|
-
readonly maxMs?: number;
|
|
611
|
-
/**
|
|
612
|
-
* Jitter factor [0–1]. The returned delay is multiplied by
|
|
613
|
-
* `1 + (random * jitter)` to spread retry bursts across workers.
|
|
614
|
-
* Default: 0.2 (±20%). Set to 0 to disable.
|
|
615
|
-
*/
|
|
616
|
-
readonly jitter?: number;
|
|
617
|
-
/** Reference time (for deterministic tests). Default: `Date.now()` */
|
|
618
|
-
readonly now?: number;
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Compute a `retryAt` `Date` using exponential backoff with jitter.
|
|
622
|
-
*
|
|
623
|
-
* This is a convenience helper for store authors implementing
|
|
624
|
-
* {@link OutboxStore.fail}: call it to compute the retry visibility window
|
|
625
|
-
* based on the event's current attempt count.
|
|
626
|
-
*
|
|
627
|
-
* Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
|
|
628
|
-
*
|
|
629
|
-
* @example Basic usage inside a store's `fail()` method
|
|
630
|
-
* ```typescript
|
|
631
|
-
* async fail(eventId, error, options) {
|
|
632
|
-
* const entry = await this.findById(eventId);
|
|
633
|
-
* entry.attempts++;
|
|
634
|
-
* if (entry.attempts >= MAX_ATTEMPTS) {
|
|
635
|
-
* return this.deadLetter(eventId, error);
|
|
636
|
-
* }
|
|
637
|
-
* const retryAt = exponentialBackoff({ attempt: entry.attempts });
|
|
638
|
-
* entry.visibleAt = retryAt;
|
|
639
|
-
* await this.update(entry);
|
|
640
|
-
* }
|
|
641
|
-
* ```
|
|
642
|
-
*
|
|
643
|
-
* @example Tuning for a faster transport
|
|
644
|
-
* ```typescript
|
|
645
|
-
* exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
|
|
646
|
-
* // attempt=1 → ~250ms ±30%
|
|
647
|
-
* // attempt=2 → ~500ms ±30%
|
|
648
|
-
* // attempt=3 → ~1000ms ±30%
|
|
649
|
-
* // attempt=10 → capped at 10_000ms
|
|
650
|
-
* ```
|
|
651
|
-
*/
|
|
652
|
-
declare function exponentialBackoff(options: ExponentialBackoffOptions): Date;
|
|
653
396
|
//#endregion
|
|
654
397
|
//#region src/events/repository-outbox-adapter.d.ts
|
|
655
398
|
declare function repositoryAsOutboxStore(repository: RepositoryLike): OutboxStore;
|
|
@@ -813,4 +556,4 @@ interface FastifyEventBus {
|
|
|
813
556
|
};
|
|
814
557
|
}
|
|
815
558
|
//#endregion
|
|
816
|
-
export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type EventDefinitionInput, type EventDefinitionOutput, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventRegistryOptions, type EventSchema, type ExponentialBackoffOptions,
|
|
559
|
+
export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type EventDefinitionInput, type EventDefinitionOutput, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventRegistryOptions, type EventSchema, type ExponentialBackoffOptions, MemoryEventTransport, type MemoryEventTransportOptions, MemoryOutboxStore, type OutboxRelayErrorHandler, type OutboxRelayErrorKind, type PayloadOf, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RelayResult, type RetryOptions, type ValidationResult, type WrapWithBoundaryOptions, type WrapWithSchemaOptions, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
|