@classytic/arc 2.23.0 → 2.28.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/README.md +2 -0
- package/dist/{BaseController-BQ8KNvWC.mjs → BaseController-B65zbz9m.mjs} +94 -23
- package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
- package/dist/actionPermissions-CAphcmUG.mjs +24 -0
- package/dist/{applyPermissionResult-Tw4iXtlf.mjs → applyPermissionResult-BnyVE9eD.mjs} +10 -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/cleanup/index.d.mts +313 -0
- package/dist/cleanup/index.mjs +1267 -0
- 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 +5 -4
- package/dist/core-BGa8TXJR.mjs +221 -0
- package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-C-N7z2EL.mjs} +3 -2
- package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
- package/dist/{createApp-CjOeWJ9_.mjs → createApp-Du5G54TM.mjs} +187 -96
- package/dist/{core-DQ6rDTyH.mjs → defineResource-2zvVVpxg.mjs} +297 -230
- 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 +83 -314
- package/dist/events/index.mjs +183 -95
- 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 +5 -5
- package/dist/factory/index.mjs +30 -8
- package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-OzYGIiU1.d.mts} +26 -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-CZY4EGga.d.mts +107 -0
- package/dist/{index-CC5ruDGh.d.mts → index-CjVg-Epg.d.mts} +37 -5
- package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +7 -6
- 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-Cs6nRGCt.mjs} +12 -10
- 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/module-_-M7C5ES.mjs +29 -0
- package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
- package/dist/{openapi-jSDx-VDv.mjs → openapi-BLV4OMXb.mjs} +3 -3
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BGRnV-To.mjs} +4 -27
- 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-CM4uUaFv.d.mts} +51 -50
- package/dist/preloadResources-DSY5HGpS.mjs +45 -0
- 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-DEuDu9io.mjs} +3 -3
- package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
- package/dist/registerRoute-DTp18Ah_.mjs +108 -0
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/{registry-vcOCtPij.mjs → registry-9km_w1aP.mjs} +15 -0
- 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/cleanupStoreContract.d.mts +10 -0
- package/dist/testing/cleanupStoreContract.mjs +375 -0
- package/dist/testing/index.d.mts +5 -3
- package/dist/testing/index.mjs +4 -2
- package/dist/testing/outboxStoreContract.d.mts +13 -0
- package/dist/testing/outboxStoreContract.mjs +247 -0
- 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-C_ZZRcD6.d.mts +450 -0
- package/dist/{module-B4u10iOJ.d.mts → types-Cu6vVJmh.d.mts} +196 -60
- 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 +541 -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
|
@@ -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
|
/**
|
|
@@ -71,6 +72,16 @@ interface MemoryEntry {
|
|
|
71
72
|
declare class MemoryOutboxStore implements OutboxStore {
|
|
72
73
|
private readonly entries;
|
|
73
74
|
private readonly seenDedupeKeys;
|
|
75
|
+
/**
|
|
76
|
+
* Drop every entry (including delivered / failed / dead-lettered rows) and
|
|
77
|
+
* the dedupe-key memory.
|
|
78
|
+
*
|
|
79
|
+
* For tests only — it is the in-memory analogue of truncating the table, and
|
|
80
|
+
* exists so a suite can isolate cases against ONE store instance (the
|
|
81
|
+
* `reset` hook of `runOutboxStoreContract`). Not part of `OutboxStore`; a
|
|
82
|
+
* production store must never expose this.
|
|
83
|
+
*/
|
|
84
|
+
clear(): void;
|
|
74
85
|
save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
|
|
75
86
|
getPending(limit: number): Promise<DomainEvent[]>;
|
|
76
87
|
claimPending(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
|
|
@@ -82,279 +93,65 @@ declare class MemoryOutboxStore implements OutboxStore {
|
|
|
82
93
|
_getEntry(eventId: string): Readonly<MemoryEntry> | undefined;
|
|
83
94
|
}
|
|
84
95
|
//#endregion
|
|
85
|
-
//#region src/events/outbox.d.ts
|
|
86
|
-
/**
|
|
87
|
-
* **Terminology (v2.8.1+):**
|
|
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
|
-
*/
|
|
96
|
+
//#region src/events/outbox/backoff.d.ts
|
|
100
97
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
98
|
+
* Exponential backoff for outbox failure policies — pure date math shared
|
|
99
|
+
* by `failurePolicy({ attempts })` style handlers.
|
|
103
100
|
*/
|
|
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
101
|
/**
|
|
121
|
-
* Options for {@link
|
|
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}.
|
|
156
|
-
*/
|
|
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
|
-
/**
|
|
172
|
-
* Decision returned by {@link OutboxFailurePolicy} — controls how the
|
|
173
|
-
* relay calls `store.fail()` for a failed event.
|
|
102
|
+
* Options for {@link exponentialBackoff}.
|
|
174
103
|
*/
|
|
175
|
-
interface
|
|
176
|
-
/**
|
|
177
|
-
readonly
|
|
104
|
+
interface ExponentialBackoffOptions {
|
|
105
|
+
/** Current attempt count (1-indexed — first retry is attempt 1) */
|
|
106
|
+
readonly attempt: number;
|
|
107
|
+
/** Base delay in ms (first retry delay). Default: 1000 (1 second) */
|
|
108
|
+
readonly baseMs?: number;
|
|
109
|
+
/** Maximum delay in ms — caps exponential growth. Default: 60_000 (1 minute) */
|
|
110
|
+
readonly maxMs?: number;
|
|
178
111
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
112
|
+
* Jitter factor [0–1]. The returned delay is multiplied by
|
|
113
|
+
* `1 + (random * jitter)` to spread retry bursts across workers.
|
|
114
|
+
* Default: 0.2 (±20%). Set to 0 to disable.
|
|
181
115
|
*/
|
|
182
|
-
readonly
|
|
116
|
+
readonly jitter?: number;
|
|
117
|
+
/** Reference time (for deterministic tests). Default: `Date.now()` */
|
|
118
|
+
readonly now?: number;
|
|
183
119
|
}
|
|
184
120
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
121
|
+
* Compute a `retryAt` `Date` using exponential backoff with jitter.
|
|
122
|
+
*
|
|
123
|
+
* This is a convenience helper for store authors implementing
|
|
124
|
+
* {@link OutboxStore.fail}: call it to compute the retry visibility window
|
|
125
|
+
* based on the event's current attempt count.
|
|
126
|
+
*
|
|
127
|
+
* Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
|
|
187
128
|
*
|
|
188
|
-
*
|
|
129
|
+
* @example Basic usage inside a store's `fail()` method
|
|
189
130
|
* ```typescript
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
131
|
+
* async fail(eventId, error, options) {
|
|
132
|
+
* const entry = await this.findById(eventId);
|
|
133
|
+
* entry.attempts++;
|
|
134
|
+
* if (entry.attempts >= MAX_ATTEMPTS) {
|
|
135
|
+
* return this.deadLetter(eventId, error);
|
|
136
|
+
* }
|
|
137
|
+
* const retryAt = exponentialBackoff({ attempt: entry.attempts });
|
|
138
|
+
* entry.visibleAt = retryAt;
|
|
139
|
+
* await this.update(entry);
|
|
193
140
|
* }
|
|
194
141
|
* ```
|
|
195
142
|
*
|
|
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).
|
|
143
|
+
* @example Tuning for a faster transport
|
|
144
|
+
* ```typescript
|
|
145
|
+
* exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
|
|
146
|
+
* // attempt=1 → ~250ms ±30%
|
|
147
|
+
* // attempt=2 → ~500ms ±30%
|
|
148
|
+
* // attempt=3 → ~1000ms ±30%
|
|
149
|
+
* // attempt=10 → capped at 10_000ms
|
|
150
|
+
* ```
|
|
263
151
|
*/
|
|
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
|
-
}
|
|
152
|
+
declare function exponentialBackoff(options: ExponentialBackoffOptions): Date;
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/events/outbox/relay.d.ts
|
|
358
155
|
/** Reason codes passed to {@link EventOutboxOptions.onError}. */
|
|
359
156
|
type OutboxRelayErrorKind = "publish_failed" | "acknowledge_failed" | "fail_failed" | "ownership_mismatch" | "malformed_event";
|
|
360
157
|
/**
|
|
@@ -387,6 +184,14 @@ interface RelayResult {
|
|
|
387
184
|
readonly deadLettered: number;
|
|
388
185
|
/** Whether `publishMany` was used (true) or per-event `publish` (false) */
|
|
389
186
|
readonly usedPublishMany: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Wall-clock ms spent claiming this batch from the store. With the
|
|
189
|
+
* portable repository adapter this is ~2 DB round trips; a persistently
|
|
190
|
+
* high value under low batch sizes points at store latency, not volume.
|
|
191
|
+
*/
|
|
192
|
+
readonly claimMs?: number;
|
|
193
|
+
/** Wall-clock ms spent publishing + acknowledging the batch. */
|
|
194
|
+
readonly publishMs?: number;
|
|
390
195
|
}
|
|
391
196
|
/**
|
|
392
197
|
* Called by {@link EventOutbox.relay} when a non-fatal error occurs during
|
|
@@ -598,61 +403,25 @@ declare class EventOutbox {
|
|
|
598
403
|
*/
|
|
599
404
|
purge(olderThanMs?: number): Promise<number>;
|
|
600
405
|
}
|
|
601
|
-
|
|
602
|
-
|
|
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;
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/events/repository-outbox-adapter.d.ts
|
|
408
|
+
interface RepositoryOutboxStoreOptions {
|
|
611
409
|
/**
|
|
612
|
-
*
|
|
613
|
-
* `
|
|
614
|
-
*
|
|
410
|
+
* Column holding the "not claimable before" timestamp. Default
|
|
411
|
+
* `'visibleAt'`.
|
|
412
|
+
*
|
|
413
|
+
* Exists so a host with an EXISTING outbox table can adopt this adapter
|
|
414
|
+
* without renaming a column and rebuilding its claim index — the one thing
|
|
415
|
+
* that otherwise forces a migration on the table that guarantees delivery.
|
|
416
|
+
* A store using `nextVisibleAt` (with a `{status, nextVisibleAt, createdAt}`
|
|
417
|
+
* index) passes `{ visibleAtField: 'nextVisibleAt' }` and keeps both.
|
|
418
|
+
*
|
|
419
|
+
* The field is used in the claimable filter, on `save`, and on `fail`'s
|
|
420
|
+
* retry scheduling — all three stay in lockstep by construction.
|
|
615
421
|
*/
|
|
616
|
-
readonly
|
|
617
|
-
/** Reference time (for deterministic tests). Default: `Date.now()` */
|
|
618
|
-
readonly now?: number;
|
|
422
|
+
readonly visibleAtField?: string;
|
|
619
423
|
}
|
|
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
|
-
//#endregion
|
|
654
|
-
//#region src/events/repository-outbox-adapter.d.ts
|
|
655
|
-
declare function repositoryAsOutboxStore(repository: RepositoryLike): OutboxStore;
|
|
424
|
+
declare function repositoryAsOutboxStore(repository: RepositoryLike, options?: RepositoryOutboxStoreOptions): OutboxStore;
|
|
656
425
|
//#endregion
|
|
657
426
|
//#region src/events/subscribe-helpers.d.ts
|
|
658
427
|
/**
|
|
@@ -813,4 +582,4 @@ interface FastifyEventBus {
|
|
|
813
582
|
};
|
|
814
583
|
}
|
|
815
584
|
//#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,
|
|
585
|
+
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 RepositoryOutboxStoreOptions, type RetryOptions, type ValidationResult, type WrapWithBoundaryOptions, type WrapWithSchemaOptions, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
|