@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
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fp from "fastify-plugin";
|
|
3
|
+
//#region src/plugins/requestId.ts
|
|
4
|
+
/**
|
|
5
|
+
* Request ID Plugin
|
|
6
|
+
*
|
|
7
|
+
* Propagates request IDs for distributed tracing.
|
|
8
|
+
* - Accepts incoming x-request-id header (sanitized)
|
|
9
|
+
* - Generates UUID if not provided
|
|
10
|
+
* - Echoes the id on the response header + W3C trace-context propagation
|
|
11
|
+
*
|
|
12
|
+
* ID RESOLUTION LIVES AT THE SERVER LEVEL, NOT HERE. Fastify binds
|
|
13
|
+
* `request.log = logger.child({ reqId: request.id })` at request
|
|
14
|
+
* construction — before any hook runs — so an id assigned inside a hook
|
|
15
|
+
* never reaches the logs. `createApp` therefore wires
|
|
16
|
+
* {@link createRequestIdGenerator} into Fastify's `genReqId` option, and
|
|
17
|
+
* this plugin simply adopts `request.id`. Standalone registration on a
|
|
18
|
+
* bare Fastify instance falls back to the legacy in-hook resolution
|
|
19
|
+
* (header echo / UUID), with the documented limitation that
|
|
20
|
+
* `request.log`'s `reqId` binding predates the overwrite — pass
|
|
21
|
+
* `genReqId: createRequestIdGenerator()` to `Fastify()` to fix that.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* import { requestIdPlugin, createRequestIdGenerator } from '@classytic/arc';
|
|
25
|
+
*
|
|
26
|
+
* const fastify = Fastify({ genReqId: createRequestIdGenerator() });
|
|
27
|
+
* await fastify.register(requestIdPlugin);
|
|
28
|
+
*
|
|
29
|
+
* fastify.get('/', async (request) => {
|
|
30
|
+
* request.log.info('traced'); // carries the same reqId as the header
|
|
31
|
+
* return { id: request.id };
|
|
32
|
+
* });
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Sanitize an incoming request-id header value: max 128 chars, alphanumeric
|
|
36
|
+
* plus dashes/underscores/dots/colons only. Returns `undefined` for anything
|
|
37
|
+
* else (crafted values could pollute logs or response headers). Array values
|
|
38
|
+
* (repeated headers) are rejected outright — a client sending multiple
|
|
39
|
+
* request-id headers is not tracing in good faith.
|
|
40
|
+
*/
|
|
41
|
+
function sanitizeRequestId(value) {
|
|
42
|
+
if (typeof value !== "string") return void 0;
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return trimmed.length > 0 && trimmed.length <= 128 && /^[\w.:-]+$/.test(trimmed) ? trimmed : void 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build a Fastify `genReqId` function: adopt a sanitized incoming request-id
|
|
48
|
+
* header, or generate one. Wire this into the `Fastify()` constructor so
|
|
49
|
+
* `request.id`, `request.log`'s `reqId` binding, and the echoed response
|
|
50
|
+
* header all agree — `createApp` does this automatically.
|
|
51
|
+
*/
|
|
52
|
+
function createRequestIdGenerator(opts = {}) {
|
|
53
|
+
const { header = "x-request-id", generator = randomUUID } = opts;
|
|
54
|
+
const headerName = header.toLowerCase();
|
|
55
|
+
return (req) => sanitizeRequestId(req.headers[headerName]) ?? generator();
|
|
56
|
+
}
|
|
57
|
+
/** Fastify's default request ids are `req-<base36 counter>`. */
|
|
58
|
+
const FASTIFY_DEFAULT_ID = /^req-[0-9a-z]+$/;
|
|
59
|
+
/** W3C traceparent format: `version-traceid-parentid-flags` */
|
|
60
|
+
const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
|
|
61
|
+
/** W3C spec: version ff is reserved and invalid; trace/parent IDs must not be all zeros */
|
|
62
|
+
const TRACEID_ZEROS = "0".repeat(32);
|
|
63
|
+
const PARENTID_ZEROS = "0".repeat(16);
|
|
64
|
+
/** tracestate: printable ASCII, <= 512 bytes */
|
|
65
|
+
const TRACESTATE_MAX = 512;
|
|
66
|
+
function isValidTraceparent(tp) {
|
|
67
|
+
const m = TRACEPARENT_RE.exec(tp);
|
|
68
|
+
if (!m) return false;
|
|
69
|
+
const version = m[1] ?? "";
|
|
70
|
+
const traceId = m[2] ?? "";
|
|
71
|
+
const parentId = m[3] ?? "";
|
|
72
|
+
if (version.toLowerCase() === "ff") return false;
|
|
73
|
+
if (traceId.toLowerCase() === TRACEID_ZEROS) return false;
|
|
74
|
+
if (parentId.toLowerCase() === PARENTID_ZEROS) return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const requestIdPlugin = async (fastify, opts = {}) => {
|
|
78
|
+
const { header = "x-request-id", generator = randomUUID, setResponseHeader = true, propagateTraceContext = true } = opts;
|
|
79
|
+
if (!fastify.hasRequestDecorator("requestId")) fastify.decorateRequest("requestId", "");
|
|
80
|
+
if (!fastify.hasRequestDecorator("traceContext")) fastify.decorateRequest("traceContext", void 0);
|
|
81
|
+
fastify.addHook("onRequest", async (request, reply) => {
|
|
82
|
+
const incoming = sanitizeRequestId(request.headers[header]);
|
|
83
|
+
let requestId = String(request.id);
|
|
84
|
+
if (incoming && incoming !== requestId) requestId = incoming;
|
|
85
|
+
else if (!incoming && FASTIFY_DEFAULT_ID.test(requestId)) requestId = generator();
|
|
86
|
+
if (requestId !== request.id) request.id = requestId;
|
|
87
|
+
request.requestId = requestId;
|
|
88
|
+
if (setResponseHeader) reply.header(header, requestId);
|
|
89
|
+
if (propagateTraceContext) {
|
|
90
|
+
const rawTraceparent = request.headers.traceparent;
|
|
91
|
+
const rawTracestate = request.headers.tracestate;
|
|
92
|
+
if (typeof rawTraceparent === "string") {
|
|
93
|
+
const tp = rawTraceparent.trim();
|
|
94
|
+
if (isValidTraceparent(tp)) {
|
|
95
|
+
const ts = typeof rawTracestate === "string" && rawTracestate.length > 0 && rawTracestate.length <= TRACESTATE_MAX ? rawTracestate : void 0;
|
|
96
|
+
request.traceContext = {
|
|
97
|
+
traceparent: tp,
|
|
98
|
+
...ts ? { tracestate: ts } : {}
|
|
99
|
+
};
|
|
100
|
+
reply.header("traceparent", tp);
|
|
101
|
+
if (ts) reply.header("tracestate", ts);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
fastify.log?.debug?.("Request ID plugin registered");
|
|
107
|
+
};
|
|
108
|
+
var requestId_default = fp(requestIdPlugin, {
|
|
109
|
+
name: "arc-request-id",
|
|
110
|
+
fastify: "5.x"
|
|
111
|
+
});
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/plugins/health.ts
|
|
114
|
+
/**
|
|
115
|
+
* Merge health checks without losing ownership information.
|
|
116
|
+
*
|
|
117
|
+
* Check names are the identity used in readiness responses and criticality
|
|
118
|
+
* lookup, so duplicates are ambiguous and fail at boot instead of silently
|
|
119
|
+
* picking the first declaration. Input order is preserved.
|
|
120
|
+
*/
|
|
121
|
+
function mergeHealthChecks(groups) {
|
|
122
|
+
const owners = /* @__PURE__ */ new Map();
|
|
123
|
+
const merged = [];
|
|
124
|
+
for (const group of groups) for (const check of group.checks ?? []) {
|
|
125
|
+
const prior = owners.get(check.name);
|
|
126
|
+
if (prior !== void 0) throw new Error(`[arc] duplicate health-check name "${check.name}" — declared by ${prior} and ${group.owner}. Health-check names must be unique.`);
|
|
127
|
+
owners.set(check.name, group.owner);
|
|
128
|
+
merged.push(check);
|
|
129
|
+
}
|
|
130
|
+
return merged;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Fixed histogram bucket upper bounds (ms). Recording is O(#buckets) per
|
|
134
|
+
* request; the scrape derives quantile UPPER BOUNDS from cumulative counts
|
|
135
|
+
* in O(#buckets) — no sample retention, no per-scrape sort (the previous
|
|
136
|
+
* 10k-sample ring buffer copied + sorted on EVERY scrape), and bucket
|
|
137
|
+
* counts aggregate correctly across instances.
|
|
138
|
+
*/
|
|
139
|
+
const DURATION_BUCKETS_MS = [
|
|
140
|
+
1,
|
|
141
|
+
2,
|
|
142
|
+
5,
|
|
143
|
+
10,
|
|
144
|
+
25,
|
|
145
|
+
50,
|
|
146
|
+
100,
|
|
147
|
+
250,
|
|
148
|
+
500,
|
|
149
|
+
1e3,
|
|
150
|
+
2500,
|
|
151
|
+
5e3,
|
|
152
|
+
1e4
|
|
153
|
+
];
|
|
154
|
+
function createHttpMetrics() {
|
|
155
|
+
return {
|
|
156
|
+
requestsTotal: {},
|
|
157
|
+
durationBuckets: new Array(DURATION_BUCKETS_MS.length + 1).fill(0),
|
|
158
|
+
durationCount: 0,
|
|
159
|
+
durationSumMs: 0,
|
|
160
|
+
startTime: Date.now()
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Smallest bucket upper bound whose cumulative count reaches `q × total`. */
|
|
164
|
+
function bucketQuantile(metrics, q) {
|
|
165
|
+
const target = Math.ceil(metrics.durationCount * q);
|
|
166
|
+
let cumulative = 0;
|
|
167
|
+
for (let i = 0; i < metrics.durationBuckets.length; i++) {
|
|
168
|
+
cumulative += metrics.durationBuckets[i] ?? 0;
|
|
169
|
+
if (cumulative >= target) return DURATION_BUCKETS_MS[i] ?? DURATION_BUCKETS_MS[DURATION_BUCKETS_MS.length - 1] ?? 0;
|
|
170
|
+
}
|
|
171
|
+
return DURATION_BUCKETS_MS[DURATION_BUCKETS_MS.length - 1] ?? 0;
|
|
172
|
+
}
|
|
173
|
+
const healthPlugin = async (fastify, opts = {}) => {
|
|
174
|
+
const { prefix = "/_health", checks = [], metrics = false, metricsCollector, version, collectHttpMetrics = metrics } = opts;
|
|
175
|
+
const readinessChecks = mergeHealthChecks([{
|
|
176
|
+
owner: "healthPlugin options",
|
|
177
|
+
checks
|
|
178
|
+
}]);
|
|
179
|
+
const httpMetrics = createHttpMetrics();
|
|
180
|
+
fastify.get(prefix, { schema: {
|
|
181
|
+
tags: ["Health"],
|
|
182
|
+
summary: "Liveness probe (alias for /live)",
|
|
183
|
+
description: "Returns 200 if the process is alive",
|
|
184
|
+
response: { 200: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: {
|
|
187
|
+
status: {
|
|
188
|
+
type: "string",
|
|
189
|
+
enum: ["ok"]
|
|
190
|
+
},
|
|
191
|
+
timestamp: { type: "string" },
|
|
192
|
+
version: { type: "string" }
|
|
193
|
+
}
|
|
194
|
+
} }
|
|
195
|
+
} }, async () => ({
|
|
196
|
+
status: "ok",
|
|
197
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
198
|
+
...version ? { version } : {}
|
|
199
|
+
}));
|
|
200
|
+
fastify.get(`${prefix}/live`, { schema: {
|
|
201
|
+
tags: ["Health"],
|
|
202
|
+
summary: "Liveness probe",
|
|
203
|
+
description: "Returns 200 if the process is alive",
|
|
204
|
+
response: { 200: {
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {
|
|
207
|
+
status: {
|
|
208
|
+
type: "string",
|
|
209
|
+
enum: ["ok"]
|
|
210
|
+
},
|
|
211
|
+
timestamp: { type: "string" },
|
|
212
|
+
version: { type: "string" }
|
|
213
|
+
}
|
|
214
|
+
} }
|
|
215
|
+
} }, async () => {
|
|
216
|
+
return {
|
|
217
|
+
status: "ok",
|
|
218
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
219
|
+
...version ? { version } : {}
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
fastify.get(`${prefix}/ready`, { schema: {
|
|
223
|
+
tags: ["Health"],
|
|
224
|
+
summary: "Readiness probe",
|
|
225
|
+
description: "Returns 200 if all dependencies are healthy",
|
|
226
|
+
response: {
|
|
227
|
+
200: {
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
status: {
|
|
231
|
+
type: "string",
|
|
232
|
+
enum: ["ready", "not_ready"]
|
|
233
|
+
},
|
|
234
|
+
timestamp: { type: "string" },
|
|
235
|
+
checks: {
|
|
236
|
+
type: "array",
|
|
237
|
+
items: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
name: { type: "string" },
|
|
241
|
+
healthy: { type: "boolean" },
|
|
242
|
+
duration: { type: "number" },
|
|
243
|
+
error: { type: "string" }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
503: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
status: {
|
|
253
|
+
type: "string",
|
|
254
|
+
enum: ["not_ready"]
|
|
255
|
+
},
|
|
256
|
+
timestamp: { type: "string" },
|
|
257
|
+
checks: { type: "array" }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} }, async (_, reply) => {
|
|
262
|
+
const results = await runChecks(readinessChecks);
|
|
263
|
+
const criticalFailed = results.some((result, index) => !result.healthy && (readinessChecks[index]?.critical ?? true));
|
|
264
|
+
const response = {
|
|
265
|
+
status: criticalFailed ? "not_ready" : "ready",
|
|
266
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
267
|
+
checks: results
|
|
268
|
+
};
|
|
269
|
+
if (criticalFailed) reply.code(503);
|
|
270
|
+
return response;
|
|
271
|
+
});
|
|
272
|
+
if (metrics) fastify.get(`${prefix}/metrics`, async (_, reply) => {
|
|
273
|
+
reply.type("text/plain; charset=utf-8");
|
|
274
|
+
if (metricsCollector) return await metricsCollector();
|
|
275
|
+
const uptime = process.uptime();
|
|
276
|
+
const memory = process.memoryUsage();
|
|
277
|
+
const cpu = process.cpuUsage();
|
|
278
|
+
const lines = [
|
|
279
|
+
"# HELP process_uptime_seconds Process uptime in seconds",
|
|
280
|
+
"# TYPE process_uptime_seconds gauge",
|
|
281
|
+
`process_uptime_seconds ${uptime.toFixed(2)}`,
|
|
282
|
+
"",
|
|
283
|
+
"# HELP process_memory_heap_bytes Heap memory usage in bytes",
|
|
284
|
+
"# TYPE process_memory_heap_bytes gauge",
|
|
285
|
+
`process_memory_heap_bytes{type="used"} ${memory.heapUsed}`,
|
|
286
|
+
`process_memory_heap_bytes{type="total"} ${memory.heapTotal}`,
|
|
287
|
+
"",
|
|
288
|
+
"# HELP process_memory_rss_bytes RSS memory in bytes",
|
|
289
|
+
"# TYPE process_memory_rss_bytes gauge",
|
|
290
|
+
`process_memory_rss_bytes ${memory.rss}`,
|
|
291
|
+
"",
|
|
292
|
+
"# HELP process_memory_external_bytes External memory in bytes",
|
|
293
|
+
"# TYPE process_memory_external_bytes gauge",
|
|
294
|
+
`process_memory_external_bytes ${memory.external}`,
|
|
295
|
+
"",
|
|
296
|
+
"# HELP process_cpu_user_microseconds User CPU time in microseconds",
|
|
297
|
+
"# TYPE process_cpu_user_microseconds counter",
|
|
298
|
+
`process_cpu_user_microseconds ${cpu.user}`,
|
|
299
|
+
"",
|
|
300
|
+
"# HELP process_cpu_system_microseconds System CPU time in microseconds",
|
|
301
|
+
"# TYPE process_cpu_system_microseconds counter",
|
|
302
|
+
`process_cpu_system_microseconds ${cpu.system}`,
|
|
303
|
+
""
|
|
304
|
+
];
|
|
305
|
+
if (collectHttpMetrics && Object.keys(httpMetrics.requestsTotal).length > 0) {
|
|
306
|
+
lines.push("# HELP http_requests_total Total HTTP requests by status code", "# TYPE http_requests_total counter");
|
|
307
|
+
for (const [status, count] of Object.entries(httpMetrics.requestsTotal)) lines.push(`http_requests_total{status="${status}"} ${count}`);
|
|
308
|
+
lines.push("");
|
|
309
|
+
if (httpMetrics.durationCount > 0) {
|
|
310
|
+
const p50 = bucketQuantile(httpMetrics, .5);
|
|
311
|
+
const p95 = bucketQuantile(httpMetrics, .95);
|
|
312
|
+
const p99 = bucketQuantile(httpMetrics, .99);
|
|
313
|
+
lines.push("# HELP http_request_duration_milliseconds HTTP request duration (bucket upper bounds)", "# TYPE http_request_duration_milliseconds summary", `http_request_duration_milliseconds{quantile="0.5"} ${p50.toFixed(2)}`, `http_request_duration_milliseconds{quantile="0.95"} ${p95.toFixed(2)}`, `http_request_duration_milliseconds{quantile="0.99"} ${p99.toFixed(2)}`, `http_request_duration_milliseconds_sum ${httpMetrics.durationSumMs.toFixed(2)}`, `http_request_duration_milliseconds_count ${httpMetrics.durationCount}`, "");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return lines.join("\n");
|
|
317
|
+
});
|
|
318
|
+
if (collectHttpMetrics) fastify.addHook("onResponse", async (_request, reply) => {
|
|
319
|
+
const duration = reply.elapsedTime;
|
|
320
|
+
const statusBucket = `${Math.floor(reply.statusCode / 100)}xx`;
|
|
321
|
+
httpMetrics.requestsTotal[statusBucket] = (httpMetrics.requestsTotal[statusBucket] || 0) + 1;
|
|
322
|
+
let bucket = DURATION_BUCKETS_MS.length;
|
|
323
|
+
for (let i = 0; i < DURATION_BUCKETS_MS.length; i++) {
|
|
324
|
+
const bound = DURATION_BUCKETS_MS[i];
|
|
325
|
+
if (bound !== void 0 && duration <= bound) {
|
|
326
|
+
bucket = i;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
httpMetrics.durationBuckets[bucket] = (httpMetrics.durationBuckets[bucket] ?? 0) + 1;
|
|
331
|
+
httpMetrics.durationCount++;
|
|
332
|
+
httpMetrics.durationSumMs += duration;
|
|
333
|
+
});
|
|
334
|
+
fastify.log?.debug?.(`Health plugin registered at ${prefix} (alias), ${prefix}/live, ${prefix}/ready${metrics ? `, ${prefix}/metrics` : ""}`);
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* Run all health checks with timeout
|
|
338
|
+
*/
|
|
339
|
+
async function runChecks(checks) {
|
|
340
|
+
return Promise.all(checks.map(async (check) => {
|
|
341
|
+
const start = Date.now();
|
|
342
|
+
const timeout = check.timeout ?? 5e3;
|
|
343
|
+
let timer;
|
|
344
|
+
const abort = new AbortController();
|
|
345
|
+
try {
|
|
346
|
+
const checkPromise = Promise.resolve(check.check(abort.signal));
|
|
347
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
348
|
+
timer = setTimeout(() => {
|
|
349
|
+
const err = /* @__PURE__ */ new Error("Health check timeout");
|
|
350
|
+
abort.abort(err);
|
|
351
|
+
reject(err);
|
|
352
|
+
}, timeout);
|
|
353
|
+
});
|
|
354
|
+
const healthy = await Promise.race([checkPromise, timeoutPromise]);
|
|
355
|
+
return {
|
|
356
|
+
name: check.name,
|
|
357
|
+
healthy: Boolean(healthy),
|
|
358
|
+
duration: Date.now() - start
|
|
359
|
+
};
|
|
360
|
+
} catch (err) {
|
|
361
|
+
return {
|
|
362
|
+
name: check.name,
|
|
363
|
+
healthy: false,
|
|
364
|
+
duration: Date.now() - start,
|
|
365
|
+
error: err.message
|
|
366
|
+
};
|
|
367
|
+
} finally {
|
|
368
|
+
if (timer) clearTimeout(timer);
|
|
369
|
+
}
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
var health_default = fp(healthPlugin, {
|
|
373
|
+
name: "arc-health",
|
|
374
|
+
fastify: "5.x"
|
|
375
|
+
});
|
|
376
|
+
//#endregion
|
|
377
|
+
export { requestIdPlugin as a, createRequestIdGenerator as i, health_default as n, requestId_default as o, mergeHealthChecks as r, sanitizeRequestId as s, healthPlugin as t };
|
package/dist/hooks/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Cn as defineHook, Sn as createHookSystem, _n as afterDelete, bn as beforeDelete, cn as DefineHookOptions, dn as HookOperation, fn as HookPhase, gn as afterCreate, hn as HookSystemOptions, ln as HookContext, mn as HookSystem, pn as HookRegistration, un as HookHandler, vn as afterUpdate, xn as beforeUpdate, yn as beforeCreate } from "../index-DPRH0Cme.mjs";
|
|
2
2
|
export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as requireSingleHeaderValue } from "../headers-BvlQSDMB.mjs";
|
|
1
2
|
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DY6xq4sM.mjs";
|
|
2
3
|
import { t as createIdempotencyResult } from "../interface-n5hpKg1d.mjs";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
@@ -249,6 +250,26 @@ var MemoryIdempotencyStore = class {
|
|
|
249
250
|
* // Idempotency-Key: order-123-abc
|
|
250
251
|
*
|
|
251
252
|
* // If same key sent again within TTL, returns cached response
|
|
253
|
+
*
|
|
254
|
+
* ## Guarantee: best-effort effectively-once, NOT exactly-once
|
|
255
|
+
*
|
|
256
|
+
* The plugin gives strong request replay + overlap protection: a
|
|
257
|
+
* distributed lock guards execution (concurrent same-key requests get
|
|
258
|
+
* 409), the store key includes a body/caller fingerprint (a different
|
|
259
|
+
* body under the same key executes as its own operation instead of
|
|
260
|
+
* replaying someone else's response), and a completed result replays on
|
|
261
|
+
* hit. It does NOT make the business effect exactly-once, because the
|
|
262
|
+
* domain write and the idempotency record live in different systems:
|
|
263
|
+
*
|
|
264
|
+
* - a crash after the DB mutation but before the result is recorded
|
|
265
|
+
* (preSerialization) lets a retry re-execute the mutation;
|
|
266
|
+
* - a handler that outruns `lockTimeoutMs` can overlap its own retry —
|
|
267
|
+
* size the lock timeout above the slowest protected handler.
|
|
268
|
+
*
|
|
269
|
+
* For mutations where re-execution is unacceptable, write the idempotency
|
|
270
|
+
* record in the SAME transaction as the business write inside the handler
|
|
271
|
+
* (the store contract is public — `@classytic/arc/idempotency` exports it),
|
|
272
|
+
* or make the handler naturally idempotent (upserts, compare-and-set).
|
|
252
273
|
*/
|
|
253
274
|
const HEADER_IDEMPOTENCY_REPLAYED = "x-idempotency-replayed";
|
|
254
275
|
const HEADER_IDEMPOTENCY_KEY = "x-idempotency-key";
|
|
@@ -306,7 +327,11 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
|
|
|
306
327
|
*/
|
|
307
328
|
function normalizeBody(obj, depth = 0) {
|
|
308
329
|
if (obj === null || typeof obj !== "object") return obj;
|
|
309
|
-
if (depth >= MAX_FINGERPRINT_DEPTH)
|
|
330
|
+
if (depth >= MAX_FINGERPRINT_DEPTH) try {
|
|
331
|
+
return `<deep:${createHash("sha256").update(JSON.stringify(obj)).digest("hex").slice(0, 16)}>`;
|
|
332
|
+
} catch {
|
|
333
|
+
return "<truncated>";
|
|
334
|
+
}
|
|
310
335
|
if (Array.isArray(obj)) return obj.map((item) => normalizeBody(item, depth + 1));
|
|
311
336
|
const sorted = {};
|
|
312
337
|
const keys = Object.keys(obj).sort();
|
|
@@ -335,8 +360,7 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
|
|
|
335
360
|
}
|
|
336
361
|
const idempotencyMiddleware = async (request, reply) => {
|
|
337
362
|
if (!shouldApplyIdempotency(request)) return;
|
|
338
|
-
const
|
|
339
|
-
const idempotencyKey = typeof keyHeader === "string" ? keyHeader.trim() : void 0;
|
|
363
|
+
const idempotencyKey = requireSingleHeaderValue(request.headers, headerName)?.trim();
|
|
340
364
|
if (!idempotencyKey) return;
|
|
341
365
|
request.idempotencyKey = idempotencyKey;
|
|
342
366
|
const fullKey = `${idempotencyKey}:${getRequestFingerprint(request)}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { r as CacheStore, t as CacheLogger } from "./interface-
|
|
2
|
-
import { i as RequestScope, n as Mandate } from "./types-
|
|
3
|
-
import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-
|
|
1
|
+
import { r as CacheStore, t as CacheLogger } from "./interface-BGZCzOGL.mjs";
|
|
2
|
+
import { i as RequestScope, n as Mandate } from "./types-DjC-KUCz.mjs";
|
|
3
|
+
import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-Dmw-6Qeo.mjs";
|
|
4
4
|
import { FastifyReply, FastifyRequest } from "fastify";
|
|
5
5
|
//#region src/permissions/agent.d.ts
|
|
6
6
|
/**
|
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
import { FastifyPluginAsync } from "fastify";
|
|
2
|
-
//#region src/integrations/jobs.d.ts
|
|
2
|
+
//#region src/integrations/jobs/execution.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Timed handler execution — the concurrency core of the jobs worker,
|
|
5
|
+
* extracted from the BullMQ registration so the algorithm is testable
|
|
6
|
+
* without queue mocks.
|
|
7
|
+
*
|
|
8
|
+
* Contract:
|
|
9
|
+
* - The handler receives an `AbortSignal`, aborted when `timeoutMs`
|
|
10
|
+
* elapses. The timeout rejects THIS call; the handler itself is only
|
|
11
|
+
* signalled — cooperative cancellation.
|
|
12
|
+
* - `releaseSlot` (the bulkhead semaphore) fires when the handler
|
|
13
|
+
* promise SETTLES, not when the timeout rejects — a timed-out handler
|
|
14
|
+
* that keeps running still occupies its concurrency slot, so retries
|
|
15
|
+
* can't stack live executions past the limit.
|
|
16
|
+
* - `cancelGraceMs` bounds that hold: a handler ignoring its signal past
|
|
17
|
+
* timeout + grace has its slot force-released (logged loudly — beyond
|
|
18
|
+
* this point the bulkhead can be exceeded by the abandoned execution).
|
|
19
|
+
* - A timed-out handler's eventual settle is logged for observability;
|
|
20
|
+
* its result is discarded.
|
|
21
|
+
* - `releaseSlot` is invoked exactly once, on every path (success,
|
|
22
|
+
* failure, timeout-then-settle, grace expiry). Synchronous throws from
|
|
23
|
+
* the handler are converted to rejections so release always fires.
|
|
24
|
+
*/
|
|
25
|
+
interface TimedHandlerLogger {
|
|
26
|
+
warn(obj: Record<string, unknown>, msg: string): void;
|
|
27
|
+
}
|
|
28
|
+
interface ExecuteTimedHandlerOptions<T> {
|
|
29
|
+
/** The work. Must treat the signal as its cancellation channel. */
|
|
30
|
+
run: (signal: AbortSignal) => Promise<T>;
|
|
31
|
+
/** Job/queue label for timeout errors and logs. */
|
|
32
|
+
label: string;
|
|
33
|
+
/** Correlation id for logs (e.g. the BullMQ job id). */
|
|
34
|
+
jobId?: string;
|
|
35
|
+
/** Reject after this many ms and abort the signal. Omit = no timeout. */
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
/** Slot hold-bound after timeout (default: 30_000). */
|
|
38
|
+
cancelGraceMs?: number;
|
|
39
|
+
/** Bulkhead slot release — called exactly once. Omit when unguarded. */
|
|
40
|
+
releaseSlot?: () => void;
|
|
41
|
+
logger?: TimedHandlerLogger;
|
|
42
|
+
}
|
|
43
|
+
declare function executeTimedHandler<T>(options: ExecuteTimedHandlerOptions<T>): Promise<T>;
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/integrations/jobs/types.d.ts
|
|
46
|
+
/**
|
|
47
|
+
* Job queue integration — public contract (definitions, meta, dispatcher).
|
|
48
|
+
*
|
|
49
|
+
* Directory map (mechanical split of the former single file):
|
|
50
|
+
* types.ts — this contract + `defineJob`
|
|
51
|
+
* execution.ts — `executeTimedHandler` (timeout / cancellation / bulkhead)
|
|
52
|
+
* repeat.ts — repeatable-schedule reconciliation (boot hygiene)
|
|
53
|
+
* plugin.ts — BullMQ wiring (queues, workers, DLQ, routes, shutdown)
|
|
54
|
+
* index.ts — public barrel; `src/integrations/jobs.ts` re-exports it
|
|
55
|
+
* so the package subpath is unchanged
|
|
56
|
+
*/
|
|
3
57
|
/** Repeat schedule — cron pattern or fixed interval. Explicit timezone is required. */
|
|
4
58
|
interface JobRepeatOptions {
|
|
5
59
|
/** Cron pattern (e.g. '0 9 * * *' = every day 09:00). Mutually exclusive with `every`. */
|
|
@@ -44,13 +98,37 @@ interface JobDefinition<TData = unknown, TResult = unknown> {
|
|
|
44
98
|
* and appear as `throttled: true` in `GET /jobs/:id/status`.
|
|
45
99
|
* Distinct from BullMQ `concurrency` — use this when the constraint is a
|
|
46
100
|
* downstream resource (AI model rate limit, DB connection pool, external API).
|
|
101
|
+
*
|
|
102
|
+
* The slot is held until the handler actually SETTLES — a timed-out
|
|
103
|
+
* handler that keeps running (ignoring `meta.signal`) still counts
|
|
104
|
+
* against `maxConcurrent`, so retries can't stack live executions past
|
|
105
|
+
* the limit. `cancelGraceMs` bounds how long a stuck handler may hold
|
|
106
|
+
* its slot after timeout.
|
|
47
107
|
*/
|
|
48
108
|
maxConcurrent?: number;
|
|
109
|
+
/**
|
|
110
|
+
* How long after a TIMEOUT a still-running handler may keep its
|
|
111
|
+
* `maxConcurrent` slot before the slot is force-released (default:
|
|
112
|
+
* 30_000). The bulkhead limits LIVE handlers, not worker awaits — but a
|
|
113
|
+
* handler that ignores `meta.signal` forever must not wedge the queue.
|
|
114
|
+
* A force-release is logged loudly: past that point the bulkhead can be
|
|
115
|
+
* exceeded by the abandoned execution.
|
|
116
|
+
*/
|
|
117
|
+
cancelGraceMs?: number;
|
|
49
118
|
}
|
|
50
119
|
interface JobMeta {
|
|
51
120
|
jobId: string;
|
|
52
121
|
attemptsMade: number;
|
|
53
122
|
timestamp: number;
|
|
123
|
+
/**
|
|
124
|
+
* Aborted when the job's `timeout` elapses. Pass it to fetch/DB calls so
|
|
125
|
+
* timed-out work actually STOPS — the timeout alone only rejects the
|
|
126
|
+
* worker's await (releasing the concurrency slot) while the handler keeps
|
|
127
|
+
* running; a BullMQ retry can then overlap the original execution.
|
|
128
|
+
* Handlers that ignore the signal must be idempotent / compare-and-set
|
|
129
|
+
* safe under that overlap.
|
|
130
|
+
*/
|
|
131
|
+
signal: AbortSignal;
|
|
54
132
|
}
|
|
55
133
|
interface JobDispatchOptions {
|
|
56
134
|
/** Delay job execution by ms */
|
|
@@ -148,6 +226,8 @@ interface QueueStats {
|
|
|
148
226
|
* });
|
|
149
227
|
*/
|
|
150
228
|
declare function defineJob<TData = unknown, TResult = unknown>(definition: JobDefinition<TData, TResult>): JobDefinition<TData, TResult>;
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/integrations/jobs/plugin.d.ts
|
|
151
231
|
/**
|
|
152
232
|
* Pluggable BullMQ job queue integration for Arc.
|
|
153
233
|
*
|
|
@@ -156,4 +236,4 @@ declare function defineJob<TData = unknown, TResult = unknown>(definition: JobDe
|
|
|
156
236
|
*/
|
|
157
237
|
declare const jobsPlugin: FastifyPluginAsync<JobsPluginOptions>;
|
|
158
238
|
//#endregion
|
|
159
|
-
export {
|
|
239
|
+
export { JobMeta as a, JobsPluginOptions as c, ExecuteTimedHandlerOptions as d, TimedHandlerLogger as f, JobDispatcher as i, QueueStats as l, JobDefinition as n, JobRepeatOptions as o, executeTimedHandler as p, JobDispatchOptions as r, JobStatus as s, jobsPlugin as t, defineJob as u };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { a as ModuleContribution, h as HealthCheck, n as ArcModuleInput, r as ArcModuleRegistry, t as ArcModule, u as ScheduleDefinition } from "./types-Cu6vVJmh.mjs";
|
|
2
|
+
import { FastifyInstance } from "fastify";
|
|
3
|
+
//#region src/factory/module/contributions.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Collect every module's `healthChecks` in the caller's order. Fails at boot
|
|
6
|
+
* on a duplicate check `name` across modules, naming BOTH owning modules.
|
|
7
|
+
* The host's app-level checks are appended AFTER this (modules-first,
|
|
8
|
+
* host-last) by the caller.
|
|
9
|
+
*/
|
|
10
|
+
declare function collectModuleHealthChecks(modules: readonly ArcModule[]): HealthCheck[];
|
|
11
|
+
/**
|
|
12
|
+
* Collect every module's `workflows`. Resolves factory contributions (so a
|
|
13
|
+
* factory can close over a container the caller has already decorated on
|
|
14
|
+
* `fastify`) and flattens to a single opaque array. Arc core deliberately
|
|
15
|
+
* does NOT validate or interpret the values — the streamline integration
|
|
16
|
+
* owns name/shape checks and registration. Called by the integration at init
|
|
17
|
+
* time, not by arc's boot pipeline (workflows depend on the integration's
|
|
18
|
+
* container).
|
|
19
|
+
*/
|
|
20
|
+
declare function collectModuleWorkflows(fastify: FastifyInstance, modules: readonly ArcModule[]): Promise<unknown[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Collect every module's `scheduledJobs`. Resolves factory contributions
|
|
23
|
+
* (after bootstraps) and fails at boot on a duplicate job `name` across
|
|
24
|
+
* modules. The caller passes the returned definitions to the canonical
|
|
25
|
+
* schedules plugin; factories must therefore be resolved only once.
|
|
26
|
+
*/
|
|
27
|
+
declare function collectModuleScheduledJobs(fastify: FastifyInstance, modules: readonly ArcModule[]): Promise<ScheduleDefinition[]>;
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/factory/module/lifecycle.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe every module's `eventHandlers` in dependency order (pass the
|
|
32
|
+
* `orderModules`-sorted list). Returns the ordered list of unsubscribe
|
|
33
|
+
* functions so the caller can tear them down at shutdown BEFORE module
|
|
34
|
+
* `onClose` (while module deps are still alive). Fails at boot on:
|
|
35
|
+
* - a duplicate NAMED handler across modules (attributing both owners);
|
|
36
|
+
* - a module declaring handlers while the event subsystem is unavailable.
|
|
37
|
+
* Resolves factory contributions (after bootstraps) so a handler can close over
|
|
38
|
+
* booted engines rather than a global getter.
|
|
39
|
+
*/
|
|
40
|
+
declare function subscribeModuleEventHandlers(fastify: FastifyInstance, modules: readonly ArcModule[]): Promise<Array<() => void | Promise<void>>>;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/factory/module/order.d.ts
|
|
43
|
+
/**
|
|
44
|
+
* Order modules for composition by their `dependsOn` edges — a STABLE
|
|
45
|
+
* topological sort. Called once at the start of the bootstrap phase; every
|
|
46
|
+
* subsequent phase (bootstrap, resources, afterResources, and reverse-order
|
|
47
|
+
* onClose) iterates the returned list, so a module's declared dependencies are
|
|
48
|
+
* always composed before it.
|
|
49
|
+
*
|
|
50
|
+
* "Stable" = modules with no edge between them keep their original list order,
|
|
51
|
+
* so declaring `dependsOn` on one module never silently reorders an unrelated
|
|
52
|
+
* one. Backward compatible: a `modules` array with NO `dependsOn` anywhere is
|
|
53
|
+
* returned unchanged.
|
|
54
|
+
*
|
|
55
|
+
* Fail-fast — throws (never reorders past a broken contract) on:
|
|
56
|
+
* - duplicate module names (the name is the graph key)
|
|
57
|
+
* - a `dependsOn` name not present in the composed set
|
|
58
|
+
* - a self-reference (`dependsOn` includes the module's own name)
|
|
59
|
+
* - a dependency cycle (reports the concrete `a → b → … → a` path)
|
|
60
|
+
*/
|
|
61
|
+
declare function orderModules(modules: readonly ArcModule[]): ArcModule[];
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/factory/module/resolve.d.ts
|
|
64
|
+
/**
|
|
65
|
+
* Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
|
|
66
|
+
* (this is where a dynamic `import()` fires); a promise (or plain module) is
|
|
67
|
+
* awaited. Called once per module at the start of the bootstrap phase.
|
|
68
|
+
*/
|
|
69
|
+
declare function resolveModule(input: ArcModuleInput): Promise<ArcModule>;
|
|
70
|
+
/** Resolve a `ModuleContribution` to a concrete array (array passthrough or factory call). */
|
|
71
|
+
declare function resolveContribution<T>(contribution: ModuleContribution<T> | undefined, fastify: FastifyInstance): Promise<readonly T[]>;
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/factory/module/index.d.ts
|
|
74
|
+
/**
|
|
75
|
+
* Identity helper for authoring a typed module — mirrors `defineResource`.
|
|
76
|
+
* Gives inference + a single obvious construction site; does no work.
|
|
77
|
+
*
|
|
78
|
+
* `TExports` is inferred from the `bootstrap` return value, so a module
|
|
79
|
+
* author gets a typed public export for free:
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* export const accountingModule = (deps: Deps) =>
|
|
83
|
+
* defineModule({
|
|
84
|
+
* name: "accounting",
|
|
85
|
+
* bootstrap: async () => createAccountingEngine(deps), // TExports inferred
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
declare function defineModule<TExports = unknown>(module: ArcModule<TExports>): ArcModule<TExports>;
|
|
90
|
+
/**
|
|
91
|
+
* Typed accessor for a module's public export (its `bootstrap` return value),
|
|
92
|
+
* recorded at `fastify.arc.modules[name]`.
|
|
93
|
+
*
|
|
94
|
+
* Throwing, in line with the fail-fast contract (and `requireOrgId`-style
|
|
95
|
+
* accessors): a missing entry means the module either isn't composed, is
|
|
96
|
+
* composed AFTER the caller (list order = init order), or returned nothing —
|
|
97
|
+
* all wiring bugs that must surface at boot, not as `undefined` downstream.
|
|
98
|
+
*
|
|
99
|
+
* The type parameter is an assertion, not a proof. Two ways to type it:
|
|
100
|
+
* - augment `ArcModuleRegistry` once — then the name alone infers the type:
|
|
101
|
+
* `getModuleExports(f, "accounting")` → `AccountingEngine`.
|
|
102
|
+
* - or pass it inline: `getModuleExports<AccountingEngine>(f, "accounting")`.
|
|
103
|
+
*/
|
|
104
|
+
declare function getModuleExports<K extends keyof ArcModuleRegistry>(fastify: FastifyInstance, name: K): ArcModuleRegistry[K];
|
|
105
|
+
declare function getModuleExports<TExports = unknown>(fastify: FastifyInstance, name: string): TExports;
|
|
106
|
+
//#endregion
|
|
107
|
+
export { orderModules as a, collectModuleScheduledJobs as c, resolveModule as i, collectModuleWorkflows as l, getModuleExports as n, subscribeModuleEventHandlers as o, resolveContribution as r, collectModuleHealthChecks as s, defineModule as t };
|