@classytic/arc 2.8.5 → 2.9.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 +88 -5
- package/dist/{BaseController-DAGGc5Xn.mjs → BaseController-Vu2yc56T.mjs} +188 -102
- package/dist/EventTransport-CqZ8FyM_.d.mts +293 -0
- package/dist/adapters/index.d.mts +2 -2
- package/dist/audit/index.d.mts +100 -11
- package/dist/audit/index.mjs +71 -18
- package/dist/auth/index.d.mts +16 -8
- package/dist/auth/index.mjs +13 -6
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-BuUcUEJq.mjs → betterAuthOpenApi--rdY15Ld.mjs} +1 -1
- package/dist/cache/index.d.mts +2 -2
- package/dist/cache/index.mjs +2 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -5
- package/dist/{core-F0QoWBt2.mjs → core-DNncu0xF.mjs} +1 -1
- package/dist/{createActionRouter-BORM8f17.mjs → createActionRouter-DH1YFL9m.mjs} +3 -3
- package/dist/{createApp-B1EY8zxa.mjs → createApp-CBJUJKGP.mjs} +13 -12
- package/dist/{defineResource-tcgySDo1.mjs → defineResource-C__jkwvs.mjs} +22 -57
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/dynamic/index.d.mts +1 -1
- package/dist/dynamic/index.mjs +3 -3
- package/dist/{elevation-DtFxrG0s.mjs → elevation-DxQ6ACbt.mjs} +21 -7
- package/dist/{errorHandler-f869_8PQ.mjs → errorHandler-CZDW4EXS.mjs} +59 -7
- package/dist/{errorHandler-Bah5JhBd.d.mts → errorHandler-DixGcttC.d.mts} +37 -2
- package/dist/{eventPlugin-D9DKB2zM.d.mts → eventPlugin-BxvaCIZF.d.mts} +14 -2
- package/dist/{eventPlugin-CDjVTM82.mjs → eventPlugin-Dl7MoVWH.mjs} +83 -5
- package/dist/events/index.d.mts +147 -36
- package/dist/events/index.mjs +338 -101
- 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 +2 -2
- package/dist/{fields-DpZQa_Q3.d.mts → fields-BC7zcmI9.d.mts} +15 -3
- package/dist/{fields-ipsbIRPK.mjs → fields-CU6FlaDV.mjs} +18 -5
- package/dist/{filesUpload-C7r7HIeA.mjs → filesUpload-q8oHt--L.mjs} +65 -7
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +29 -5
- package/dist/idempotency/index.mjs +111 -2
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-BLXBmWud.d.mts → index-C-xjcA6F.d.mts} +1 -1
- package/dist/{index-DtDzOBn8.d.mts → index-Cibkchnx.d.mts} +3 -134
- package/dist/{index-C1meYuDn.d.mts → index-CtGKT0lf.d.mts} +1 -1
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +9 -9
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +26 -8
- package/dist/integrations/mcp/index.mjs +96 -17
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +5 -0
- package/dist/integrations/webhooks.mjs +6 -0
- package/dist/{interface-CMRutPfe.d.mts → interface-YrWsmKqE.d.mts} +287 -179
- package/dist/{openapi-CbKUJY_m.mjs → openapi-CXuTG1M9.mjs} +2 -2
- package/dist/org/index.d.mts +1 -1
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-CH4cNwJi.mjs → permissions-oNZawnkR.mjs} +1 -1
- package/dist/plugins/index.d.mts +7 -7
- package/dist/plugins/index.mjs +11 -11
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/policies/index.d.mts +25 -32
- package/dist/presets/filesUpload.d.mts +26 -4
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +3 -2
- package/dist/presets/index.mjs +4 -3
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +91 -0
- package/dist/presets/search.mjs +150 -0
- package/dist/{presets-C2xgzW6x.mjs → presets-hM4WhNWY.mjs} +1 -1
- package/dist/{queryCachePlugin-BJJGBTlu.d.mts → queryCachePlugin-CnTZZTC5.d.mts} +1 -1
- package/dist/{queryCachePlugin-BH-fidlv.mjs → queryCachePlugin-DbUVroUG.mjs} +2 -2
- package/dist/{redis-BM00zaPB.d.mts → redis-MXLp1oOf.d.mts} +1 -1
- package/dist/{redis-stream-CrsfUmPt.d.mts → redis-stream-Bz-4q96t.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{resourceToTools-8s-EsCCe.mjs → resourceToTools-C3cWymnW.mjs} +64 -47
- package/dist/rpc/index.d.mts +1 -1
- package/dist/rpc/index.mjs +1 -1
- package/dist/{schemaConverter-Y7nCYaLJ.mjs → schemaConverter-BxFDdtXu.mjs} +1 -1
- package/dist/scope/index.mjs +1 -1
- package/dist/{sse-Ad7ypl9e.mjs → sse-CJpt7LGI.mjs} +1 -1
- package/dist/store-helpers-DFiZl5TL.mjs +57 -0
- package/dist/testing/index.d.mts +5 -14
- package/dist/testing/index.mjs +21 -75
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +2 -2
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-BsbNMEDR.d.mts → types-CoSzA-s-.d.mts} +1 -1
- package/dist/{types-Ch9pTQbf.d.mts → types-CunEX4UX.d.mts} +10 -8
- package/dist/utils/index.d.mts +4 -4
- package/dist/utils/index.mjs +6 -6
- package/dist/{utils-yYT3HDXt.mjs → utils-B7FuRr9w.mjs} +1 -1
- package/package.json +8 -11
- package/skills/arc/SKILL.md +92 -14
- package/skills/arc/references/auth.md +94 -0
- package/skills/arc/references/events.md +200 -12
- package/skills/arc/references/mcp.md +4 -17
- package/skills/arc/references/multi-tenancy.md +43 -0
- package/skills/arc/references/production.md +34 -19
- package/dist/EventTransport-BXja8NOc.d.mts +0 -135
- package/dist/audit/mongodb.d.mts +0 -2
- package/dist/audit/mongodb.mjs +0 -2
- package/dist/idempotency/mongodb.d.mts +0 -2
- package/dist/idempotency/mongodb.mjs +0 -123
- package/dist/mongodb-BsP-WbhN.d.mts +0 -127
- package/dist/mongodb-CTcp0hQZ.d.mts +0 -80
- package/dist/mongodb-Utc5k_-0.mjs +0 -90
- /package/dist/{HookSystem-HprTmvVY.mjs → HookSystem-BjFu7zf1.mjs} +0 -0
- /package/dist/{ResourceRegistry-C6uXlWe3.mjs → ResourceRegistry-Dq3_zBQP.mjs} +0 -0
- /package/dist/{applyPermissionResult-D6GPMsvh.mjs → applyPermissionResult-bqGpo9ML.mjs} +0 -0
- /package/dist/{caching-IMuYVjTL.mjs → caching-CjybdRwx.mjs} +0 -0
- /package/dist/{circuitBreaker-dTtG-UyS.d.mts → circuitBreaker-CvXkjfrW.d.mts} +0 -0
- /package/dist/{circuitBreaker-cmi5XDv5.mjs → circuitBreaker-l18oRgL5.mjs} +0 -0
- /package/dist/{errors-Ck2h67pm.d.mts → errors-BI8kEKsO.d.mts} +0 -0
- /package/dist/{errors-BF2bIOIS.mjs → errors-CqWnSqM-.mjs} +0 -0
- /package/dist/{externalPaths-BnkYrNzp.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
- /package/dist/{interface-DfLGcus7.d.mts → interface-B-pe8fhj.d.mts} +0 -0
- /package/dist/{interface-4y979v99.d.mts → interface-DplgQO2e.d.mts} +0 -0
- /package/dist/{loadResources-PWd0OCpV.mjs → loadResources-Bksk8ydA.mjs} +0 -0
- /package/dist/{logger-D1YrIImS.mjs → logger-CDjpjySd.mjs} +0 -0
- /package/dist/{memory-Cp7_cAko.mjs → memory-BFAYkf8H.mjs} +0 -0
- /package/dist/{metrics-B-PU4-Yu.mjs → metrics-TuOmguhi.mjs} +0 -0
- /package/dist/{queryParser-CgCtsjti.mjs → queryParser-Cs-6SHQK.mjs} +0 -0
- /package/dist/{registry-BiTKT1Dg.mjs → registry-B0Wl7uVV.mjs} +0 -0
- /package/dist/{replyHelpers-CxkYGT81.mjs → replyHelpers-BLojtuvR.mjs} +0 -0
- /package/dist/{requestContext-DYvHl113.mjs → requestContext-DYtmNpm5.mjs} +0 -0
- /package/dist/{sessionManager-DDCmiNIo.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
- /package/dist/{storage-Dfzt4VTl.d.mts → storage-BwGQXUpd.d.mts} +0 -0
- /package/dist/{tracing-DdN2-wHJ.d.mts → tracing-xqXzWeaf.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-Cj5Rgvlg.mjs} +0 -0
- /package/dist/{versioning-CDugduqI.mjs → versioning-Cm8qoFDg.mjs} +0 -0
|
@@ -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-DYtmNpm5.mjs";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
//#region src/events/EventTransport.ts
|
|
5
5
|
/**
|
|
@@ -67,7 +67,10 @@ var MemoryEventTransport = class {
|
|
|
67
67
|
}
|
|
68
68
|
};
|
|
69
69
|
/**
|
|
70
|
-
* Create a domain event with auto-generated metadata
|
|
70
|
+
* Create a domain event with auto-generated metadata.
|
|
71
|
+
*
|
|
72
|
+
* `id` and `timestamp` are filled in; everything else is caller-controlled.
|
|
73
|
+
* Set `schemaVersion` explicitly for any event type you plan to evolve.
|
|
71
74
|
*/
|
|
72
75
|
function createEvent(type, payload, meta) {
|
|
73
76
|
return {
|
|
@@ -80,6 +83,55 @@ function createEvent(type, payload, meta) {
|
|
|
80
83
|
}
|
|
81
84
|
};
|
|
82
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Create a child event that chains causation from a parent event.
|
|
88
|
+
*
|
|
89
|
+
* Rules:
|
|
90
|
+
* - `causationId` is set to the parent's `id` (direct cause)
|
|
91
|
+
* - `correlationId` is inherited from the parent if set, else falls back
|
|
92
|
+
* to the parent's `id` (root correlation)
|
|
93
|
+
* - `userId` / `organizationId` are inherited when not overridden so the
|
|
94
|
+
* whole chain stays scoped to the originating principal/tenant
|
|
95
|
+
*
|
|
96
|
+
* Caller-supplied `meta` wins over inherited fields — pass `{ userId: newActor }`
|
|
97
|
+
* to override when a subsystem acts on behalf of a different principal.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const orderPlaced = createEvent('order.placed', { orderId: 'o1' }, {
|
|
102
|
+
* correlationId: req.id, userId: user.id,
|
|
103
|
+
* });
|
|
104
|
+
* await events.publish(orderPlaced);
|
|
105
|
+
*
|
|
106
|
+
* // Downstream handler emits a child event:
|
|
107
|
+
* const reserved = createChildEvent(orderPlaced, 'inventory.reserved', {
|
|
108
|
+
* orderId: 'o1', skus: ['sku-1', 'sku-2'],
|
|
109
|
+
* });
|
|
110
|
+
* // reserved.meta.causationId === orderPlaced.meta.id
|
|
111
|
+
* // reserved.meta.correlationId === orderPlaced.meta.correlationId
|
|
112
|
+
* // reserved.meta.userId === user.id (inherited)
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
function createChildEvent(parent, type, payload, meta) {
|
|
116
|
+
const inherited = {
|
|
117
|
+
correlationId: parent.meta.correlationId ?? parent.meta.id,
|
|
118
|
+
causationId: parent.meta.id
|
|
119
|
+
};
|
|
120
|
+
if (parent.meta.userId !== void 0) inherited.userId = parent.meta.userId;
|
|
121
|
+
if (parent.meta.organizationId !== void 0) inherited.organizationId = parent.meta.organizationId;
|
|
122
|
+
if (parent.meta.source !== void 0) inherited.source = parent.meta.source;
|
|
123
|
+
if (parent.meta.idempotencyKey !== void 0) inherited.idempotencyKey = parent.meta.idempotencyKey;
|
|
124
|
+
return {
|
|
125
|
+
type,
|
|
126
|
+
payload,
|
|
127
|
+
meta: {
|
|
128
|
+
id: crypto.randomUUID(),
|
|
129
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
130
|
+
...inherited,
|
|
131
|
+
...meta
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
83
135
|
//#endregion
|
|
84
136
|
//#region src/events/retry.ts
|
|
85
137
|
/**
|
|
@@ -89,16 +141,21 @@ function createEvent(type, payload, meta) {
|
|
|
89
141
|
* After all retries exhausted, calls `onDead` callback if provided.
|
|
90
142
|
*/
|
|
91
143
|
function withRetry(handler, options = {}) {
|
|
92
|
-
const { maxRetries = 3, backoffMs = 1e3, maxBackoffMs = 3e4, jitter = .1, onDead, name, logger = console } = options;
|
|
144
|
+
const { maxRetries = 3, backoffMs = 1e3, maxBackoffMs = 3e4, jitter = .1, transport, onDead, name, logger = console } = options;
|
|
93
145
|
const label = name ?? handler.name ?? "anonymous";
|
|
94
146
|
return async (event) => {
|
|
95
147
|
const errors = [];
|
|
148
|
+
let firstFailedAt;
|
|
149
|
+
let lastFailedAt;
|
|
96
150
|
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
97
151
|
await handler(event);
|
|
98
152
|
return;
|
|
99
153
|
} catch (err) {
|
|
100
154
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
155
|
+
const now = /* @__PURE__ */ new Date();
|
|
101
156
|
errors.push(error);
|
|
157
|
+
if (firstFailedAt === void 0) firstFailedAt = now;
|
|
158
|
+
lastFailedAt = now;
|
|
102
159
|
if (attempt < maxRetries) {
|
|
103
160
|
const baseDelay = Math.min(backoffMs * 2 ** attempt, maxBackoffMs);
|
|
104
161
|
const delay = baseDelay + jitter * baseDelay * Math.random();
|
|
@@ -106,7 +163,28 @@ function withRetry(handler, options = {}) {
|
|
|
106
163
|
await sleep(delay);
|
|
107
164
|
}
|
|
108
165
|
}
|
|
109
|
-
|
|
166
|
+
const attempts = maxRetries + 1;
|
|
167
|
+
logger.error(`[Arc Events] Handler '${label}' permanently failed for ${event.type} after ${attempts} attempts. ${errors.length} errors.`);
|
|
168
|
+
if (transport?.deadLetter) {
|
|
169
|
+
const lastError = errors[errors.length - 1];
|
|
170
|
+
const dlq = {
|
|
171
|
+
event,
|
|
172
|
+
error: {
|
|
173
|
+
message: lastError?.message ?? "unknown",
|
|
174
|
+
...lastError && "code" in lastError && typeof lastError.code === "string" ? { code: lastError.code } : {},
|
|
175
|
+
...lastError?.stack ? { stack: lastError.stack } : {}
|
|
176
|
+
},
|
|
177
|
+
attempts,
|
|
178
|
+
firstFailedAt: firstFailedAt ?? /* @__PURE__ */ new Date(),
|
|
179
|
+
lastFailedAt: lastFailedAt ?? /* @__PURE__ */ new Date(),
|
|
180
|
+
handlerName: label
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
await transport.deadLetter(dlq);
|
|
184
|
+
} catch (dlqErr) {
|
|
185
|
+
logger.error("[Arc Events] transport.deadLetter() failed:", dlqErr);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
110
188
|
if (onDead) try {
|
|
111
189
|
await onDead(event, errors);
|
|
112
190
|
} catch (dlqErr) {
|
|
@@ -243,4 +321,4 @@ var eventPlugin_default = fp(eventPlugin, {
|
|
|
243
321
|
fastify: "5.x"
|
|
244
322
|
});
|
|
245
323
|
//#endregion
|
|
246
|
-
export { MemoryEventTransport as a, withRetry as i, eventPlugin_exports as n,
|
|
324
|
+
export { MemoryEventTransport as a, withRetry as i, eventPlugin_exports as n, createChildEvent as o, createDeadLetterPublisher as r, createEvent as s, eventPlugin as t };
|
package/dist/events/index.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
1
|
+
import { o as RepositoryLike } from "../interface-YrWsmKqE.mjs";
|
|
2
|
+
import { a as EventMeta, c as MemoryEventTransportOptions, d as createEvent, i as EventLogger, l as PublishManyResult, n as DomainEvent, o as EventTransport, r as EventHandler, s as MemoryEventTransport, t as DeadLetteredEvent, u as createChildEvent } from "../EventTransport-CqZ8FyM_.mjs";
|
|
3
|
+
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-BxvaCIZF.mjs";
|
|
3
4
|
import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
|
|
4
|
-
import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-
|
|
5
|
+
import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-Bz-4q96t.mjs";
|
|
5
6
|
|
|
6
7
|
//#region src/events/eventTypes.d.ts
|
|
7
8
|
/**
|
|
@@ -50,6 +51,34 @@ declare const CACHE_EVENTS: Readonly<{
|
|
|
50
51
|
/** Type for cache event names */
|
|
51
52
|
type CacheEvent = (typeof CACHE_EVENTS)[keyof typeof CACHE_EVENTS];
|
|
52
53
|
//#endregion
|
|
54
|
+
//#region src/events/memory-outbox.d.ts
|
|
55
|
+
interface MemoryEntry {
|
|
56
|
+
event: DomainEvent;
|
|
57
|
+
status: "pending" | "delivered" | "dead_letter";
|
|
58
|
+
attempts: number;
|
|
59
|
+
visibleAt: number;
|
|
60
|
+
leaseOwner: string | null;
|
|
61
|
+
leaseExpiresAt: number;
|
|
62
|
+
deliveredAt: number | null;
|
|
63
|
+
firstFailedAt: number | null;
|
|
64
|
+
lastFailedAt: number | null;
|
|
65
|
+
lastError: OutboxErrorInfo | null;
|
|
66
|
+
dedupeKey?: string;
|
|
67
|
+
}
|
|
68
|
+
declare class MemoryOutboxStore implements OutboxStore {
|
|
69
|
+
private readonly entries;
|
|
70
|
+
private readonly seenDedupeKeys;
|
|
71
|
+
save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
|
|
72
|
+
getPending(limit: number): Promise<DomainEvent[]>;
|
|
73
|
+
claimPending(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
|
|
74
|
+
acknowledge(eventId: string, options?: OutboxAcknowledgeOptions): Promise<void>;
|
|
75
|
+
fail(eventId: string, error: OutboxErrorInfo, options?: OutboxFailOptions): Promise<void>;
|
|
76
|
+
getDeadLettered(limit: number): Promise<DeadLetteredEvent[]>;
|
|
77
|
+
purge(olderThanMs: number): Promise<number>;
|
|
78
|
+
/** Test helper: inspect entry by id */
|
|
79
|
+
_getEntry(eventId: string): Readonly<MemoryEntry> | undefined;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
53
82
|
//#region src/events/outbox.d.ts
|
|
54
83
|
/**
|
|
55
84
|
* **Terminology (v2.8.1+):**
|
|
@@ -119,6 +148,52 @@ interface OutboxErrorInfo {
|
|
|
119
148
|
readonly message: string;
|
|
120
149
|
readonly code?: string;
|
|
121
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Context passed to {@link OutboxFailurePolicy}.
|
|
153
|
+
*/
|
|
154
|
+
interface OutboxFailureContext {
|
|
155
|
+
/** The event whose delivery just failed */
|
|
156
|
+
readonly event: DomainEvent;
|
|
157
|
+
/** The error returned by the transport / handler */
|
|
158
|
+
readonly error: Error;
|
|
159
|
+
/**
|
|
160
|
+
* Attempt count including this failure (1 on the first fail).
|
|
161
|
+
*
|
|
162
|
+
* Tracked by {@link EventOutbox} in-process — accurate within a single
|
|
163
|
+
* relay process, resets on restart. For durable attempt counts, query
|
|
164
|
+
* your store directly inside the policy.
|
|
165
|
+
*/
|
|
166
|
+
readonly attempts: number;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Decision returned by {@link OutboxFailurePolicy} — controls how the
|
|
170
|
+
* relay calls `store.fail()` for a failed event.
|
|
171
|
+
*/
|
|
172
|
+
interface OutboxFailureDecision {
|
|
173
|
+
/** Schedule retry for this time. Omit for immediate (next-poll) retry. */
|
|
174
|
+
readonly retryAt?: Date;
|
|
175
|
+
/**
|
|
176
|
+
* Move the event to dead-letter state (no further retries). When `true`,
|
|
177
|
+
* {@link RelayResult.deadLettered} is incremented.
|
|
178
|
+
*/
|
|
179
|
+
readonly deadLetter?: boolean;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Centralised retry/DLQ policy evaluated by {@link EventOutbox.relayBatch}
|
|
183
|
+
* on every failure. Returns the options passed to `store.fail()`.
|
|
184
|
+
*
|
|
185
|
+
* Typical shape:
|
|
186
|
+
* ```typescript
|
|
187
|
+
* failurePolicy: ({ attempts, error }) => {
|
|
188
|
+
* if (attempts >= 5) return { deadLetter: true };
|
|
189
|
+
* return { retryAt: exponentialBackoff({ attempt: attempts }) };
|
|
190
|
+
* }
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* Without a policy, the relay uses the legacy "immediate re-visibility" fail
|
|
194
|
+
* behaviour (each fail() is equivalent to `{}`).
|
|
195
|
+
*/
|
|
196
|
+
type OutboxFailurePolicy = (ctx: OutboxFailureContext) => OutboxFailureDecision | Promise<OutboxFailureDecision>;
|
|
122
197
|
/**
|
|
123
198
|
* Thrown by a store when `acknowledge` / `fail` is called by a consumer that
|
|
124
199
|
* does not own the event's current lease.
|
|
@@ -240,6 +315,22 @@ interface OutboxStore {
|
|
|
240
315
|
* @throws {@link OutboxOwnershipError} on ownership mismatch
|
|
241
316
|
*/
|
|
242
317
|
fail?(eventId: string, error: OutboxErrorInfo, options?: OutboxFailOptions): Promise<void>;
|
|
318
|
+
/**
|
|
319
|
+
* Return events currently in dead-letter state as typed
|
|
320
|
+
* {@link DeadLetteredEvent} envelopes.
|
|
321
|
+
*
|
|
322
|
+
* Closes the loop between {@link OutboxStore.fail} (with `deadLetter: true`)
|
|
323
|
+
* and {@link import('./EventTransport.js').DeadLetteredEvent} — apps get the
|
|
324
|
+
* same shape out of the outbox that {@link import('./retry.js').withRetry}
|
|
325
|
+
* delivers to transports. No hand-building DLQ envelopes.
|
|
326
|
+
*
|
|
327
|
+
* Implementations MUST populate `error`, `attempts`, and both
|
|
328
|
+
* `firstFailedAt` / `lastFailedAt` from whatever they tracked during `fail()`
|
|
329
|
+
* calls. `handlerName` is optional — stores that don't track it can omit.
|
|
330
|
+
*
|
|
331
|
+
* @param limit - Max envelopes to return
|
|
332
|
+
*/
|
|
333
|
+
getDeadLettered?(limit: number): Promise<DeadLetteredEvent[]>;
|
|
243
334
|
/**
|
|
244
335
|
* Purge old **delivered** events (optional, DB-agnostic contract).
|
|
245
336
|
*
|
|
@@ -285,6 +376,12 @@ interface RelayResult {
|
|
|
285
376
|
readonly malformed: number;
|
|
286
377
|
/** Number of fail() calls that themselves threw (store bugs / contention) */
|
|
287
378
|
readonly failHookErrors: number;
|
|
379
|
+
/**
|
|
380
|
+
* Number of events moved to dead-letter state this batch via the configured
|
|
381
|
+
* {@link OutboxFailurePolicy}. Zero when no policy is set or no failure
|
|
382
|
+
* tripped the `deadLetter` branch.
|
|
383
|
+
*/
|
|
384
|
+
readonly deadLettered: number;
|
|
288
385
|
/** Whether `publishMany` was used (true) or per-event `publish` (false) */
|
|
289
386
|
readonly usedPublishMany: boolean;
|
|
290
387
|
}
|
|
@@ -298,8 +395,23 @@ type OutboxRelayErrorHandler = (info: {
|
|
|
298
395
|
readonly error: Error;
|
|
299
396
|
}) => void;
|
|
300
397
|
interface EventOutboxOptions {
|
|
301
|
-
/**
|
|
302
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Repository managing the outbox collection. Arc consumes it directly —
|
|
400
|
+
* no wrapper classes. Requires `create`, `getOne`, `findAll`,
|
|
401
|
+
* `deleteMany`, and `findOneAndUpdate` from `RepositoryLike` (mongokit
|
|
402
|
+
* ≥3.8 satisfies all of these). Takes precedence over `store` when both
|
|
403
|
+
* are passed.
|
|
404
|
+
*
|
|
405
|
+
* Use this for the common path where the outbox lives in your primary
|
|
406
|
+
* database. Use `store` for non-repository backends (memory / custom).
|
|
407
|
+
*/
|
|
408
|
+
readonly repository?: RepositoryLike;
|
|
409
|
+
/**
|
|
410
|
+
* Non-repository outbox store. Use when your backend isn't a repository
|
|
411
|
+
* (memory for tests, Kafka, DynamoDB, custom). Ignored if `repository`
|
|
412
|
+
* is also passed.
|
|
413
|
+
*/
|
|
414
|
+
readonly store?: OutboxStore;
|
|
303
415
|
/** Transport to relay events to (optional — can relay later) */
|
|
304
416
|
readonly transport?: EventTransport;
|
|
305
417
|
/** Max events per relay batch (default: 100) */
|
|
@@ -327,6 +439,17 @@ interface EventOutboxOptions {
|
|
|
327
439
|
* throughput, or to debug batch-specific issues.
|
|
328
440
|
*/
|
|
329
441
|
readonly usePublishMany?: boolean;
|
|
442
|
+
/**
|
|
443
|
+
* Retry/DLQ decision policy. When set, {@link EventOutbox.relayBatch}
|
|
444
|
+
* invokes this on every failure and uses the returned options for
|
|
445
|
+
* `store.fail()`. Centralises the "after N fails, dead-letter" rule so
|
|
446
|
+
* apps don't recompute `exponentialBackoff` + escalation thresholds on
|
|
447
|
+
* every failure site.
|
|
448
|
+
*
|
|
449
|
+
* Without a policy, `fail()` is called with `{}` (immediate re-visibility
|
|
450
|
+
* on next poll) — legacy behaviour, unchanged.
|
|
451
|
+
*/
|
|
452
|
+
readonly failurePolicy?: OutboxFailurePolicy;
|
|
330
453
|
}
|
|
331
454
|
declare class EventOutbox {
|
|
332
455
|
private readonly _store;
|
|
@@ -336,6 +459,14 @@ declare class EventOutbox {
|
|
|
336
459
|
private readonly _leaseMs;
|
|
337
460
|
private readonly _onError?;
|
|
338
461
|
private readonly _usePublishMany;
|
|
462
|
+
private readonly _failurePolicy?;
|
|
463
|
+
/**
|
|
464
|
+
* In-process attempt counter per event id. Accurate within this relay
|
|
465
|
+
* process; resets on restart. Populated as failures occur and cleared on
|
|
466
|
+
* successful ack or dead-letter transition. For durable authoritative
|
|
467
|
+
* counts, apps can query the store directly inside {@link OutboxFailurePolicy}.
|
|
468
|
+
*/
|
|
469
|
+
private readonly _attempts;
|
|
339
470
|
constructor(opts: EventOutboxOptions);
|
|
340
471
|
/** Unique consumer ID used for lease ownership when the store supports claims */
|
|
341
472
|
get consumerId(): string;
|
|
@@ -395,6 +526,16 @@ declare class EventOutbox {
|
|
|
395
526
|
* @returns Per-kind outcome counts for the batch
|
|
396
527
|
*/
|
|
397
528
|
relayBatch(): Promise<RelayResult>;
|
|
529
|
+
/**
|
|
530
|
+
* Fetch current dead-lettered events as typed {@link DeadLetteredEvent}
|
|
531
|
+
* envelopes. Delegates to {@link OutboxStore.getDeadLettered} — returns
|
|
532
|
+
* `[]` when the store doesn't implement it.
|
|
533
|
+
*
|
|
534
|
+
* Pairs with {@link OutboxFailurePolicy}: apps set a policy that routes to
|
|
535
|
+
* `deadLetter: true` after N attempts, then read back with this to alert,
|
|
536
|
+
* replay, or archive.
|
|
537
|
+
*/
|
|
538
|
+
getDeadLettered(limit?: number): Promise<DeadLetteredEvent[]>;
|
|
398
539
|
/**
|
|
399
540
|
* Purge old **delivered** events from the outbox store.
|
|
400
541
|
* Delegates to `store.purge()` if implemented; no-op otherwise.
|
|
@@ -403,36 +544,6 @@ declare class EventOutbox {
|
|
|
403
544
|
*/
|
|
404
545
|
purge(olderThanMs?: number): Promise<number>;
|
|
405
546
|
}
|
|
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
|
-
*/
|
|
424
|
-
declare class MemoryOutboxStore implements OutboxStore {
|
|
425
|
-
private readonly entries;
|
|
426
|
-
private readonly seenDedupeKeys;
|
|
427
|
-
save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
|
|
428
|
-
getPending(limit: number): Promise<DomainEvent[]>;
|
|
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;
|
|
435
|
-
}
|
|
436
547
|
/**
|
|
437
548
|
* Options for {@link exponentialBackoff}.
|
|
438
549
|
*/
|
|
@@ -486,4 +597,4 @@ interface ExponentialBackoffOptions {
|
|
|
486
597
|
*/
|
|
487
598
|
declare function exponentialBackoff(options: ExponentialBackoffOptions): Date;
|
|
488
599
|
//#endregion
|
|
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 };
|
|
600
|
+
export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type DeadLetteredEvent, type DomainEvent, type EventDefinitionInput, type EventDefinitionOutput, type EventHandler, type EventLogger, type EventMeta, 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, type OutboxFailureContext, type OutboxFailureDecision, type OutboxFailurePolicy, 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, createChildEvent, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, withRetry };
|