@classytic/arc 2.11.3 → 2.13.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 +27 -18
- package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
- package/dist/EventTransport-CT_52aWU.d.mts +34 -0
- package/dist/EventTransport-DLWoUMHy.mjs +103 -0
- package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
- package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/audit.d.mts +199 -0
- package/dist/auth/audit.mjs +288 -0
- package/dist/auth/index.d.mts +5 -5
- package/dist/auth/index.mjs +117 -191
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
- package/dist/buildHandler-olo-gt94.mjs +610 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/describe.d.mts +89 -13
- package/dist/cli/commands/describe.mjs +56 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +147 -48
- package/dist/cli/commands/init.d.mts +13 -0
- package/dist/cli/commands/init.mjs +237 -112
- package/dist/cli/commands/introspect.mjs +8 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/core-D72ia0EH.mjs +1399 -0
- package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
- package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
- package/dist/{createApp-BFxtdKy6.mjs → createApp-XX2-N0Yd.mjs} +31 -27
- package/dist/defineEvent-D5h7EvAx.mjs +188 -0
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
- package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
- package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
- package/dist/errors-j4aJm1Wg.mjs +184 -0
- package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-CaKTYkYM.mjs} +35 -137
- package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-qXpqTebY.d.mts} +57 -7
- package/dist/events/index.d.mts +164 -5
- package/dist/events/index.mjs +133 -209
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +204 -31
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-C8Y0XLAu.d.mts → fields-COhcH3fk.d.mts} +23 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +1 -20
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-BYCqHCVu.d.mts → index-BTqLEvhu.d.mts} +164 -4
- package/dist/{index-6u4_Gg6G.d.mts → index-BtW7qYwa.d.mts} +661 -281
- package/dist/{index-BdXnTPRj.d.mts → index-Ds61mrJE.d.mts} +50 -4
- package/dist/{index-DdQ3O9Pg.d.mts → index-Dz5IKsrE.d.mts} +360 -219
- package/dist/index.d.mts +6 -7
- package/dist/index.mjs +9 -10
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +60 -11
- package/dist/integrations/streamline.mjs +75 -85
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +2 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +2 -2
- package/dist/migrations/index.d.mts +23 -3
- package/dist/migrations/index.mjs +0 -7
- package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
- package/dist/{openapi-BGUn7Ki1.mjs → openapi-CiOMVW1p.mjs} +143 -13
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
- package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +18 -33
- package/dist/plugins/index.mjs +33 -13
- 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/presets/filesUpload.d.mts +5 -5
- package/dist/presets/filesUpload.mjs +6 -9
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -2
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +6 -8
- package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
- package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
- package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
- package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
- package/dist/redis-stream-D6HzR1Z_.d.mts +232 -0
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
- package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-C5coh64w.mjs} +224 -71
- package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
- package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
- package/dist/schemas/index.d.mts +100 -30
- package/dist/schemas/index.mjs +86 -29
- package/dist/scim/index.d.mts +264 -0
- package/dist/scim/index.mjs +963 -0
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +4 -4
- package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
- package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -8
- package/dist/testing/index.mjs +16 -24
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-BH7dEGvU.d.mts → types-BvqwCCSx.d.mts} +77 -29
- package/dist/{types-tgR4Pt8F.d.mts → types-CTYvcwHe.d.mts} +195 -1
- package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
- package/dist/{types-9beEMe25.d.mts → types-DQHFc8PM.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +5 -5
- package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
- package/dist/{versioning-M9lNLhO8.d.mts → versioning-DTTvc80y.d.mts} +1 -1
- package/package.json +24 -34
- package/skills/arc/SKILL.md +521 -785
- package/skills/arc/references/agent-auth.md +238 -0
- package/skills/arc/references/api-reference.md +187 -0
- package/skills/arc/references/auth.md +354 -7
- package/skills/arc/references/enterprise-auth.md +94 -0
- package/skills/arc/references/events.md +8 -6
- package/skills/arc/references/mcp.md +2 -2
- package/skills/arc/references/multi-tenancy.md +11 -2
- package/skills/arc/references/production.md +10 -9
- package/skills/arc/references/scim.md +247 -0
- package/skills/arc/references/testing.md +1 -1
- package/skills/arc-code-review/SKILL.md +141 -0
- package/skills/arc-code-review/references/anti-patterns.md +911 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
- package/skills/arc-code-review/references/migration-recipes.md +700 -0
- package/skills/arc-code-review/references/mongokit-migration.md +386 -0
- package/skills/arc-code-review/references/scaffolding.md +230 -0
- package/skills/arc-code-review/references/severity.md +127 -0
- package/dist/EventTransport-CfVEGaEl.d.mts +0 -293
- package/dist/adapters/index.d.mts +0 -3
- package/dist/adapters/index.mjs +0 -2
- package/dist/adapters-D0tT2Tyo.mjs +0 -949
- package/dist/auth/mongoose.d.mts +0 -191
- package/dist/auth/mongoose.mjs +0 -73
- package/dist/core-DnUsRpuX.mjs +0 -1049
- package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
- package/dist/errorHandler-Co3lnVmJ.d.mts +0 -114
- package/dist/errors-D5c-5BJL.mjs +0 -232
- package/dist/index-BbMrcvGp.d.mts +0 -362
- package/dist/redis-stream-CM8TXTix.d.mts +0 -110
- /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
- /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
- /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
- /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
- /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
- /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
- /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
- /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
- /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
- /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{pluralize-BneOJkpi.mjs → pluralize-DQgqgifU.mjs} +0 -0
- /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
- /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
- /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
- /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
- /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
- /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
- /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-CT_52aWU.mjs";
|
|
2
2
|
import { FastifyPluginAsync } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/events/defineEvent.d.ts
|
|
@@ -73,8 +73,15 @@ interface EventRegistry {
|
|
|
73
73
|
description?: string;
|
|
74
74
|
schema?: EventSchema;
|
|
75
75
|
}>;
|
|
76
|
-
/**
|
|
77
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Validate a payload against a registered event's schema.
|
|
78
|
+
*
|
|
79
|
+
* @param version - Optional schema version. When set, validation runs
|
|
80
|
+
* against that exact version (use during migrations: producer A still
|
|
81
|
+
* on v1 must validate against v1's schema even if v2 is registered).
|
|
82
|
+
* When omitted, validates against the latest registered version.
|
|
83
|
+
*/
|
|
84
|
+
validate(name: string, payload: unknown, version?: number): ValidationResult;
|
|
78
85
|
}
|
|
79
86
|
/**
|
|
80
87
|
* Define a typed event with optional schema validation.
|
|
@@ -155,8 +162,13 @@ interface RetryOptions {
|
|
|
155
162
|
*
|
|
156
163
|
* On failure, retries with exponential backoff (with jitter).
|
|
157
164
|
* After all retries exhausted, calls `onDead` callback if provided.
|
|
165
|
+
*
|
|
166
|
+
* Generic in the payload type `T` so composing with `wrapWithSchema<T>` /
|
|
167
|
+
* `subscribeWithSchema<T>` doesn't force a cast at the boundary — the inner
|
|
168
|
+
* `handler: EventHandler<T>` flows through to the returned wrapper. Defaults
|
|
169
|
+
* to `unknown` for raw `subscribe(pattern, withRetry(...))` call sites.
|
|
158
170
|
*/
|
|
159
|
-
declare function withRetry(handler: EventHandler
|
|
171
|
+
declare function withRetry<T = unknown>(handler: EventHandler<T>, options?: RetryOptions): EventHandler<T>;
|
|
160
172
|
/**
|
|
161
173
|
* Create a dead letter publisher that sends failed events to a `$deadLetter` channel.
|
|
162
174
|
*
|
|
@@ -203,9 +215,24 @@ interface EventPluginOptions {
|
|
|
203
215
|
*/
|
|
204
216
|
failOpen?: boolean;
|
|
205
217
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
218
|
+
* Low-level write-ahead hook called BEFORE the transport publish, with an
|
|
219
|
+
* optional acknowledge() called AFTER a successful publish.
|
|
220
|
+
*
|
|
221
|
+
* **Important**: this is NOT at-least-once delivery on its own. If
|
|
222
|
+
* `transport.publish()` throws after `wal.save()`, the saved row stays
|
|
223
|
+
* but arc does NOT relay it on next boot — there is no replay loop here.
|
|
224
|
+
* For at-least-once you must EITHER:
|
|
225
|
+
*
|
|
226
|
+
* 1. Run a relay loop yourself (read unacknowledged WAL rows on boot,
|
|
227
|
+
* republish, ack on success), or
|
|
228
|
+
* 2. Use `EventOutbox` ([./outbox.ts]) — `outbox.relay()` is the
|
|
229
|
+
* production-grade at-least-once primitive with claim/lease,
|
|
230
|
+
* retry/DLQ, multi-worker safety, and `repository`-backed durable
|
|
231
|
+
* storage. New code should prefer `EventOutbox` over `wal`.
|
|
232
|
+
*
|
|
233
|
+
* The `wal` slot is kept for hosts that want to integrate with custom
|
|
234
|
+
* write-ahead infrastructure (Kafka producer transactions, S3 batch
|
|
235
|
+
* archives, debug audit logs) without arc's outbox claim/lease semantics.
|
|
209
236
|
*/
|
|
210
237
|
wal?: {
|
|
211
238
|
save: (event: DomainEvent) => Promise<void>;
|
|
@@ -248,6 +275,29 @@ interface EventPluginOptions {
|
|
|
248
275
|
* - `'off'`: skip validation entirely (registry is only for introspection)
|
|
249
276
|
*/
|
|
250
277
|
validateMode?: "warn" | "reject" | "off";
|
|
278
|
+
/**
|
|
279
|
+
* Dev-mode duplicate-publish detector (v2.12).
|
|
280
|
+
*
|
|
281
|
+
* When enabled, arc keeps a 5-second LRU on `(eventType, correlationId)`
|
|
282
|
+
* and emits an `arcLog("events").warn(...)` the second time a request
|
|
283
|
+
* publishes the same event with the same correlation id within the
|
|
284
|
+
* window. Catches the dual-publish trap where a domain service holds
|
|
285
|
+
* BOTH a publisher AND a notification helper that internally publishes
|
|
286
|
+
* to the same bus — every subscriber fires twice for one logical event.
|
|
287
|
+
*
|
|
288
|
+
* Defaults:
|
|
289
|
+
* - `undefined` → enabled when `process.env.NODE_ENV !== 'production'`.
|
|
290
|
+
* - `true` → always enabled (catches duplicates in prod too — overhead
|
|
291
|
+
* is one Map lookup per publish).
|
|
292
|
+
* - `false` → always disabled.
|
|
293
|
+
*
|
|
294
|
+
* When a duplicate is detected, arc logs once and **still publishes** —
|
|
295
|
+
* the detector is observability, not enforcement. Pair with the outbox
|
|
296
|
+
* for at-most-once delivery.
|
|
297
|
+
*
|
|
298
|
+
* Documented in `wiki/gotchas.md` (#20).
|
|
299
|
+
*/
|
|
300
|
+
warnOnDuplicate?: boolean;
|
|
251
301
|
}
|
|
252
302
|
declare module "fastify" {
|
|
253
303
|
interface FastifyInstance {
|
package/dist/events/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
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-CUNjYYRY.mjs";
|
|
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-CT_52aWU.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-qXpqTebY.mjs";
|
|
4
3
|
import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
|
|
5
|
-
import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-
|
|
4
|
+
import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-D6HzR1Z_.mjs";
|
|
5
|
+
import { RepositoryLike } from "@classytic/repo-core/adapter";
|
|
6
6
|
|
|
7
7
|
//#region src/events/eventTypes.d.ts
|
|
8
8
|
/**
|
|
@@ -600,4 +600,163 @@ declare function exponentialBackoff(options: ExponentialBackoffOptions): Date;
|
|
|
600
600
|
//#region src/events/repository-outbox-adapter.d.ts
|
|
601
601
|
declare function repositoryAsOutboxStore(repository: RepositoryLike): OutboxStore;
|
|
602
602
|
//#endregion
|
|
603
|
-
|
|
603
|
+
//#region src/events/subscribe-helpers.d.ts
|
|
604
|
+
/**
|
|
605
|
+
* Extract the payload type from an `EventDefinitionOutput<T>`.
|
|
606
|
+
*
|
|
607
|
+
* `defineEvent<T>` already threads `T` through `.create(payload: T, ...)`, but
|
|
608
|
+
* there's no exposed way to recover `T` for use in handler signatures, factory
|
|
609
|
+
* helpers, or test fixtures. `PayloadOf<typeof OrderPaid>` closes the loop
|
|
610
|
+
* without forcing every host to define their own copy.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* ```ts
|
|
614
|
+
* const OrderPaid = defineEvent<{ orderId: string; total: number }>({
|
|
615
|
+
* name: 'order.paid',
|
|
616
|
+
* schema: { type: 'object', required: ['orderId', 'total'] },
|
|
617
|
+
* });
|
|
618
|
+
* type OrderPaidPayload = PayloadOf<typeof OrderPaid>;
|
|
619
|
+
* // ^? { orderId: string; total: number }
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
type PayloadOf<D> = D extends EventDefinitionOutput<infer T> ? T : never;
|
|
623
|
+
interface WrapWithSchemaOptions<_T> {
|
|
624
|
+
/**
|
|
625
|
+
* Custom validator. Overrides the built-in lookup. Use this to plug AJV /
|
|
626
|
+
* Zod / TypeBox in. Same shape as `EventRegistryOptions.validate`.
|
|
627
|
+
*
|
|
628
|
+
* Resolution order:
|
|
629
|
+
* 1. `validate` (this option)
|
|
630
|
+
* 2. `registry.validate(definition.name, payload)` — uses whatever
|
|
631
|
+
* validator the registry was configured with
|
|
632
|
+
* 3. Built-in minimal validator (top-level `required` + property types)
|
|
633
|
+
*/
|
|
634
|
+
validate?: CustomValidator;
|
|
635
|
+
/**
|
|
636
|
+
* Optional registry — when set and `validate` is omitted, validation routes
|
|
637
|
+
* through `registry.validate(definition.name, payload)`. Lets the subscriber
|
|
638
|
+
* use the same configured validator (AJV, custom) the publish side uses
|
|
639
|
+
* via `eventPlugin({ registry })`.
|
|
640
|
+
*/
|
|
641
|
+
registry?: EventRegistry;
|
|
642
|
+
/**
|
|
643
|
+
* Called when payload validation fails. Default behaviour: log a warning
|
|
644
|
+
* with the event's id/type/errors and skip the handler (the event is NOT
|
|
645
|
+
* acknowledged as a failure, since the handler never ran — matches `withRetry`'s
|
|
646
|
+
* `onDead` semantics for terminal failures, but at the validation boundary).
|
|
647
|
+
*
|
|
648
|
+
* Receives the raw event (untyped — the payload is by definition not the
|
|
649
|
+
* declared shape) plus the validation errors array.
|
|
650
|
+
*/
|
|
651
|
+
onInvalid?: (event: DomainEvent<unknown>, errors: string[]) => void | Promise<void>;
|
|
652
|
+
/**
|
|
653
|
+
* Logger for invalid-payload warnings. Pass `fastify.log` to integrate
|
|
654
|
+
* with the application logger. Default: `console`.
|
|
655
|
+
*/
|
|
656
|
+
logger?: EventLogger;
|
|
657
|
+
/**
|
|
658
|
+
* Optional name for log output (otherwise the definition name is used).
|
|
659
|
+
*/
|
|
660
|
+
name?: string;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Pure handler wrapper — returns a new `EventHandler` that validates
|
|
664
|
+
* `event.payload` against the definition's schema before invoking the handler.
|
|
665
|
+
*
|
|
666
|
+
* The returned handler's input is `DomainEvent<unknown>` (since the transport
|
|
667
|
+
* delivers untyped events) but the inner `handler` receives `DomainEvent<T>`.
|
|
668
|
+
* No cast at the call site.
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```ts
|
|
672
|
+
* await fastify.events.subscribe(
|
|
673
|
+
* OrderPaid.name,
|
|
674
|
+
* wrapWithSchema(OrderPaid, async (event) => {
|
|
675
|
+
* // event.payload is typed via the registered schema — no cast.
|
|
676
|
+
* await postSalesEntry(event.payload.orderId, event.payload.total);
|
|
677
|
+
* }),
|
|
678
|
+
* );
|
|
679
|
+
* ```
|
|
680
|
+
*/
|
|
681
|
+
declare function wrapWithSchema<T>(definition: EventDefinitionOutput<T>, handler: EventHandler<T>, options?: WrapWithSchemaOptions<T>): EventHandler<unknown>;
|
|
682
|
+
/**
|
|
683
|
+
* Convenience: validate + subscribe in one call. Equivalent to
|
|
684
|
+
* `fastify.events.subscribe(definition.name, wrapWithSchema(definition, handler, options))`.
|
|
685
|
+
*
|
|
686
|
+
* Returns the unsubscribe function from the underlying transport.
|
|
687
|
+
*
|
|
688
|
+
* @example
|
|
689
|
+
* ```ts
|
|
690
|
+
* await subscribeWithSchema(fastify, OrderPaid, async (event) => {
|
|
691
|
+
* await postSalesEntry(event.payload.orderId, event.payload.total);
|
|
692
|
+
* });
|
|
693
|
+
*
|
|
694
|
+
* // Compose with withRetry — schema validation runs FIRST, then retry on
|
|
695
|
+
* // handler failure. Invalid payloads skip without burning retry attempts.
|
|
696
|
+
* await subscribeWithSchema(
|
|
697
|
+
* fastify,
|
|
698
|
+
* OrderPaid,
|
|
699
|
+
* withRetry(handler, { maxRetries: 3 }),
|
|
700
|
+
* );
|
|
701
|
+
* ```
|
|
702
|
+
*/
|
|
703
|
+
declare function subscribeWithSchema<T>(fastify: FastifyEventBus, definition: EventDefinitionOutput<T>, handler: EventHandler<T>, options?: WrapWithSchemaOptions<T>): Promise<() => void>;
|
|
704
|
+
interface WrapWithBoundaryOptions {
|
|
705
|
+
/**
|
|
706
|
+
* Called when the handler throws. Default behaviour: log the error with
|
|
707
|
+
* `{ err, event: event.type, eventId: event.meta.id }` and swallow.
|
|
708
|
+
*
|
|
709
|
+
* Use this to push metrics (`statsd.increment('handler.error', { type })`)
|
|
710
|
+
* or alert on specific event types.
|
|
711
|
+
*/
|
|
712
|
+
onError?: (err: Error, event: DomainEvent) => void | Promise<void>;
|
|
713
|
+
/**
|
|
714
|
+
* Logger for handler errors. Pass `fastify.log` to integrate with the
|
|
715
|
+
* application logger. Default: `console`.
|
|
716
|
+
*/
|
|
717
|
+
logger?: EventLogger;
|
|
718
|
+
/**
|
|
719
|
+
* Optional name for log output (otherwise the handler's `.name` is used).
|
|
720
|
+
*/
|
|
721
|
+
name?: string;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Pure handler wrapper — returns a new `EventHandler` that catches handler
|
|
725
|
+
* exceptions and routes them to `onError` (or logs and swallows). For
|
|
726
|
+
* projection / cache-invalidation / fire-and-forget handlers where retry
|
|
727
|
+
* would just delay the next-event resync, and where one bad event must NOT
|
|
728
|
+
* stop processing of subsequent events.
|
|
729
|
+
*
|
|
730
|
+
* Lighter than `withRetry`: no exponential backoff, no DLQ. Composes with
|
|
731
|
+
* `withRetry` if you want both ("retry, then log if exhausted, never throw").
|
|
732
|
+
*
|
|
733
|
+
* @example
|
|
734
|
+
* ```ts
|
|
735
|
+
* await fastify.events.subscribe(
|
|
736
|
+
* 'product:variants.changed',
|
|
737
|
+
* wrapWithBoundary(async (event) => {
|
|
738
|
+
* cache.invalidate(event.payload.productId);
|
|
739
|
+
* }),
|
|
740
|
+
* );
|
|
741
|
+
* ```
|
|
742
|
+
*/
|
|
743
|
+
declare function wrapWithBoundary(handler: EventHandler, options?: WrapWithBoundaryOptions): EventHandler;
|
|
744
|
+
/**
|
|
745
|
+
* Convenience: subscribe + error-boundary in one call. Equivalent to
|
|
746
|
+
* `fastify.events.subscribe(pattern, wrapWithBoundary(handler, options))`.
|
|
747
|
+
*
|
|
748
|
+
* Returns the unsubscribe function from the underlying transport.
|
|
749
|
+
*/
|
|
750
|
+
declare function subscribeWithBoundary(fastify: FastifyEventBus, pattern: string, handler: EventHandler, options?: WrapWithBoundaryOptions): Promise<() => void>;
|
|
751
|
+
/**
|
|
752
|
+
* Structural type — accepts anything with the `events.subscribe` method,
|
|
753
|
+
* including `FastifyInstance` (declaration-merged in `eventPlugin.ts`) and
|
|
754
|
+
* test doubles. Avoids importing the full Fastify type in this module.
|
|
755
|
+
*/
|
|
756
|
+
interface FastifyEventBus {
|
|
757
|
+
events: {
|
|
758
|
+
subscribe: (pattern: string, handler: EventHandler) => Promise<() => void>;
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
//#endregion
|
|
762
|
+
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, 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 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 };
|
package/dist/events/index.mjs
CHANGED
|
@@ -1,184 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as
|
|
1
|
+
import { t as MemoryEventTransport } from "../EventTransport-DLWoUMHy.mjs";
|
|
2
|
+
import { n as defineEvent, t as createEventRegistry } from "../defineEvent-D5h7EvAx.mjs";
|
|
3
|
+
import { i as withRetry, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-CaKTYkYM.mjs";
|
|
4
|
+
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-BkIN9-vu.mjs";
|
|
3
5
|
import { and, anyOf, eq, lte, ne, or } from "@classytic/repo-core/filter";
|
|
4
6
|
import { update } from "@classytic/repo-core/update";
|
|
5
|
-
//#region src/events/defineEvent.ts
|
|
6
|
-
/**
|
|
7
|
-
* defineEvent — Typed Event Definitions with Optional Schema Validation
|
|
8
|
-
*
|
|
9
|
-
* Provides:
|
|
10
|
-
* 1. defineEvent() — declare an event with name, schema, version, description
|
|
11
|
-
* 2. EventRegistry — catalog of all known events + payload validation
|
|
12
|
-
* 3. .create() helper — build DomainEvent with auto-generated metadata
|
|
13
|
-
*
|
|
14
|
-
* The built-in validator checks: object type, required fields, and top-level
|
|
15
|
-
* property types. It does NOT recurse into nested objects, validate arrays,
|
|
16
|
-
* enums, patterns, formats, or $ref. This is intentional — it's a lightweight
|
|
17
|
-
* guard, not a full JSON Schema engine.
|
|
18
|
-
*
|
|
19
|
-
* For full validation, pass a custom `validate` function to `createEventRegistry()`:
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```typescript
|
|
23
|
-
* import Ajv from 'ajv';
|
|
24
|
-
* const ajv = new Ajv();
|
|
25
|
-
*
|
|
26
|
-
* const registry = createEventRegistry({
|
|
27
|
-
* validate: (schema, payload) => {
|
|
28
|
-
* const valid = ajv.validate(schema, payload);
|
|
29
|
-
* return valid
|
|
30
|
-
* ? { valid: true }
|
|
31
|
-
* : { valid: false, errors: ajv.errorsText().split(', ') };
|
|
32
|
-
* },
|
|
33
|
-
* });
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```typescript
|
|
38
|
-
* import { defineEvent, createEventRegistry } from '@classytic/arc/events';
|
|
39
|
-
*
|
|
40
|
-
* const OrderCreated = defineEvent({
|
|
41
|
-
* name: 'order.created',
|
|
42
|
-
* version: 1,
|
|
43
|
-
* schema: {
|
|
44
|
-
* type: 'object',
|
|
45
|
-
* properties: {
|
|
46
|
-
* orderId: { type: 'string' },
|
|
47
|
-
* total: { type: 'number' },
|
|
48
|
-
* },
|
|
49
|
-
* required: ['orderId', 'total'],
|
|
50
|
-
* },
|
|
51
|
-
* });
|
|
52
|
-
*
|
|
53
|
-
* // Type-safe event creation
|
|
54
|
-
* const event = OrderCreated.create({ orderId: 'o-1', total: 100 });
|
|
55
|
-
* await fastify.events.publish(event.type, event.payload, event.meta);
|
|
56
|
-
*
|
|
57
|
-
* // Registry for introspection + validation
|
|
58
|
-
* const registry = createEventRegistry();
|
|
59
|
-
* registry.register(OrderCreated);
|
|
60
|
-
* const result = registry.validate('order.created', payload);
|
|
61
|
-
* ```
|
|
62
|
-
*/
|
|
63
|
-
/**
|
|
64
|
-
* Define a typed event with optional schema validation.
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* const OrderCreated = defineEvent({
|
|
68
|
-
* name: 'order.created',
|
|
69
|
-
* schema: { type: 'object', properties: { orderId: { type: 'string' } }, required: ['orderId'] },
|
|
70
|
-
* });
|
|
71
|
-
*
|
|
72
|
-
* const event = OrderCreated.create({ orderId: '123' });
|
|
73
|
-
*/
|
|
74
|
-
function defineEvent(input) {
|
|
75
|
-
const { name, schema, version = 1, description } = input;
|
|
76
|
-
return {
|
|
77
|
-
name,
|
|
78
|
-
schema,
|
|
79
|
-
version,
|
|
80
|
-
description,
|
|
81
|
-
create(payload, meta) {
|
|
82
|
-
return createEvent(name, payload, meta);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Create an event registry for cataloging and validating events.
|
|
88
|
-
*
|
|
89
|
-
* The registry is opt-in — unregistered events pass validation.
|
|
90
|
-
* This allows gradual adoption without breaking existing code.
|
|
91
|
-
*
|
|
92
|
-
* @param options.validate - Custom validator replacing the built-in minimal validator.
|
|
93
|
-
* The built-in validator only checks top-level object structure (type, required, property types).
|
|
94
|
-
* For nested objects, arrays, enums, patterns, or $ref, provide AJV or similar.
|
|
95
|
-
*/
|
|
96
|
-
function createEventRegistry(options) {
|
|
97
|
-
const customValidator = options?.validate;
|
|
98
|
-
const definitions = /* @__PURE__ */ new Map();
|
|
99
|
-
function registryKey(name, version) {
|
|
100
|
-
return `${name}:v${version}`;
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
register(definition) {
|
|
104
|
-
const key = registryKey(definition.name, definition.version);
|
|
105
|
-
if (definitions.has(key)) throw new Error(`Event '${definition.name}' v${definition.version} is already registered. Use a different version number for schema evolution.`);
|
|
106
|
-
definitions.set(key, definition);
|
|
107
|
-
},
|
|
108
|
-
get(name, version) {
|
|
109
|
-
if (version !== void 0) return definitions.get(registryKey(name, version));
|
|
110
|
-
let latest;
|
|
111
|
-
let latestVersion = -1;
|
|
112
|
-
for (const def of definitions.values()) if (def.name === name && def.version > latestVersion) {
|
|
113
|
-
latest = def;
|
|
114
|
-
latestVersion = def.version;
|
|
115
|
-
}
|
|
116
|
-
return latest;
|
|
117
|
-
},
|
|
118
|
-
catalog() {
|
|
119
|
-
return Array.from(definitions.values()).map((def) => ({
|
|
120
|
-
name: def.name,
|
|
121
|
-
version: def.version,
|
|
122
|
-
description: def.description,
|
|
123
|
-
schema: def.schema
|
|
124
|
-
}));
|
|
125
|
-
},
|
|
126
|
-
validate(name, payload) {
|
|
127
|
-
let latest;
|
|
128
|
-
let latestVersion = -1;
|
|
129
|
-
for (const def of definitions.values()) if (def.name === name && def.version > latestVersion) {
|
|
130
|
-
latest = def;
|
|
131
|
-
latestVersion = def.version;
|
|
132
|
-
}
|
|
133
|
-
if (!latest) return { valid: true };
|
|
134
|
-
if (!latest.schema) return { valid: true };
|
|
135
|
-
if (customValidator) return customValidator(latest.schema, payload);
|
|
136
|
-
return validatePayload(payload, latest.schema);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Built-in minimal validator — lightweight guard, NOT a full JSON Schema engine.
|
|
142
|
-
*
|
|
143
|
-
* Checks:
|
|
144
|
-
* - payload is an object (not null, not array)
|
|
145
|
-
* - required fields are present
|
|
146
|
-
* - top-level property types match (string, number, boolean, array, object)
|
|
147
|
-
*
|
|
148
|
-
* Does NOT check:
|
|
149
|
-
* - nested object properties
|
|
150
|
-
* - array item types
|
|
151
|
-
* - enum, pattern, format, minLength, minimum, $ref
|
|
152
|
-
*
|
|
153
|
-
* For full validation, pass a custom `validate` function to `createEventRegistry()`.
|
|
154
|
-
*/
|
|
155
|
-
function validatePayload(payload, schema) {
|
|
156
|
-
const errors = [];
|
|
157
|
-
if (schema.type === "object") {
|
|
158
|
-
if (payload === null || payload === void 0 || typeof payload !== "object" || Array.isArray(payload)) return {
|
|
159
|
-
valid: false,
|
|
160
|
-
errors: ["Payload must be an object"]
|
|
161
|
-
};
|
|
162
|
-
const record = payload;
|
|
163
|
-
if (schema.required) {
|
|
164
|
-
for (const field of schema.required) if (!(field in record) || record[field] === void 0) errors.push(`Missing required field: '${field}'`);
|
|
165
|
-
}
|
|
166
|
-
if (schema.properties) {
|
|
167
|
-
for (const [key, propSchema] of Object.entries(schema.properties)) if (key in record && record[key] !== void 0 && record[key] !== null) {
|
|
168
|
-
const expectedType = propSchema.type;
|
|
169
|
-
if (expectedType) {
|
|
170
|
-
const actualType = Array.isArray(record[key]) ? "array" : typeof record[key];
|
|
171
|
-
if (expectedType !== actualType) errors.push(`Field '${key}': expected ${expectedType}, got ${actualType}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return errors.length === 0 ? { valid: true } : {
|
|
177
|
-
valid: false,
|
|
178
|
-
errors
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
//#endregion
|
|
182
7
|
//#region src/events/eventTypes.ts
|
|
183
8
|
/**
|
|
184
9
|
* Event Type Constants and Helpers
|
|
@@ -345,30 +170,6 @@ var MemoryOutboxStore = class {
|
|
|
345
170
|
};
|
|
346
171
|
//#endregion
|
|
347
172
|
//#region src/events/repository-outbox-adapter.ts
|
|
348
|
-
/**
|
|
349
|
-
* RepositoryLike → OutboxStore adapter.
|
|
350
|
-
*
|
|
351
|
-
* Maps the `OutboxStore` vocabulary (save / claimPending / acknowledge /
|
|
352
|
-
* fail / getDeadLettered / purge) onto arc's own `RepositoryLike` primitives
|
|
353
|
-
* (create / getOne / findAll / deleteMany / findOneAndUpdate). `EventOutbox`
|
|
354
|
-
* wraps a passed repository with this helper when you use the
|
|
355
|
-
* `{ repository }` option; the function is also re-exported from
|
|
356
|
-
* `@classytic/arc/events` so consumers can build and decorate the store
|
|
357
|
-
* manually (metrics, tracing, multi-transport fan-out).
|
|
358
|
-
*
|
|
359
|
-
* Portability: filters compose via `@classytic/repo-core/filter` and
|
|
360
|
-
* updates via `@classytic/repo-core/update`. The primary-key column name
|
|
361
|
-
* is read from `repository.idField` — mongokit defaults to `_id`,
|
|
362
|
-
* sqlitekit / pgkit / prismakit to the schema's declared PK. The adapter
|
|
363
|
-
* therefore runs on any kit that implements `StandardRepo.findOneAndUpdate`
|
|
364
|
-
* + `getOne` + `getAll` + `deleteMany` + `create`.
|
|
365
|
-
*
|
|
366
|
-
* `fail()` uses a lease-gated read-then-write pair to preserve
|
|
367
|
-
* `firstFailedAt` across retries without relying on Mongo's aggregation-
|
|
368
|
-
* pipeline `$ifNull`. Leases guarantee single-writer during the failure
|
|
369
|
-
* window (`claimPending` filters out non-owned rows), so the two calls are
|
|
370
|
-
* safe under concurrent relayers.
|
|
371
|
-
*/
|
|
372
173
|
const DEFAULT_LEASE_MS$1 = 3e4;
|
|
373
174
|
const DEFAULT_CLAIM_LIMIT = 100;
|
|
374
175
|
const DEFAULT_PURGE_BATCH = 500;
|
|
@@ -383,12 +184,12 @@ function repositoryAsOutboxStore(repository) {
|
|
|
383
184
|
const r = repository;
|
|
384
185
|
const idField = repository.idField ?? "_id";
|
|
385
186
|
/**
|
|
386
|
-
* Unwrap mongokit's pagination envelope ({
|
|
187
|
+
* Unwrap mongokit's pagination envelope ({ data, total, ... }) — some
|
|
387
188
|
* kits may return a bare array when pagination is disabled. Handle both.
|
|
388
189
|
*/
|
|
389
190
|
const unwrapDocs = (result) => {
|
|
390
191
|
if (Array.isArray(result)) return result;
|
|
391
|
-
return result?.
|
|
192
|
+
return result?.data ?? [];
|
|
392
193
|
};
|
|
393
194
|
const isDuplicateKeyError = createIsDuplicateKeyError(repository);
|
|
394
195
|
const safeGetOne = createSafeGetOne(repository);
|
|
@@ -707,7 +508,7 @@ var EventOutbox = class {
|
|
|
707
508
|
const valid = [];
|
|
708
509
|
let malformed = 0;
|
|
709
510
|
for (const event of pending) {
|
|
710
|
-
if (!event
|
|
511
|
+
if (!event?.type || !event.meta?.id) {
|
|
711
512
|
this._reportError("malformed_event", new InvalidOutboxEventError("store returned event missing type or meta.id — batch aborted"), event);
|
|
712
513
|
malformed++;
|
|
713
514
|
break;
|
|
@@ -726,7 +527,7 @@ var EventOutbox = class {
|
|
|
726
527
|
const canFail = typeof this._store.fail === "function";
|
|
727
528
|
let publishOutcomes;
|
|
728
529
|
if (canPublishMany && valid.length > 0) try {
|
|
729
|
-
const result = await this._transport.publishMany(valid);
|
|
530
|
+
const result = await this._transport.publishMany?.(valid);
|
|
730
531
|
publishOutcomes = new Map(result);
|
|
731
532
|
} catch (batchErr) {
|
|
732
533
|
publishOutcomes = /* @__PURE__ */ new Map();
|
|
@@ -771,7 +572,7 @@ var EventOutbox = class {
|
|
|
771
572
|
this._reportError("fail_failed", policyErr, event);
|
|
772
573
|
}
|
|
773
574
|
try {
|
|
774
|
-
await this._store.fail(event.meta.id, normalizeError(publishErr), failOpts);
|
|
575
|
+
await this._store.fail?.(event.meta.id, normalizeError(publishErr), failOpts);
|
|
775
576
|
if (failOpts.deadLetter) {
|
|
776
577
|
counts.deadLettered++;
|
|
777
578
|
this._attempts.delete(event.meta.id);
|
|
@@ -886,4 +687,127 @@ function exponentialBackoff(options) {
|
|
|
886
687
|
return new Date(now + jittered);
|
|
887
688
|
}
|
|
888
689
|
//#endregion
|
|
889
|
-
|
|
690
|
+
//#region src/events/subscribe-helpers.ts
|
|
691
|
+
/**
|
|
692
|
+
* Pure handler wrapper — returns a new `EventHandler` that validates
|
|
693
|
+
* `event.payload` against the definition's schema before invoking the handler.
|
|
694
|
+
*
|
|
695
|
+
* The returned handler's input is `DomainEvent<unknown>` (since the transport
|
|
696
|
+
* delivers untyped events) but the inner `handler` receives `DomainEvent<T>`.
|
|
697
|
+
* No cast at the call site.
|
|
698
|
+
*
|
|
699
|
+
* @example
|
|
700
|
+
* ```ts
|
|
701
|
+
* await fastify.events.subscribe(
|
|
702
|
+
* OrderPaid.name,
|
|
703
|
+
* wrapWithSchema(OrderPaid, async (event) => {
|
|
704
|
+
* // event.payload is typed via the registered schema — no cast.
|
|
705
|
+
* await postSalesEntry(event.payload.orderId, event.payload.total);
|
|
706
|
+
* }),
|
|
707
|
+
* );
|
|
708
|
+
* ```
|
|
709
|
+
*/
|
|
710
|
+
function wrapWithSchema(definition, handler, options = {}) {
|
|
711
|
+
const { validate, registry, onInvalid, logger = console, name } = options;
|
|
712
|
+
const label = name ?? definition.name;
|
|
713
|
+
return async (event) => {
|
|
714
|
+
const eventVersion = typeof event.meta?.schemaVersion === "number" ? event.meta.schemaVersion : definition.version;
|
|
715
|
+
let result;
|
|
716
|
+
if (validate && definition.schema) result = validate(definition.schema, event.payload);
|
|
717
|
+
else if (registry) result = registry.validate(definition.name, event.payload, eventVersion);
|
|
718
|
+
else if (definition.schema) {
|
|
719
|
+
const { createEventRegistry } = await import("../defineEvent-D5h7EvAx.mjs").then((n) => n.r);
|
|
720
|
+
const adhoc = createEventRegistry();
|
|
721
|
+
adhoc.register(definition);
|
|
722
|
+
result = adhoc.validate(definition.name, event.payload);
|
|
723
|
+
} else result = { valid: true };
|
|
724
|
+
if (!result.valid) {
|
|
725
|
+
const errors = result.errors ?? ["payload failed validation"];
|
|
726
|
+
if (onInvalid) try {
|
|
727
|
+
await onInvalid(event, errors);
|
|
728
|
+
} catch (cbErr) {
|
|
729
|
+
logger.error(`[Arc Events] '${label}' onInvalid callback threw:`, cbErr);
|
|
730
|
+
}
|
|
731
|
+
else logger.warn(`[Arc Events] '${label}' skipped event ${event.meta?.id ?? "<no-id>"} — payload failed validation: ${errors.join("; ")}`);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
await handler(event);
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Convenience: validate + subscribe in one call. Equivalent to
|
|
739
|
+
* `fastify.events.subscribe(definition.name, wrapWithSchema(definition, handler, options))`.
|
|
740
|
+
*
|
|
741
|
+
* Returns the unsubscribe function from the underlying transport.
|
|
742
|
+
*
|
|
743
|
+
* @example
|
|
744
|
+
* ```ts
|
|
745
|
+
* await subscribeWithSchema(fastify, OrderPaid, async (event) => {
|
|
746
|
+
* await postSalesEntry(event.payload.orderId, event.payload.total);
|
|
747
|
+
* });
|
|
748
|
+
*
|
|
749
|
+
* // Compose with withRetry — schema validation runs FIRST, then retry on
|
|
750
|
+
* // handler failure. Invalid payloads skip without burning retry attempts.
|
|
751
|
+
* await subscribeWithSchema(
|
|
752
|
+
* fastify,
|
|
753
|
+
* OrderPaid,
|
|
754
|
+
* withRetry(handler, { maxRetries: 3 }),
|
|
755
|
+
* );
|
|
756
|
+
* ```
|
|
757
|
+
*/
|
|
758
|
+
async function subscribeWithSchema(fastify, definition, handler, options) {
|
|
759
|
+
return fastify.events.subscribe(definition.name, wrapWithSchema(definition, handler, options));
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Pure handler wrapper — returns a new `EventHandler` that catches handler
|
|
763
|
+
* exceptions and routes them to `onError` (or logs and swallows). For
|
|
764
|
+
* projection / cache-invalidation / fire-and-forget handlers where retry
|
|
765
|
+
* would just delay the next-event resync, and where one bad event must NOT
|
|
766
|
+
* stop processing of subsequent events.
|
|
767
|
+
*
|
|
768
|
+
* Lighter than `withRetry`: no exponential backoff, no DLQ. Composes with
|
|
769
|
+
* `withRetry` if you want both ("retry, then log if exhausted, never throw").
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* ```ts
|
|
773
|
+
* await fastify.events.subscribe(
|
|
774
|
+
* 'product:variants.changed',
|
|
775
|
+
* wrapWithBoundary(async (event) => {
|
|
776
|
+
* cache.invalidate(event.payload.productId);
|
|
777
|
+
* }),
|
|
778
|
+
* );
|
|
779
|
+
* ```
|
|
780
|
+
*/
|
|
781
|
+
function wrapWithBoundary(handler, options = {}) {
|
|
782
|
+
const { onError, logger = console, name } = options;
|
|
783
|
+
const label = name ?? handler.name ?? "anonymous";
|
|
784
|
+
return async (event) => {
|
|
785
|
+
try {
|
|
786
|
+
await handler(event);
|
|
787
|
+
} catch (err) {
|
|
788
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
789
|
+
if (onError) try {
|
|
790
|
+
await onError(error, event);
|
|
791
|
+
} catch (cbErr) {
|
|
792
|
+
logger.error(`[Arc Events] '${label}' onError callback threw:`, cbErr);
|
|
793
|
+
}
|
|
794
|
+
else logger.error(`[Arc Events] '${label}' threw on ${event.type} — swallowed (boundary): ${error.message}`, {
|
|
795
|
+
err: error,
|
|
796
|
+
event: event.type,
|
|
797
|
+
eventId: event.meta?.id,
|
|
798
|
+
handler: label
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Convenience: subscribe + error-boundary in one call. Equivalent to
|
|
805
|
+
* `fastify.events.subscribe(pattern, wrapWithBoundary(handler, options))`.
|
|
806
|
+
*
|
|
807
|
+
* Returns the unsubscribe function from the underlying transport.
|
|
808
|
+
*/
|
|
809
|
+
async function subscribeWithBoundary(fastify, pattern, handler, options) {
|
|
810
|
+
return fastify.events.subscribe(pattern, wrapWithBoundary(handler, options));
|
|
811
|
+
}
|
|
812
|
+
//#endregion
|
|
813
|
+
export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, InvalidOutboxEventError, MemoryEventTransport, MemoryOutboxStore, OutboxOwnershipError, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-
|
|
1
|
+
import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-D6HzR1Z_.mjs";
|
|
2
2
|
export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
|