@classytic/arc 2.11.2 → 2.11.4
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/LICENSE +21 -21
- package/README.md +20 -21
- package/bin/arc.js +2 -2
- package/dist/{BaseController-JNV08qOT.mjs → BaseController-swXruJ2_.mjs} +2 -2
- package/dist/EventTransport-BFQjw9pB.mjs +133 -0
- package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
- package/dist/{actionPermissions-C8YYU92K.mjs → actionPermissions-sUUKDhtP.mjs} +4 -2
- package/dist/adapters/index.d.mts +3 -3
- package/dist/adapters/index.mjs +2 -2
- package/dist/{adapters-D0tT2Tyo.mjs → adapters-DUUiiimH.mjs} +17 -2
- package/dist/audit/index.d.mts +2 -2
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +1 -1
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/cache/index.d.mts +3 -3
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/generate.d.mts +0 -2
- package/dist/cli/commands/generate.mjs +16 -16
- package/dist/cli/commands/init.mjs +149 -65
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-DXdSSFW-.mjs → core-CbcQRIch.mjs} +25 -8
- package/dist/{createActionRouter-BwaSM0No.mjs → createActionRouter-CIKOcNA7.mjs} +74 -14
- package/dist/{createApp-P1d6rjPy.mjs → createApp-C9bRrqlX.mjs} +4 -6
- package/dist/defineEvent-D1Ky9M1D.mjs +188 -0
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{eventPlugin--5HIkdPU.mjs → eventPlugin-Cts2-Tfj.mjs} +9 -135
- package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-DDJoNEPL.d.mts} +34 -7
- package/dist/events/index.d.mts +164 -5
- package/dist/events/index.mjs +138 -182
- 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 +1 -1
- package/dist/factory/index.mjs +1 -1
- package/dist/{fields-C8Y0XLAu.d.mts → fields-BRjxOAFp.d.mts} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-6u4_Gg6G.d.mts → index-CXXRbnf8.d.mts} +51 -5
- package/dist/{index-DdQ3O9Pg.d.mts → index-D9t1KNaB.d.mts} +2 -2
- package/dist/{index-BbMrcvGp.d.mts → index-Rg8axYPz.d.mts} +12 -4
- package/dist/{index-BdXnTPRj.d.mts → index-m8mOOlFW.d.mts} +3 -3
- package/dist/{index-BYCqHCVu.d.mts → index-rHjXmJar.d.mts} +3 -3
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +7 -7
- package/dist/integrations/event-gateway.d.mts +2 -2
- 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/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{openapi-C0L9ar7m.mjs → openapi-D7G1V7ex.mjs} +2 -2
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-B4vU9L0Q.mjs → permissions-gd_aUWrR.mjs} +42 -0
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +5 -5
- package/dist/plugins/index.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 +4 -4
- package/dist/presets/filesUpload.mjs +1 -1
- 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/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-k604Lj99.mjs → presets-Z7P5w4gF.mjs} +1 -1
- package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
- package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
- package/dist/redis-stream-xTGxB2bm.d.mts +232 -0
- package/dist/registry/index.d.mts +1 -1
- package/dist/{requestContext-CfRkaxwf.mjs → requestContext-C5XeK3VA.mjs} +15 -0
- package/dist/{resourceToTools--okX6QBr.mjs → resourceToTools-CxNmI6xF.mjs} +7 -6
- package/dist/{routerShared-DeESFp4a.mjs → routerShared-BqLRb5l7.mjs} +60 -3
- package/dist/scope/index.d.mts +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- 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-9beEMe25.d.mts → types-BQ9TJQNy.d.mts} +1 -1
- package/dist/{types-BH7dEGvU.d.mts → types-D7KpfiL1.d.mts} +10 -10
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-D3Yxnrwr.mjs → utils-CcYTj09l.mjs} +1 -1
- package/dist/{versioning-M9lNLhO8.d.mts → versioning-DsglKfM_.d.mts} +1 -1
- package/package.json +3 -1
- package/skills/arc/SKILL.md +409 -769
- package/skills/arc/references/events.md +489 -489
- package/dist/redis-stream-CM8TXTix.d.mts +0 -110
- /package/dist/{EventTransport-CfVEGaEl.d.mts → EventTransport-CYNUXdCJ.d.mts} +0 -0
- /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BQQXZ_VR.d.mts} +0 -0
- /package/dist/{errorHandler-Co3lnVmJ.d.mts → errorHandler-DEWmGWPz.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/{pluralize-BneOJkpi.mjs → pluralize-CWP6MB39.mjs} +0 -0
- /package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-Dy2p4MxS.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/{store-helpers-BhrzxvyQ.mjs → store-helpers-Cp4uKC1U.mjs} +0 -0
- /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
- /package/dist/{types-tgR4Pt8F.d.mts → types-DDyTPc6y.d.mts} +0 -0
- /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
|
+
import { r as createEvent } from "./EventTransport-BFQjw9pB.mjs";
|
|
3
|
+
//#region src/events/defineEvent.ts
|
|
4
|
+
/**
|
|
5
|
+
* defineEvent — Typed Event Definitions with Optional Schema Validation
|
|
6
|
+
*
|
|
7
|
+
* Provides:
|
|
8
|
+
* 1. defineEvent() — declare an event with name, schema, version, description
|
|
9
|
+
* 2. EventRegistry — catalog of all known events + payload validation
|
|
10
|
+
* 3. .create() helper — build DomainEvent with auto-generated metadata
|
|
11
|
+
*
|
|
12
|
+
* The built-in validator checks: object type, required fields, and top-level
|
|
13
|
+
* property types. It does NOT recurse into nested objects, validate arrays,
|
|
14
|
+
* enums, patterns, formats, or $ref. This is intentional — it's a lightweight
|
|
15
|
+
* guard, not a full JSON Schema engine.
|
|
16
|
+
*
|
|
17
|
+
* For full validation, pass a custom `validate` function to `createEventRegistry()`:
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import Ajv from 'ajv';
|
|
22
|
+
* const ajv = new Ajv();
|
|
23
|
+
*
|
|
24
|
+
* const registry = createEventRegistry({
|
|
25
|
+
* validate: (schema, payload) => {
|
|
26
|
+
* const valid = ajv.validate(schema, payload);
|
|
27
|
+
* return valid
|
|
28
|
+
* ? { valid: true }
|
|
29
|
+
* : { valid: false, errors: ajv.errorsText().split(', ') };
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { defineEvent, createEventRegistry } from '@classytic/arc/events';
|
|
37
|
+
*
|
|
38
|
+
* const OrderCreated = defineEvent({
|
|
39
|
+
* name: 'order.created',
|
|
40
|
+
* version: 1,
|
|
41
|
+
* schema: {
|
|
42
|
+
* type: 'object',
|
|
43
|
+
* properties: {
|
|
44
|
+
* orderId: { type: 'string' },
|
|
45
|
+
* total: { type: 'number' },
|
|
46
|
+
* },
|
|
47
|
+
* required: ['orderId', 'total'],
|
|
48
|
+
* },
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // Type-safe event creation
|
|
52
|
+
* const event = OrderCreated.create({ orderId: 'o-1', total: 100 });
|
|
53
|
+
* await fastify.events.publish(event.type, event.payload, event.meta);
|
|
54
|
+
*
|
|
55
|
+
* // Registry for introspection + validation
|
|
56
|
+
* const registry = createEventRegistry();
|
|
57
|
+
* registry.register(OrderCreated);
|
|
58
|
+
* const result = registry.validate('order.created', payload);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
var defineEvent_exports = /* @__PURE__ */ __exportAll({
|
|
62
|
+
createEventRegistry: () => createEventRegistry,
|
|
63
|
+
defineEvent: () => defineEvent
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* Define a typed event with optional schema validation.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* const OrderCreated = defineEvent({
|
|
70
|
+
* name: 'order.created',
|
|
71
|
+
* schema: { type: 'object', properties: { orderId: { type: 'string' } }, required: ['orderId'] },
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* const event = OrderCreated.create({ orderId: '123' });
|
|
75
|
+
*/
|
|
76
|
+
function defineEvent(input) {
|
|
77
|
+
const { name, schema, version = 1, description } = input;
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
schema,
|
|
81
|
+
version,
|
|
82
|
+
description,
|
|
83
|
+
create(payload, meta) {
|
|
84
|
+
return createEvent(name, payload, {
|
|
85
|
+
schemaVersion: version,
|
|
86
|
+
...meta
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create an event registry for cataloging and validating events.
|
|
93
|
+
*
|
|
94
|
+
* The registry is opt-in — unregistered events pass validation.
|
|
95
|
+
* This allows gradual adoption without breaking existing code.
|
|
96
|
+
*
|
|
97
|
+
* @param options.validate - Custom validator replacing the built-in minimal validator.
|
|
98
|
+
* The built-in validator only checks top-level object structure (type, required, property types).
|
|
99
|
+
* For nested objects, arrays, enums, patterns, or $ref, provide AJV or similar.
|
|
100
|
+
*/
|
|
101
|
+
function createEventRegistry(options) {
|
|
102
|
+
const customValidator = options?.validate;
|
|
103
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
104
|
+
function registryKey(name, version) {
|
|
105
|
+
return `${name}:v${version}`;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
register(definition) {
|
|
109
|
+
const key = registryKey(definition.name, definition.version);
|
|
110
|
+
if (definitions.has(key)) throw new Error(`Event '${definition.name}' v${definition.version} is already registered. Use a different version number for schema evolution.`);
|
|
111
|
+
definitions.set(key, definition);
|
|
112
|
+
},
|
|
113
|
+
get(name, version) {
|
|
114
|
+
if (version !== void 0) return definitions.get(registryKey(name, version));
|
|
115
|
+
let latest;
|
|
116
|
+
let latestVersion = -1;
|
|
117
|
+
for (const def of definitions.values()) if (def.name === name && def.version > latestVersion) {
|
|
118
|
+
latest = def;
|
|
119
|
+
latestVersion = def.version;
|
|
120
|
+
}
|
|
121
|
+
return latest;
|
|
122
|
+
},
|
|
123
|
+
catalog() {
|
|
124
|
+
return Array.from(definitions.values()).map((def) => ({
|
|
125
|
+
name: def.name,
|
|
126
|
+
version: def.version,
|
|
127
|
+
description: def.description,
|
|
128
|
+
schema: def.schema
|
|
129
|
+
}));
|
|
130
|
+
},
|
|
131
|
+
validate(name, payload, version) {
|
|
132
|
+
let target = version !== void 0 ? definitions.get(registryKey(name, version)) : void 0;
|
|
133
|
+
if (!target && version === void 0) {
|
|
134
|
+
let latestVersion = -1;
|
|
135
|
+
for (const candidate of definitions.values()) if (candidate.name === name && candidate.version > latestVersion) {
|
|
136
|
+
target = candidate;
|
|
137
|
+
latestVersion = candidate.version;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!target) return { valid: true };
|
|
141
|
+
if (!target.schema) return { valid: true };
|
|
142
|
+
return customValidator ? customValidator(target.schema, payload) : validatePayload(payload, target.schema);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Built-in minimal validator — lightweight guard, NOT a full JSON Schema engine.
|
|
148
|
+
*
|
|
149
|
+
* Checks:
|
|
150
|
+
* - payload is an object (not null, not array)
|
|
151
|
+
* - required fields are present
|
|
152
|
+
* - top-level property types match (string, number, boolean, array, object)
|
|
153
|
+
*
|
|
154
|
+
* Does NOT check:
|
|
155
|
+
* - nested object properties
|
|
156
|
+
* - array item types
|
|
157
|
+
* - enum, pattern, format, minLength, minimum, $ref
|
|
158
|
+
*
|
|
159
|
+
* For full validation, pass a custom `validate` function to `createEventRegistry()`.
|
|
160
|
+
*/
|
|
161
|
+
function validatePayload(payload, schema) {
|
|
162
|
+
const errors = [];
|
|
163
|
+
if (schema.type === "object") {
|
|
164
|
+
if (payload === null || payload === void 0 || typeof payload !== "object" || Array.isArray(payload)) return {
|
|
165
|
+
valid: false,
|
|
166
|
+
errors: ["Payload must be an object"]
|
|
167
|
+
};
|
|
168
|
+
const record = payload;
|
|
169
|
+
if (schema.required) {
|
|
170
|
+
for (const field of schema.required) if (!(field in record) || record[field] === void 0) errors.push(`Missing required field: '${field}'`);
|
|
171
|
+
}
|
|
172
|
+
if (schema.properties) {
|
|
173
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) if (key in record && record[key] !== void 0 && record[key] !== null) {
|
|
174
|
+
const expectedType = propSchema.type;
|
|
175
|
+
if (expectedType) {
|
|
176
|
+
const actualType = Array.isArray(record[key]) ? "array" : typeof record[key];
|
|
177
|
+
if (expectedType !== actualType) errors.push(`Field '${key}': expected ${expectedType}, got ${actualType}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return errors.length === 0 ? { valid: true } : {
|
|
183
|
+
valid: false,
|
|
184
|
+
errors
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
export { defineEvent as n, defineEvent_exports as r, createEventRegistry as t };
|
package/dist/docs/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { p as RegistryEntry } from "../index-
|
|
2
|
-
import { t as ExternalOpenApiPaths } from "../externalPaths-
|
|
1
|
+
import { p as RegistryEntry } from "../index-CXXRbnf8.mjs";
|
|
2
|
+
import { t as ExternalOpenApiPaths } from "../externalPaths-BD5nw6St.mjs";
|
|
3
3
|
import { FastifyPluginAsync } from "fastify";
|
|
4
4
|
|
|
5
5
|
//#region src/docs/openapi.d.ts
|
package/dist/docs/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as getUserRoles } from "../types-DV9WDfeg.mjs";
|
|
2
|
-
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-
|
|
2
|
+
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-D7G1V7ex.mjs";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
//#region src/docs/scalar.ts
|
|
5
5
|
const scalarPlugin = async (fastify, opts = {}) => {
|
|
@@ -1,144 +1,18 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
|
-
import { t as requestContext } from "./requestContext-
|
|
2
|
+
import { t as requestContext } from "./requestContext-C5XeK3VA.mjs";
|
|
3
|
+
import { r as createEvent, t as MemoryEventTransport } from "./EventTransport-BFQjw9pB.mjs";
|
|
3
4
|
import fp from "fastify-plugin";
|
|
4
|
-
//#region src/events/EventTransport.ts
|
|
5
|
-
/**
|
|
6
|
-
* In-memory event transport (default)
|
|
7
|
-
* Events are delivered synchronously within the process.
|
|
8
|
-
* Not suitable for multi-instance deployments.
|
|
9
|
-
*/
|
|
10
|
-
var MemoryEventTransport = class {
|
|
11
|
-
name = "memory";
|
|
12
|
-
handlers = /* @__PURE__ */ new Map();
|
|
13
|
-
logger;
|
|
14
|
-
constructor(options) {
|
|
15
|
-
this.logger = options?.logger ?? console;
|
|
16
|
-
}
|
|
17
|
-
async publish(event) {
|
|
18
|
-
const exactHandlers = this.handlers.get(event.type) ?? /* @__PURE__ */ new Set();
|
|
19
|
-
const wildcardHandlers = this.handlers.get("*") ?? /* @__PURE__ */ new Set();
|
|
20
|
-
const patternHandlers = /* @__PURE__ */ new Set();
|
|
21
|
-
for (const [pattern, handlers] of this.handlers.entries()) if (pattern.endsWith(".*")) {
|
|
22
|
-
const prefix = pattern.slice(0, -2);
|
|
23
|
-
if (event.type.startsWith(`${prefix}.`)) for (const h of handlers) patternHandlers.add(h);
|
|
24
|
-
}
|
|
25
|
-
const allHandlers = new Set([
|
|
26
|
-
...exactHandlers,
|
|
27
|
-
...wildcardHandlers,
|
|
28
|
-
...patternHandlers
|
|
29
|
-
]);
|
|
30
|
-
for (const handler of allHandlers) try {
|
|
31
|
-
await handler(event);
|
|
32
|
-
} catch (err) {
|
|
33
|
-
this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Reference `publishMany` implementation — delegates to `publish()` in order.
|
|
38
|
-
*
|
|
39
|
-
* Production transports (Kafka, Redis pipeline, SQS batch) should override
|
|
40
|
-
* this with a single batched network call. Memory transport has nothing to
|
|
41
|
-
* batch, so we just loop — the loop still returns a proper result map so
|
|
42
|
-
* `EventOutbox.relay` can exercise the batched code path in tests.
|
|
43
|
-
*/
|
|
44
|
-
async publishMany(events) {
|
|
45
|
-
const results = /* @__PURE__ */ new Map();
|
|
46
|
-
for (const event of events) try {
|
|
47
|
-
await this.publish(event);
|
|
48
|
-
results.set(event.meta.id, null);
|
|
49
|
-
} catch (err) {
|
|
50
|
-
results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
|
|
51
|
-
}
|
|
52
|
-
return results;
|
|
53
|
-
}
|
|
54
|
-
async subscribe(pattern, handler) {
|
|
55
|
-
if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
|
|
56
|
-
this.handlers.get(pattern)?.add(handler);
|
|
57
|
-
return () => {
|
|
58
|
-
const set = this.handlers.get(pattern);
|
|
59
|
-
if (set) {
|
|
60
|
-
set.delete(handler);
|
|
61
|
-
if (set.size === 0) this.handlers.delete(pattern);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
async close() {
|
|
66
|
-
this.handlers.clear();
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
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.
|
|
74
|
-
*/
|
|
75
|
-
function createEvent(type, payload, meta) {
|
|
76
|
-
return {
|
|
77
|
-
type,
|
|
78
|
-
payload,
|
|
79
|
-
meta: {
|
|
80
|
-
id: crypto.randomUUID(),
|
|
81
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
82
|
-
...meta
|
|
83
|
-
}
|
|
84
|
-
};
|
|
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
|
-
}
|
|
135
|
-
//#endregion
|
|
136
5
|
//#region src/events/retry.ts
|
|
137
6
|
/**
|
|
138
7
|
* Wrap an event handler with retry logic and dead letter support.
|
|
139
8
|
*
|
|
140
9
|
* On failure, retries with exponential backoff (with jitter).
|
|
141
10
|
* After all retries exhausted, calls `onDead` callback if provided.
|
|
11
|
+
*
|
|
12
|
+
* Generic in the payload type `T` so composing with `wrapWithSchema<T>` /
|
|
13
|
+
* `subscribeWithSchema<T>` doesn't force a cast at the boundary — the inner
|
|
14
|
+
* `handler: EventHandler<T>` flows through to the returned wrapper. Defaults
|
|
15
|
+
* to `unknown` for raw `subscribe(pattern, withRetry(...))` call sites.
|
|
142
16
|
*/
|
|
143
17
|
function withRetry(handler, options = {}) {
|
|
144
18
|
const { maxRetries = 3, backoffMs = 1e3, maxBackoffMs = 3e4, jitter = .1, transport, onDead, name, logger = console } = options;
|
|
@@ -253,7 +127,7 @@ const eventPlugin = async (fastify, opts = {}) => {
|
|
|
253
127
|
correlationId: event.meta.correlationId
|
|
254
128
|
}, "Publishing event");
|
|
255
129
|
if (registry && validateMode !== "off") {
|
|
256
|
-
const result = registry.validate(type, payload);
|
|
130
|
+
const result = registry.validate(type, payload, event.meta.schemaVersion);
|
|
257
131
|
if (!result.valid) {
|
|
258
132
|
const msg = `[Arc Events] Event '${type}' payload validation failed: ${result.errors?.join("; ")}`;
|
|
259
133
|
if (validateMode === "reject") throw new Error(msg);
|
|
@@ -321,4 +195,4 @@ var eventPlugin_default = fp(eventPlugin, {
|
|
|
321
195
|
fastify: "5.x"
|
|
322
196
|
});
|
|
323
197
|
//#endregion
|
|
324
|
-
export {
|
|
198
|
+
export { withRetry as i, eventPlugin_exports as n, createDeadLetterPublisher as r, eventPlugin as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as EventLogger, n as DomainEvent, o as EventTransport, r as EventHandler } from "./EventTransport-
|
|
1
|
+
import { i as EventLogger, n as DomainEvent, o as EventTransport, r as EventHandler } from "./EventTransport-CYNUXdCJ.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>;
|
package/dist/events/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
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-
|
|
1
|
+
import { Mn as RepositoryLike } from "../index-CXXRbnf8.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-CYNUXdCJ.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-DDJoNEPL.mjs";
|
|
4
4
|
import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
|
|
5
|
-
import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-
|
|
5
|
+
import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-xTGxB2bm.mjs";
|
|
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 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 PayloadOf, type PublishManyResult, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RelayResult, type RetryOptions, type ValidationResult, type WrapWithBoundaryOptions, type WrapWithSchemaOptions, createChildEvent, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
|