@classytic/arc 2.28.0 → 2.31.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/LICENSE +1 -1
- package/README.md +7 -0
- package/TRADEMARK.md +22 -0
- package/dist/{BaseController-B65zbz9m.mjs → BaseController-De7GqHck.mjs} +25 -24
- package/dist/{EventTransport-QmY7A258.d.mts → EventTransport-2uVV34WU.d.mts} +1 -0
- package/dist/EventTransport-9mlTgqzE.mjs +104 -0
- package/dist/{HookSystem-C1vB_Gc9.mjs → HookSystem-BcmPyq5I.mjs} +16 -35
- package/dist/{QueryCache-DOMT_iq7.d.mts → QueryCache-DFA-QrU9.d.mts} +2 -1
- package/dist/{ResourceRegistry-y9Hnypus.mjs → ResourceRegistry-qq0mn2s9.mjs} +3 -2
- package/dist/{actionPermissions-CAphcmUG.mjs → actionPermissions-BE5NwaQk.mjs} +1 -0
- package/dist/audit/index.d.mts +3 -2
- package/dist/audit/index.mjs +3 -2
- package/dist/auth/audit.d.mts +1 -0
- package/dist/auth/audit.mjs +1 -0
- package/dist/auth/index.d.mts +5 -4
- package/dist/auth/index.mjs +6 -5
- package/dist/auth/redis-session.d.mts +2 -1
- package/dist/auth/redis-session.mjs +1 -0
- package/dist/authorizationDecision-ChvUQGG1.mjs +266 -0
- package/dist/{buildHandler-Dtwd8fdz.mjs → buildHandler-CDrCJzmB.mjs} +34 -7
- package/dist/cache/index.d.mts +4 -3
- package/dist/cache/index.mjs +4 -3
- package/dist/{caching-_NJeDd1_.mjs → caching-BFHA6Scf.mjs} +2 -1
- package/dist/cleanup/index.d.mts +4 -3
- package/dist/cleanup/index.mjs +9 -13
- package/dist/cli/commands/describe.d.mts +2 -1
- package/dist/cli/commands/describe.mjs +3 -2
- package/dist/cli/commands/docs.d.mts +1 -0
- package/dist/cli/commands/docs.mjs +3 -2
- package/dist/cli/commands/doctor.d.mts +1 -0
- package/dist/cli/commands/doctor.mjs +1 -0
- package/dist/cli/commands/generate.d.mts +2 -1
- package/dist/cli/commands/generate.mjs +2 -1
- package/dist/cli/commands/init.d.mts +2 -1
- package/dist/cli/commands/init.mjs +2 -1
- package/dist/cli/commands/introspect.d.mts +1 -0
- package/dist/cli/commands/introspect.mjs +2 -1
- package/dist/cli/index.d.mts +3 -2
- package/dist/cli/index.mjs +3 -2
- package/dist/{constants-DNre0wRg.mjs → constants-D3Dv4LAV.mjs} +1 -0
- package/dist/context/index.d.mts +1 -0
- package/dist/context/index.mjs +3 -2
- package/dist/core/index.d.mts +3 -2
- package/dist/core/index.mjs +7 -5
- package/dist/{core-BGa8TXJR.mjs → core-BGpIxINN.mjs} +4 -49
- package/dist/{createActionRouter-C-N7z2EL.mjs → createActionRouter-CDXEdasl.mjs} +8 -7
- package/dist/{createAggregationRouter-wzwFa_dW.mjs → createAggregationRouter-KrQDzx-k.mjs} +10 -22
- package/dist/{createApp-Du5G54TM.mjs → createApp-Df89BEni.mjs} +505 -209
- package/dist/defineAggregation-DbPPS8RI.mjs +48 -0
- package/dist/{defineResource-2zvVVpxg.mjs → defineResource-Bv6EtN1x.mjs} +90 -72
- package/dist/discovery/index.d.mts +1 -0
- package/dist/discovery/index.mjs +1 -0
- package/dist/docs/index.d.mts +3 -2
- package/dist/docs/index.mjs +3 -2
- package/dist/{elevation-C9s5hFP_.d.mts → elevation-BXCfVcNG.d.mts} +1 -0
- package/dist/{elevation-CIz1GjGm.mjs → elevation-D2l1mh4g.mjs} +4 -3
- package/dist/encryption/index.d.mts +2 -1
- package/dist/encryption/index.mjs +3 -2
- package/dist/{errorHandler-Bq2OeEQL.mjs → errorHandler-C5NeD6bT.mjs} +3 -2
- package/dist/{errorHandler-C6yv4ZTW.d.mts → errorHandler-CpYUJmPA.d.mts} +1 -0
- package/dist/{errors-DHcHVAgk.d.mts → errors-BGdVwP25.d.mts} +1 -0
- package/dist/{errors-DkO8Nk6-.mjs → errors-Cj42AntO.mjs} +1 -0
- package/dist/{eventPlugin-DrQ_FesK.mjs → eventPlugin-CkffEihm.mjs} +6 -106
- package/dist/{eventPlugin-Bg3cP2_u.d.mts → eventPlugin-GF5nnBIq.d.mts} +2 -1
- package/dist/events/index.d.mts +4 -3
- package/dist/events/index.mjs +17 -340
- package/dist/events/transports/redis-stream-entry.d.mts +2 -1
- package/dist/events/transports/redis-stream-entry.mjs +1 -0
- package/dist/events/transports/redis.d.mts +2 -1
- package/dist/events/transports/redis.mjs +1 -0
- package/dist/{externalPaths-BE6wFpYv.d.mts → externalPaths-CmGOxOtD.d.mts} +1 -0
- package/dist/factory/index.d.mts +9 -5
- package/dist/factory/index.mjs +7 -6
- package/dist/{fastify-augmentation-OzYGIiU1.d.mts → fastify-augmentation-zh0_AYHa.d.mts} +13 -4
- package/dist/{applyPermissionResult-BnyVE9eD.mjs → fields-CB5Cuzk9.mjs} +2 -107
- package/dist/{fields-D3lhgzhD.d.mts → fields-I_EeRWND.d.mts} +1 -0
- package/dist/{generate-BnsL9GBm.d.mts → generate-DCGuEbqI.d.mts} +1 -0
- package/dist/{generate-D0wZupBg.mjs → generate-EcA6ui4i.mjs} +2 -1
- package/dist/{headers-BvlQSDMB.mjs → headers-3zvE0rM3.mjs} +2 -1
- package/dist/{health-CB_m4S32.mjs → health-DjZYjM7H.mjs} +1 -0
- package/dist/hooks/index.d.mts +2 -1
- package/dist/hooks/index.mjs +2 -1
- package/dist/idempotency/index.d.mts +3 -2
- package/dist/idempotency/index.mjs +5 -4
- package/dist/idempotency/redis.d.mts +2 -1
- package/dist/idempotency/redis.mjs +1 -0
- package/dist/{index-CjVg-Epg.d.mts → index-Bc2ox8_U.d.mts} +6 -5
- package/dist/{index-CG4yqMDB.d.mts → index-C-481Zf2.d.mts} +74 -18
- package/dist/{index-BMnbzvtO.d.mts → index-CEJNZFtX.d.mts} +242 -24
- package/dist/index-CFThn1K9.d.mts +224 -0
- package/dist/{index-DPRH0Cme.d.mts → index-CpzxqePp.d.mts} +203 -97
- package/dist/index.d.mts +9 -8
- package/dist/index.mjs +11 -9
- package/dist/{init-tlRSt667.mjs → init-BEiNZBsJ.mjs} +19 -23
- package/dist/{init-BPEu8cs-.d.mts → init-Cscs4lab.d.mts} +1 -0
- package/dist/integrations/event-gateway.d.mts +3 -2
- package/dist/integrations/event-gateway.mjs +2 -1
- package/dist/integrations/index.d.mts +4 -3
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs/index.d.mts +2 -1
- package/dist/integrations/jobs/index.mjs +108 -32
- package/dist/integrations/mcp/index.d.mts +9 -9
- package/dist/integrations/mcp/index.mjs +2 -1
- package/dist/integrations/mcp/testing.d.mts +3 -2
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +1 -0
- package/dist/integrations/streamline.mjs +3 -2
- package/dist/integrations/webhooks.d.mts +1 -0
- package/dist/integrations/webhooks.mjs +1 -0
- package/dist/integrations/websocket-pushref-redis.d.mts +2 -1
- package/dist/integrations/websocket-pushref-redis.mjs +1 -0
- package/dist/integrations/websocket-redis.d.mts +2 -1
- package/dist/integrations/websocket-redis.mjs +1 -0
- package/dist/integrations/websocket.d.mts +3 -2
- package/dist/integrations/websocket.mjs +1 -0
- package/dist/{interface-BGZCzOGL.d.mts → interface-BOk3YnTz.d.mts} +1 -0
- package/dist/{interface-Uk8ONJv7.d.mts → interface-DEWNOMza.d.mts} +1 -0
- package/dist/{interface-n5hpKg1d.mjs → interface-DYzM0q-u.mjs} +1 -0
- package/dist/{jsonBody-BlzAoGCe.mjs → jsonBody-BYbLDi5D.mjs} +2 -1
- package/dist/{keys-Cd316u_y.mjs → keys-C0_pa1tY.mjs} +1 -0
- package/dist/{loadResources-DpU8pfjV.mjs → loadResources-yawi9tAE.mjs} +2 -1
- package/dist/{loadResourcesFromEntry-kvGT6EcO.mjs → loadResourcesFromEntry-BAjYZonx.mjs} +2 -1
- package/dist/logger/index.d.mts +1 -0
- package/dist/logger/index.mjs +1 -0
- package/dist/{mcpPlugin-Cs6nRGCt.mjs → mcpPlugin-B_es0hiF.mjs} +52 -60
- package/dist/{memory-CmnzXE5Z.mjs → memory-DR6wpa9F.mjs} +2 -1
- package/dist/{metrics-fdxI5mn_.mjs → metrics-CUdw_wLB.mjs} +2 -1
- package/dist/middleware/index.d.mts +2 -1
- package/dist/middleware/index.mjs +3 -2
- package/dist/migrations/index.d.mts +1 -3
- package/dist/migrations/index.mjs +2 -4
- package/dist/module-0pFJA6YW.mjs +78 -0
- package/dist/{multipartBody-D0eBvkeq.mjs → multipartBody-wOgj_fnj.mjs} +1 -0
- package/dist/{openapi-BLV4OMXb.mjs → openapi-B2L4js1v.mjs} +6 -5
- package/dist/permissions/index.d.mts +5 -4
- package/dist/permissions/index.mjs +6 -4
- package/dist/{permissions-BrtiBKGs.mjs → permissions-L--QDAir.mjs} +518 -316
- package/dist/{pipe-BOLmGIxa.mjs → pipe-DRDNsI2T.mjs} +2 -1
- package/dist/pipeline/index.d.mts +2 -1
- package/dist/pipeline/index.mjs +2 -1
- package/dist/{pipeline-BGRnV-To.mjs → pipeline-UPQFatit.mjs} +7 -20
- package/dist/plugins/index.d.mts +8 -7
- package/dist/plugins/index.mjs +16 -15
- package/dist/plugins/response-cache.d.mts +1 -0
- package/dist/plugins/response-cache.mjs +2 -1
- package/dist/plugins/tracing-entry.d.mts +2 -1
- package/dist/plugins/tracing-entry.mjs +2 -1
- package/dist/{pluralize-BhrgwZb9.mjs → pluralize-O0FZgESl.mjs} +1 -0
- package/dist/{preloadResources-CM4uUaFv.d.mts → preloadResources-CyoJMSiO.d.mts} +117 -12
- package/dist/{preloadResources-DSY5HGpS.mjs → preloadResources-Dz6BjzM6.mjs} +1 -0
- package/dist/presets/filesUpload.d.mts +6 -5
- package/dist/presets/filesUpload.mjs +9 -11
- package/dist/presets/index.d.mts +15 -2
- package/dist/presets/index.mjs +2 -1
- package/dist/presets/multiTenant.d.mts +2 -1
- package/dist/presets/multiTenant.mjs +3 -2
- package/dist/presets/search.d.mts +3 -2
- package/dist/presets/search.mjs +2 -1
- package/dist/{presets-DwFDVKP0.mjs → presets-C4-g4_4P.mjs} +27 -10
- package/dist/projection-BHw6xlOQ.mjs +18 -0
- package/dist/{pushref-registry-BbcnpNTm.d.mts → pushref-registry-Dov9l4qE.d.mts} +1 -0
- package/dist/{queryCachePlugin-DSGuSVLW.mjs → queryCachePlugin-BZr8v46E.mjs} +5 -4
- package/dist/{queryCachePlugin-br4otiqF.d.mts → queryCachePlugin-BzqxconG.d.mts} +3 -2
- package/dist/{rateLimit-DrED-C4v.mjs → rateLimit-CMPY5Qms.mjs} +7 -3
- package/dist/{rateLimitKey-BoWRH3e7.mjs → rateLimitKey-Dn5JNmkj.mjs} +1 -0
- package/dist/{realtime-DEuDu9io.mjs → realtime-C5NoxWFY.mjs} +20 -8
- package/dist/{redis-UDtLaNj6.d.mts → redis-CduhKsJi.d.mts} +2 -1
- package/dist/{redis-stream-uIaauNpZ.d.mts → redis-stream-qN6A-RFv.d.mts} +2 -1
- package/dist/{registerRoute-DTp18Ah_.mjs → registerRoute-CBN84fc7.mjs} +5 -2
- package/dist/registry/index.d.mts +3 -2
- package/dist/registry/index.mjs +3 -2
- package/dist/{registry-9km_w1aP.mjs → registry-4213U4zb.mjs} +3 -2
- package/dist/{renewingLease-3cuQH7h8.mjs → renewingLease-Dll8mGgc.mjs} +1 -0
- package/dist/{replyHelpers-67tRawQZ.mjs → replyHelpers-DIiZKGUt.mjs} +2 -1
- package/dist/repositoryFilter-f9eAfq_l.mjs +45 -0
- package/dist/{requestContext-MrlJwECf.mjs → requestContext-2TxXOJ7T.mjs} +1 -0
- package/dist/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-CUsjNX9R.mjs} +1 -0
- package/dist/{schedules-CjOd2MG9.mjs → schedules-DdtlICTf.mjs} +3 -2
- package/dist/{schemaConverter-DJvodiHj.mjs → schemaConverter-D--wdJM-.mjs} +70 -17
- package/dist/{schemaIR-Ca7ySNPx.mjs → schemaIR-Bm2enbnh.mjs} +2 -1
- package/dist/schemas/index.d.mts +1 -0
- package/dist/schemas/index.mjs +1 -0
- package/dist/scim/index.d.mts +1 -0
- package/dist/scim/index.mjs +2 -1
- package/dist/scope/index.d.mts +3 -2
- package/dist/scope/index.mjs +5 -4
- package/dist/{scope-DfaPntpo.mjs → scope-DHx5Fous.mjs} +4 -3
- package/dist/{sessionManager-BHOM3N_0.d.mts → sessionManager-qimlKV0P.d.mts} +1 -0
- package/dist/{simpleEqualityMatcher-Cg05HPsn.mjs → simpleEqualityMatcher-DjQJ7FKA.mjs} +1 -0
- package/dist/{sse-BQ1w0dfl.mjs → sse-CUTo9y0X.mjs} +5 -4
- package/dist/{sseStream-BQ7Qaynp.mjs → sseStream-B_5XcVVd.mjs} +2 -1
- package/dist/{storage-DLUZ04kK.d.mts → storage-Cx9Sz3Ou.d.mts} +1 -0
- package/dist/{store-helpers-DY6xq4sM.mjs → store-helpers-D1MP_gRG.mjs} +1 -0
- package/dist/{streaming-BQcW6_Kq.mjs → streaming-PSZwi8JV.mjs} +1 -0
- package/dist/subscribe-helpers-C0Tb2aUp.mjs +340 -0
- package/dist/sync/changelog.d.mts +1 -0
- package/dist/sync/changelog.mjs +1 -0
- package/dist/testing/cleanupStoreContract.d.mts +2 -1
- package/dist/testing/cleanupStoreContract.mjs +1 -0
- package/dist/testing/index.d.mts +167 -12
- package/dist/testing/index.mjs +522 -205
- package/dist/testing/outboxStoreContract.d.mts +1 -0
- package/dist/testing/outboxStoreContract.mjs +1 -0
- package/dist/testing/storageContract.d.mts +2 -1
- package/dist/testing/storageContract.mjs +1 -0
- package/dist/{tracing-CsGKQJGg.d.mts → tracing-Dro5eH6j.d.mts} +1 -0
- package/dist/{transactionContext-PNdoyLLm.mjs → transactionContext-dAfBVUrk.mjs} +1 -0
- package/dist/{typeGuards-C9KXUly6.mjs → typeGuards-B0ryPo3I.mjs} +1 -0
- package/dist/types/index.d.mts +6 -5
- package/dist/types/index.mjs +1 -0
- package/dist/types/storage.d.mts +2 -1
- package/dist/types/storage.mjs +1 -0
- package/dist/{types-Dmw-6Qeo.d.mts → types-B7XeLEgK.d.mts} +127 -90
- package/dist/{types-Cc91nXuA.d.mts → types-ByHn9rEO.d.mts} +1 -0
- package/dist/{types-C_ZZRcD6.d.mts → types-C4QE8qnm.d.mts} +1 -0
- package/dist/{types-Cu6vVJmh.d.mts → types-CBgvkp8O.d.mts} +82 -12
- package/dist/{types-DjC-KUCz.d.mts → types-ChuShpCP.d.mts} +1 -0
- package/dist/{types-QFGc3Cjv.d.mts → types-CwcUQ610.d.mts} +2 -6
- package/dist/{types-CSyCxl2V.mjs → types-Cys0ko9Z.mjs} +4 -3
- package/dist/usage/index.d.mts +3 -2
- package/dist/usage/index.mjs +0 -0
- package/dist/{userHelpers-Cg_y3fBz.mjs → userHelpers-ChYJ6CdK.mjs} +1 -0
- package/dist/utils/index.d.mts +16 -10
- package/dist/utils/index.mjs +10 -9
- package/dist/{utils-taUVD4p3.mjs → utils-vL8BrDE2.mjs} +3 -2
- package/dist/{validate-Bk44F2yT.mjs → validate-_KbifpHF.mjs} +2 -1
- package/dist/{versioning-gKWyryT3.mjs → versioning-CHDTor3P.mjs} +3 -2
- package/dist/{versioning-DXgY5BUm.d.mts → versioning-D_rhHfZm.d.mts} +5 -2
- package/dist/{websocket-_Pd0CgR0.d.mts → websocket-BOXVggMf.d.mts} +2 -1
- package/package.json +15 -7
- package/skills/arc/SKILL.md +22 -8
- package/skills/arc/references/api-reference.md +4 -0
- package/skills/arc/references/auth.md +3 -0
- package/skills/arc/references/integrations.md +33 -0
- package/skills/arc/references/mcp.md +6 -7
- package/skills/arc/references/production.md +24 -25
- package/skills/arc-code-review/references/anti-patterns.md +3 -3
- package/dist/index-CZY4EGga.d.mts +0 -107
- package/dist/module-_-M7C5ES.mjs +0 -29
|
@@ -1,11 +1,158 @@
|
|
|
1
|
-
|
|
1
|
+
/*! @classytic/arc v2.31.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-CUsjNX9R.mjs";
|
|
2
3
|
import { arcLog, configureArcLogger, createPinoWriter } from "./logger/index.mjs";
|
|
3
|
-
import { n as PUBLIC_SCOPE } from "./types-
|
|
4
|
-
import { t as createTenantKeyGenerator } from "./rateLimitKey-
|
|
5
|
-
import { i as createRequestIdGenerator, r as mergeHealthChecks } from "./health-
|
|
6
|
-
import { t as parseJsonBody } from "./jsonBody-
|
|
4
|
+
import { n as PUBLIC_SCOPE } from "./types-Cys0ko9Z.mjs";
|
|
5
|
+
import { t as createTenantKeyGenerator } from "./rateLimitKey-Dn5JNmkj.mjs";
|
|
6
|
+
import { i as createRequestIdGenerator, r as mergeHealthChecks } from "./health-DjZYjM7H.mjs";
|
|
7
|
+
import { t as parseJsonBody } from "./jsonBody-BYbLDi5D.mjs";
|
|
8
|
+
import { r as wrapWithBoundary } from "./subscribe-helpers-C0Tb2aUp.mjs";
|
|
7
9
|
import Fastify from "fastify";
|
|
8
10
|
import qs from "qs";
|
|
11
|
+
//#region src/factory/security/pluginLoader.ts
|
|
12
|
+
const PLUGIN_REGISTRY = {
|
|
13
|
+
cors: {
|
|
14
|
+
package: "@fastify/cors",
|
|
15
|
+
loader: () => import("@fastify/cors").then((m) => m.default)
|
|
16
|
+
},
|
|
17
|
+
helmet: {
|
|
18
|
+
package: "@fastify/helmet",
|
|
19
|
+
loader: () => import("@fastify/helmet").then((m) => m.default)
|
|
20
|
+
},
|
|
21
|
+
rateLimit: {
|
|
22
|
+
package: "@fastify/rate-limit",
|
|
23
|
+
loader: () => import("@fastify/rate-limit").then((m) => m.default)
|
|
24
|
+
},
|
|
25
|
+
underPressure: {
|
|
26
|
+
package: "@fastify/under-pressure",
|
|
27
|
+
loader: () => import("@fastify/under-pressure").then((m) => m.default)
|
|
28
|
+
},
|
|
29
|
+
sensible: {
|
|
30
|
+
package: "@fastify/sensible",
|
|
31
|
+
loader: () => import("@fastify/sensible").then((m) => m.default)
|
|
32
|
+
},
|
|
33
|
+
multipart: {
|
|
34
|
+
package: "@fastify/multipart",
|
|
35
|
+
loader: () => import("@fastify/multipart").then((m) => m.default),
|
|
36
|
+
optional: true
|
|
37
|
+
},
|
|
38
|
+
rawBody: {
|
|
39
|
+
package: "fastify-raw-body",
|
|
40
|
+
loader: () => import("fastify-raw-body").then((m) => m.default),
|
|
41
|
+
optional: true
|
|
42
|
+
},
|
|
43
|
+
static: {
|
|
44
|
+
package: "@fastify/static",
|
|
45
|
+
loader: () => import("@fastify/static").then((m) => m.default),
|
|
46
|
+
optional: true
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
async function loadPlugin(name, logger) {
|
|
50
|
+
const entry = PLUGIN_REGISTRY[name];
|
|
51
|
+
if (!entry) throw new Error(`Unknown plugin: ${name}`);
|
|
52
|
+
try {
|
|
53
|
+
return await entry.loader();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const err = error;
|
|
56
|
+
const isModuleNotFound = err.message.includes("Cannot find module") || err.message.includes("Cannot find package") || err.message.includes("MODULE_NOT_FOUND") || err.message.includes("Could not resolve");
|
|
57
|
+
if (isModuleNotFound && entry.optional) {
|
|
58
|
+
logger?.warn(`Optional plugin '${name}' skipped (${entry.package} not installed)`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (isModuleNotFound) throw new Error(`Plugin '${name}' requires package '${entry.package}' which is not installed.\nInstall it with: npm install ${entry.package}\nOr disable this plugin by setting ${name}: false in createApp options.`);
|
|
62
|
+
throw new Error(`Failed to load plugin '${name}': ${err.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/factory/assets.ts
|
|
67
|
+
/**
|
|
68
|
+
* Cache-Control per preset.
|
|
69
|
+
*
|
|
70
|
+
* - `immutable` — content-addressed output ONLY (a hashed filename that changes
|
|
71
|
+
* when the bytes change). One year + `immutable` so a revalidation is never
|
|
72
|
+
* even attempted.
|
|
73
|
+
* - `revalidate` — the default. The client may cache but MUST check freshness;
|
|
74
|
+
* `@fastify/static`'s `ETag`/`Last-Modified` then makes the common case a 304
|
|
75
|
+
* with no body, which is nearly as cheap as a cache hit and always correct.
|
|
76
|
+
* - `none` — never store. For a path that is authorization-dependent, where a
|
|
77
|
+
* shared cache holding the bytes would leak them to the next caller.
|
|
78
|
+
*/
|
|
79
|
+
const CACHE_CONTROL = {
|
|
80
|
+
immutable: "public, max-age=31536000, immutable",
|
|
81
|
+
revalidate: "public, max-age=0, must-revalidate",
|
|
82
|
+
none: "no-store"
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Resolve one asset root's declaration into the header policy arc applies per
|
|
86
|
+
* file. Pure — no Fastify, no filesystem — so the policy is unit-testable
|
|
87
|
+
* without booting an app.
|
|
88
|
+
*/
|
|
89
|
+
function resolveAssetPolicy(root) {
|
|
90
|
+
const cache = root.cache ?? "revalidate";
|
|
91
|
+
const crossOrigin = root.crossOrigin ?? "same-site";
|
|
92
|
+
const disposition = root.disposition ?? "attachment";
|
|
93
|
+
return {
|
|
94
|
+
cacheControl: CACHE_CONTROL[cache],
|
|
95
|
+
crossOriginResourcePolicy: crossOrigin,
|
|
96
|
+
disposition,
|
|
97
|
+
varyOrigin: crossOrigin !== "same-origin"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Quote a filename for `Content-Disposition` per RFC 6266 / RFC 5987. */
|
|
101
|
+
function contentDisposition(type, filePath) {
|
|
102
|
+
const name = filePath.split(/[\\/]/).pop() ?? "download";
|
|
103
|
+
return `${type}; filename="${name.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "\\$&")}"; filename*=UTF-8''${encodeURIComponent(name)}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Register every declared asset root.
|
|
107
|
+
*
|
|
108
|
+
* Each root becomes its OWN `@fastify/static` registration. That is deliberate:
|
|
109
|
+
* `@fastify/static` scopes `prefix`, `root`, and the send options per
|
|
110
|
+
* registration, so two roots with different cache or cross-origin policy stay
|
|
111
|
+
* genuinely independent instead of sharing one mutable options bag.
|
|
112
|
+
*
|
|
113
|
+
* Registered AFTER the security plugins so the per-prefix CORP override lands on
|
|
114
|
+
* top of helmet's app-wide default — helmet sets its headers in an `onRequest`
|
|
115
|
+
* hook, while `setHeaders` runs at send time, so the asset policy wins.
|
|
116
|
+
*/
|
|
117
|
+
async function registerAssetRoots(fastify, roots) {
|
|
118
|
+
if (roots.length === 0) return;
|
|
119
|
+
const staticPlugin = await loadPlugin("static", fastify.log);
|
|
120
|
+
if (!staticPlugin) throw new Error("[arc] `assets` was configured but '@fastify/static' is not installed. Install it with: npm install @fastify/static — arc delegates ranges, ETags, immutable caching and pre-compressed variants to it rather than reimplementing them.");
|
|
121
|
+
const seen = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const root of roots) {
|
|
123
|
+
const prior = seen.get(root.prefix);
|
|
124
|
+
if (prior !== void 0) throw new Error(`[arc] duplicate asset prefix "${root.prefix}" — declared for roots "${prior}" and "${root.root}". Asset prefixes must be unique; nest them (e.g. "/files/public" and "/files/private") instead of sharing one.`);
|
|
125
|
+
seen.set(root.prefix, String(root.root));
|
|
126
|
+
}
|
|
127
|
+
for (const root of roots) {
|
|
128
|
+
const policy = resolveAssetPolicy(root);
|
|
129
|
+
await fastify.register(staticPlugin, {
|
|
130
|
+
root: root.root,
|
|
131
|
+
prefix: root.prefix,
|
|
132
|
+
decorateReply: root.decorateReply ?? false,
|
|
133
|
+
list: false,
|
|
134
|
+
index: root.index ?? false,
|
|
135
|
+
dotfiles: "deny",
|
|
136
|
+
serveDotFiles: false,
|
|
137
|
+
cacheControl: false,
|
|
138
|
+
etag: root.etag ?? true,
|
|
139
|
+
lastModified: root.lastModified ?? true,
|
|
140
|
+
acceptRanges: root.acceptRanges ?? true,
|
|
141
|
+
...root.preCompressed !== void 0 ? { preCompressed: root.preCompressed } : {},
|
|
142
|
+
...root.allowedPath ? { allowedPath: root.allowedPath } : {},
|
|
143
|
+
setHeaders: (reply, filePath) => {
|
|
144
|
+
reply.header("Cache-Control", policy.cacheControl);
|
|
145
|
+
reply.header("Cross-Origin-Resource-Policy", policy.crossOriginResourcePolicy);
|
|
146
|
+
reply.header("Content-Disposition", contentDisposition(policy.disposition, filePath));
|
|
147
|
+
reply.header("X-Content-Type-Options", "nosniff");
|
|
148
|
+
if (policy.varyOrigin) reply.header("Vary", "Origin, Accept-Encoding");
|
|
149
|
+
root.setHeaders?.(reply, filePath);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
fastify.log.debug(`[arc] asset root "${root.prefix}" → ${String(root.root)} (cache: ${root.cache ?? "revalidate"}, cross-origin: ${policy.crossOriginResourcePolicy})`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
9
156
|
//#region src/factory/module/resolve.ts
|
|
10
157
|
/**
|
|
11
158
|
* Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
|
|
@@ -15,6 +162,7 @@ import qs from "qs";
|
|
|
15
162
|
async function resolveModule(input) {
|
|
16
163
|
const resolved = typeof input === "function" ? await input() : await input;
|
|
17
164
|
if (!resolved || typeof resolved !== "object" || typeof resolved.name !== "string") throw new Error("[arc] a `modules` entry resolved to something that is not an ArcModule (expected an object with a string `name`). Check dynamic-import thunks return `createXModule(deps)` (or the module object), not the namespace.");
|
|
165
|
+
if (resolved.name.trim().length === 0) throw new Error("[arc] a `modules` entry has an empty (or whitespace-only) `name`. A module's name is its graph key, its `arc.modules` export key, its `dependsOn` target, and its label in every boot diagnostic — give it a stable, non-empty identifier.");
|
|
18
166
|
return resolved;
|
|
19
167
|
}
|
|
20
168
|
/** Resolve a `ModuleContribution` to a concrete array (array passthrough or factory call). */
|
|
@@ -22,6 +170,26 @@ async function resolveContribution(contribution, fastify) {
|
|
|
22
170
|
if (!contribution) return [];
|
|
23
171
|
return typeof contribution === "function" ? await contribution(fastify) : contribution;
|
|
24
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* `resolveContribution` with module attribution — the form every arm collector
|
|
175
|
+
* should use.
|
|
176
|
+
*
|
|
177
|
+
* A contribution factory closes over booted engines, so it throws for exactly
|
|
178
|
+
* the reasons a bootstrap does ("connection unavailable", "engine not ready").
|
|
179
|
+
* Bare, that message names neither the module nor the arm, which in a graph of
|
|
180
|
+
* twenty modules turns a one-line fix into a bisect. Every other module
|
|
181
|
+
* boundary (`plugins`, `bootstrap`, `resources`, `afterResources`) already
|
|
182
|
+
* wraps with the owning module's name; this keeps the arms consistent with
|
|
183
|
+
* them. The original error is preserved via `{ cause }`.
|
|
184
|
+
*/
|
|
185
|
+
async function resolveModuleArm(module, arm, contribution, fastify) {
|
|
186
|
+
try {
|
|
187
|
+
return await resolveContribution(contribution, fastify);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
190
|
+
throw new Error(`[arc] module "${module.name}" ${arm} factory threw: ${msg}`, { cause: err });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
25
193
|
//#endregion
|
|
26
194
|
//#region src/factory/module/contributions.ts
|
|
27
195
|
/**
|
|
@@ -48,7 +216,7 @@ function collectModuleHealthChecks(modules) {
|
|
|
48
216
|
async function collectModuleWorkflows(fastify, modules) {
|
|
49
217
|
const out = [];
|
|
50
218
|
for (const m of modules) {
|
|
51
|
-
const wfs = await
|
|
219
|
+
const wfs = await resolveModuleArm(m, "workflows", m.workflows, fastify);
|
|
52
220
|
out.push(...wfs);
|
|
53
221
|
}
|
|
54
222
|
return out;
|
|
@@ -63,7 +231,7 @@ async function collectModuleScheduledJobs(fastify, modules) {
|
|
|
63
231
|
const owner = /* @__PURE__ */ new Map();
|
|
64
232
|
const out = [];
|
|
65
233
|
for (const m of modules) {
|
|
66
|
-
const jobs = await
|
|
234
|
+
const jobs = await resolveModuleArm(m, "scheduledJobs", m.scheduledJobs, fastify);
|
|
67
235
|
for (const job of jobs) {
|
|
68
236
|
const prior = owner.get(job.name);
|
|
69
237
|
if (prior !== void 0) throw new Error(`[arc] duplicate scheduled-job name "${job.name}" — declared by module "${prior}" and module "${m.name}". Scheduled-job names must be unique across the module graph.`);
|
|
@@ -90,7 +258,7 @@ async function subscribeModuleEventHandlers(fastify, modules) {
|
|
|
90
258
|
const owner = /* @__PURE__ */ new Map();
|
|
91
259
|
try {
|
|
92
260
|
for (const m of modules) {
|
|
93
|
-
const handlers = await
|
|
261
|
+
const handlers = await resolveModuleArm(m, "eventHandlers", m.eventHandlers, fastify);
|
|
94
262
|
if (handlers.length === 0) continue;
|
|
95
263
|
const bus = fastify.events;
|
|
96
264
|
if (!bus) throw new Error(`[arc] module "${m.name}" declares eventHandlers but the event subsystem is unavailable (fastify.events). Enable arcPlugins.events.`);
|
|
@@ -100,8 +268,9 @@ async function subscribeModuleEventHandlers(fastify, modules) {
|
|
|
100
268
|
if (prior !== void 0) throw new Error(`[arc] duplicate event-handler name "${def.name}" — declared by module "${prior}" and module "${m.name}". Named event handlers must be unique across the module graph; prefix names with the owning module.`);
|
|
101
269
|
owner.set(def.name, m.name);
|
|
102
270
|
}
|
|
271
|
+
const handler = applyBoundary(def, m.name, fastify);
|
|
103
272
|
const patterns = Array.isArray(def.event) ? def.event : [def.event];
|
|
104
|
-
for (const pattern of patterns) unsubscribes.push(await bus.subscribe(pattern,
|
|
273
|
+
for (const pattern of patterns) unsubscribes.push(await bus.subscribe(pattern, handler));
|
|
105
274
|
}
|
|
106
275
|
}
|
|
107
276
|
return unsubscribes;
|
|
@@ -111,6 +280,25 @@ async function subscribeModuleEventHandlers(fastify, modules) {
|
|
|
111
280
|
throw err;
|
|
112
281
|
}
|
|
113
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Apply the declaration's opt-in error boundary. Without `boundary`, the raw
|
|
285
|
+
* handler is subscribed and a throw reaches the transport — which is what makes
|
|
286
|
+
* the durable path work (unacked → redelivered → DLQ). With it, failures are
|
|
287
|
+
* logged through `fastify.log` and swallowed, for handlers whose retry would
|
|
288
|
+
* only delay the next event's resync.
|
|
289
|
+
*
|
|
290
|
+
* The boundary label falls back to `<module>.<pattern>` so an unnamed handler
|
|
291
|
+
* still produces an attributable log line instead of "anonymous".
|
|
292
|
+
*/
|
|
293
|
+
function applyBoundary(def, moduleName, fastify) {
|
|
294
|
+
if (!def.boundary) return def.handler;
|
|
295
|
+
const onError = typeof def.boundary === "object" ? def.boundary.onError : void 0;
|
|
296
|
+
return wrapWithBoundary(def.handler, {
|
|
297
|
+
logger: fastify.log,
|
|
298
|
+
name: def.name ?? `${moduleName}.${String(def.event)}`,
|
|
299
|
+
...onError ? { onError } : {}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
114
302
|
/** Best-effort reverse-order teardown; every unsubscribe is attempted. */
|
|
115
303
|
async function unsubscribeModuleEventHandlers(unsubscribes) {
|
|
116
304
|
const errors = [];
|
|
@@ -214,6 +402,98 @@ function describeModuleCycle(remaining, byName) {
|
|
|
214
402
|
return `[arc] module dependency cycle: ${cycle ? cycle.join(" → ") : [...remaining].join(", ")}. Modules cannot dependsOn each other circularly — break it with a shared module both point at, or wire the softer direction through an event/port instead of a hard dependsOn.`;
|
|
215
403
|
}
|
|
216
404
|
//#endregion
|
|
405
|
+
//#region src/factory/module/state.ts
|
|
406
|
+
/**
|
|
407
|
+
* The honest read of the state map. `fastify.arc` is optional (see
|
|
408
|
+
* fastify-augmentation.ts) and `moduleStates` is populated by
|
|
409
|
+
* registerResources — both absent means "no module graph composed here"
|
|
410
|
+
* (or a pre-2.31 embedding), so accessors degrade to undefined/false
|
|
411
|
+
* rather than throwing.
|
|
412
|
+
*/
|
|
413
|
+
function readStates(fastify) {
|
|
414
|
+
return fastify.arc?.moduleStates;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Record every composed module as `resolved`. Runs at the top of
|
|
418
|
+
* registerResources, BEFORE any side-effecting phase — so from the first
|
|
419
|
+
* lifecycle callback onward, `hasModule()` answers for the whole graph.
|
|
420
|
+
* Null-proto map for the same reason as `arc.modules`: a module named
|
|
421
|
+
* "__proto__" must be an own key, not a prototype write.
|
|
422
|
+
*/
|
|
423
|
+
function initModuleStates(fastify, names) {
|
|
424
|
+
const arc = fastify.arc;
|
|
425
|
+
if (!arc) return;
|
|
426
|
+
const states = Object.create(null);
|
|
427
|
+
for (const name of names) states[name] = "resolved";
|
|
428
|
+
arc.moduleStates = states;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Internal transition writer — no-op when no state map exists (see above),
|
|
432
|
+
* and no-op OUT of `failed`, which is terminal: rollback closes a module whose
|
|
433
|
+
* bootstrap threw, and that cleanup succeeding must not erase the fact that
|
|
434
|
+
* initialization failed (see the file header).
|
|
435
|
+
*/
|
|
436
|
+
function setModuleState(fastify, name, state) {
|
|
437
|
+
const states = readStates(fastify);
|
|
438
|
+
if (!states) return;
|
|
439
|
+
if (states[name] === "failed") return;
|
|
440
|
+
states[name] = state;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Is `name` in the composed module graph? TRUE for resource-only modules and
|
|
444
|
+
* modules that exported nothing — the question `hasModuleExports()` cannot
|
|
445
|
+
* answer. FALSE before registerResources runs (no graph yet) and for
|
|
446
|
+
* uncomposed names.
|
|
447
|
+
*/
|
|
448
|
+
function hasModule(fastify, name) {
|
|
449
|
+
const states = readStates(fastify);
|
|
450
|
+
return states !== void 0 && Object.hasOwn(states, name);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* The module's current lifecycle state, or `undefined` when the module is not
|
|
454
|
+
* composed (or no graph has been composed on this instance). See the state
|
|
455
|
+
* machine in the file header.
|
|
456
|
+
*/
|
|
457
|
+
function getModuleState(fastify, name) {
|
|
458
|
+
const states = readStates(fastify);
|
|
459
|
+
return states !== void 0 && Object.hasOwn(states, name) ? states[name] : void 0;
|
|
460
|
+
}
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/factory/module/teardown.ts
|
|
463
|
+
function createModuleTeardown(fastify, modules) {
|
|
464
|
+
const initializedNames = /* @__PURE__ */ new Set();
|
|
465
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
466
|
+
async function closeInitialized() {
|
|
467
|
+
const errors = [];
|
|
468
|
+
for (let index = modules.length - 1; index >= 0; index--) {
|
|
469
|
+
const m = modules[index];
|
|
470
|
+
if (!m || !initializedNames.has(m.name) || consumed.has(m.name)) continue;
|
|
471
|
+
consumed.add(m.name);
|
|
472
|
+
if (!m.onClose) {
|
|
473
|
+
setModuleState(fastify, m.name, "closed");
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
setModuleState(fastify, m.name, "closing");
|
|
477
|
+
try {
|
|
478
|
+
await m.onClose(fastify);
|
|
479
|
+
setModuleState(fastify, m.name, "closed");
|
|
480
|
+
} catch (err) {
|
|
481
|
+
setModuleState(fastify, m.name, "failed");
|
|
482
|
+
errors.push(new Error(`[arc] module "${m.name}" onClose() threw during teardown: ${err instanceof Error ? err.message : String(err)}`, { cause: err }));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return errors;
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
markInitialized(module) {
|
|
489
|
+
initializedNames.add(module.name);
|
|
490
|
+
},
|
|
491
|
+
rollback: closeInitialized,
|
|
492
|
+
closeAll: closeInitialized,
|
|
493
|
+
hasComposedModules: () => modules.length > 0
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
//#endregion
|
|
217
497
|
//#region src/factory/presets.ts
|
|
218
498
|
/**
|
|
219
499
|
* Production preset - strict security, performance optimized
|
|
@@ -439,7 +719,7 @@ async function registerArcCore(fastify, config, trackPlugin) {
|
|
|
439
719
|
await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
|
|
440
720
|
trackPlugin("arc-core");
|
|
441
721
|
if (config.arcPlugins?.events !== false) {
|
|
442
|
-
const { default: eventPlugin } = await import("./eventPlugin-
|
|
722
|
+
const { default: eventPlugin } = await import("./eventPlugin-CkffEihm.mjs").then((n) => n.n);
|
|
443
723
|
const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
|
|
444
724
|
await fastify.register(eventPlugin, {
|
|
445
725
|
...eventOpts,
|
|
@@ -487,15 +767,15 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
|
|
|
487
767
|
trackPlugin("arc-graceful-shutdown");
|
|
488
768
|
}
|
|
489
769
|
if (config.arcPlugins?.caching) {
|
|
490
|
-
const { default: cachingPlugin } = await import("./caching-
|
|
770
|
+
const { default: cachingPlugin } = await import("./caching-BFHA6Scf.mjs").then((n) => n.r);
|
|
491
771
|
const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
|
|
492
772
|
await fastify.register(cachingPlugin, opts);
|
|
493
773
|
trackPlugin("arc-caching", opts);
|
|
494
774
|
}
|
|
495
775
|
if (config.arcPlugins?.queryCache) {
|
|
496
|
-
const { queryCachePlugin } = await import("./queryCachePlugin-
|
|
776
|
+
const { queryCachePlugin } = await import("./queryCachePlugin-BZr8v46E.mjs").then((n) => n.n);
|
|
497
777
|
const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
|
|
498
|
-
const store = config.stores?.queryCache ?? new (await (import("./memory-
|
|
778
|
+
const store = config.stores?.queryCache ?? new (await (import("./memory-DR6wpa9F.mjs").then((n) => n.n))).MemoryCacheStore();
|
|
499
779
|
await fastify.register(queryCachePlugin, {
|
|
500
780
|
store,
|
|
501
781
|
...opts
|
|
@@ -504,26 +784,26 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
|
|
|
504
784
|
}
|
|
505
785
|
if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
|
|
506
786
|
else {
|
|
507
|
-
const { default: ssePlugin } = await import("./sse-
|
|
787
|
+
const { default: ssePlugin } = await import("./sse-CUTo9y0X.mjs").then((n) => n.r);
|
|
508
788
|
const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
|
|
509
789
|
await fastify.register(ssePlugin, opts);
|
|
510
790
|
trackPlugin("arc-sse", opts);
|
|
511
791
|
}
|
|
512
792
|
if (config.arcPlugins?.realtime) if (config.arcPlugins?.events === false) fastify.log.warn("Realtime plugin requires events plugin (arcPlugins.events). Realtime disabled.");
|
|
513
793
|
else {
|
|
514
|
-
const { default: realtimePlugin } = await import("./realtime-
|
|
794
|
+
const { default: realtimePlugin } = await import("./realtime-C5NoxWFY.mjs").then((n) => n.r);
|
|
515
795
|
const opts = config.arcPlugins.realtime === true ? {} : config.arcPlugins.realtime;
|
|
516
796
|
await fastify.register(realtimePlugin, opts);
|
|
517
797
|
trackPlugin("arc-realtime", opts);
|
|
518
798
|
}
|
|
519
799
|
if (config.arcPlugins?.metrics) {
|
|
520
|
-
const { default: metricsPlugin } = await import("./metrics-
|
|
800
|
+
const { default: metricsPlugin } = await import("./metrics-CUdw_wLB.mjs").then((n) => n.r);
|
|
521
801
|
const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
|
|
522
802
|
await fastify.register(metricsPlugin, opts);
|
|
523
803
|
trackPlugin("arc-metrics", opts);
|
|
524
804
|
}
|
|
525
805
|
if (config.arcPlugins?.versioning) {
|
|
526
|
-
const { default: versioningPlugin } = await import("./versioning-
|
|
806
|
+
const { default: versioningPlugin } = await import("./versioning-CHDTor3P.mjs").then((n) => n.r);
|
|
527
807
|
await fastify.register(versioningPlugin, config.arcPlugins.versioning);
|
|
528
808
|
trackPlugin("arc-versioning", config.arcPlugins.versioning);
|
|
529
809
|
}
|
|
@@ -592,7 +872,7 @@ async function registerAuth(fastify, config, trackPlugin) {
|
|
|
592
872
|
*/
|
|
593
873
|
async function registerElevation(fastify, config, trackPlugin) {
|
|
594
874
|
if (!config.elevation) return;
|
|
595
|
-
const { elevationPlugin } = await import("./elevation-
|
|
875
|
+
const { elevationPlugin } = await import("./elevation-D2l1mh4g.mjs").then((n) => n.r);
|
|
596
876
|
await fastify.register(elevationPlugin, config.elevation);
|
|
597
877
|
trackPlugin("arc-elevation", config.elevation);
|
|
598
878
|
fastify.log.debug("Elevation plugin enabled");
|
|
@@ -607,7 +887,7 @@ async function registerElevation(fastify, config, trackPlugin) {
|
|
|
607
887
|
*/
|
|
608
888
|
async function registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers) {
|
|
609
889
|
if (config.errorHandler === false) return;
|
|
610
|
-
const { errorHandlerPlugin } = await import("./errorHandler-
|
|
890
|
+
const { errorHandlerPlugin } = await import("./errorHandler-C5NeD6bT.mjs").then((n) => n.r);
|
|
611
891
|
let errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
|
|
612
892
|
if (moduleErrorMappers && moduleErrorMappers.length > 0) errorOpts = {
|
|
613
893
|
...errorOpts,
|
|
@@ -680,14 +960,29 @@ async function registerOne(parent, resource, mountRoutes) {
|
|
|
680
960
|
* 5. resources factory (if any) — resolved AFTER bootstrap, so engine-backed
|
|
681
961
|
* adapters can `await ensureEngine()` and pass
|
|
682
962
|
* live models/repos into `defineResource(...)`
|
|
683
|
-
*
|
|
963
|
+
* 5a. module resources — RESOLVED after the app factory, but PREPENDED
|
|
684
964
|
* so they REGISTER before app resources (two
|
|
685
|
-
* orderings, both intentional — see §
|
|
965
|
+
* orderings, both intentional — see §5a)
|
|
966
|
+
* 5b. owns validation + filter — verify each claim is supplied by its claimant,
|
|
967
|
+
* THEN drop the app forks it supersedes
|
|
686
968
|
* 6. module.afterResources → afterResources() — post-registration wiring
|
|
687
969
|
* 7. onReady — lifecycle hook
|
|
688
970
|
* 7b. onClose — module.onClose (reverse composition order)
|
|
689
971
|
* THEN app onClose, in ONE hook so module
|
|
690
972
|
* teardown runs before shared infra closes.
|
|
973
|
+
*
|
|
974
|
+
* Boot is TRANSACTIONAL for module-held resources: phases 2–6 run inside one
|
|
975
|
+
* try/catch. On any failure, arc unsubscribes already-active module event
|
|
976
|
+
* handlers, then closes every module that ENTERED an init phase — reverse
|
|
977
|
+
* composition order, best-effort (see module/teardown.ts) — and rethrows the
|
|
978
|
+
* ORIGINAL boot error. Without this, a failure after module A's bootstrap but
|
|
979
|
+
* before the §7b hook registration leaked A's clients/timers/engines with no
|
|
980
|
+
* teardown path.
|
|
981
|
+
*
|
|
982
|
+
* "Entered", not "completed": a module is eligible for teardown from the
|
|
983
|
+
* moment it enters `plugins()`/`bootstrap()`, so the callback that allocates a
|
|
984
|
+
* client and THEN throws — the commonest partial-init shape — gets its closer
|
|
985
|
+
* called too.
|
|
691
986
|
*/
|
|
692
987
|
async function registerResources(fastify, config, preResolvedModules) {
|
|
693
988
|
if (config.preset === "production") {
|
|
@@ -701,160 +996,190 @@ async function registerResources(fastify, config, preResolvedModules) {
|
|
|
701
996
|
};
|
|
702
997
|
}
|
|
703
998
|
const modules = preResolvedModules ?? orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
await m.plugins(fastify);
|
|
712
|
-
} catch (err) {
|
|
713
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
714
|
-
throw new Error(`[arc] module "${m.name}" plugins() threw: ${msg}`, { cause: err });
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
for (const m of modules) {
|
|
718
|
-
if (!m.bootstrap) continue;
|
|
719
|
-
let exported;
|
|
720
|
-
try {
|
|
721
|
-
exported = await m.bootstrap(fastify);
|
|
722
|
-
} catch (err) {
|
|
723
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
724
|
-
throw new Error(`[arc] module "${m.name}" bootstrap threw: ${msg}`, { cause: err });
|
|
999
|
+
initModuleStates(fastify, modules.map((m) => m.name));
|
|
1000
|
+
const teardown = createModuleTeardown(fastify, modules);
|
|
1001
|
+
let eventUnsubscribes = [];
|
|
1002
|
+
try {
|
|
1003
|
+
if (config.plugins) {
|
|
1004
|
+
await config.plugins(fastify);
|
|
1005
|
+
fastify.log.debug("Custom plugins registered");
|
|
725
1006
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1007
|
+
for (const m of modules) {
|
|
1008
|
+
if (!m.plugins) continue;
|
|
1009
|
+
teardown.markInitialized(m);
|
|
1010
|
+
try {
|
|
1011
|
+
await m.plugins(fastify);
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
setModuleState(fastify, m.name, "failed");
|
|
1014
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1015
|
+
throw new Error(`[arc] module "${m.name}" plugins() threw: ${msg}`, { cause: err });
|
|
1016
|
+
}
|
|
731
1017
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1018
|
+
for (const m of modules) {
|
|
1019
|
+
if (!m.bootstrap) {
|
|
1020
|
+
setModuleState(fastify, m.name, "ready");
|
|
1021
|
+
teardown.markInitialized(m);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
let exported;
|
|
1025
|
+
teardown.markInitialized(m);
|
|
1026
|
+
setModuleState(fastify, m.name, "bootstrapping");
|
|
1027
|
+
try {
|
|
1028
|
+
exported = await m.bootstrap(fastify);
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
setModuleState(fastify, m.name, "failed");
|
|
1031
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1032
|
+
throw new Error(`[arc] module "${m.name}" bootstrap threw: ${msg}`, { cause: err });
|
|
1033
|
+
}
|
|
1034
|
+
setModuleState(fastify, m.name, "ready");
|
|
1035
|
+
if (exported !== void 0) {
|
|
1036
|
+
const arc = fastify.arc;
|
|
1037
|
+
if (!arc) throw new Error(`[arc] module "${m.name}" returned a public export but fastify.arc is not decorated — arcCorePlugin must register first.`);
|
|
1038
|
+
if (!arc.modules) arc.modules = Object.create(null);
|
|
1039
|
+
arc.modules[m.name] = exported;
|
|
1040
|
+
}
|
|
746
1041
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
} else resolvedResources = config.resources;
|
|
752
|
-
let discoveryRawDir;
|
|
753
|
-
let discoveryPath;
|
|
754
|
-
let discoveryYieldedZero = false;
|
|
755
|
-
if (resolvedResources === void 0 && config.resourceDir) {
|
|
756
|
-
const { loadResources } = await import("./loadResources-DpU8pfjV.mjs").then((n) => n.n);
|
|
757
|
-
const { resolve, dirname } = await import("node:path");
|
|
758
|
-
const { fileURLToPath } = await import("node:url");
|
|
759
|
-
const rawDir = config.resourceDir;
|
|
760
|
-
const dir = rawDir.startsWith("file://") ? dirname(fileURLToPath(rawDir)) : resolve(rawDir);
|
|
761
|
-
discoveryRawDir = rawDir;
|
|
762
|
-
discoveryPath = dir;
|
|
763
|
-
const discovered = await loadResources(dir, { logger: fastify.log });
|
|
764
|
-
if (discovered.length === 0) {
|
|
765
|
-
if (config.strictResourceDir) throw new Error(`[arc] loadResources: resourceDir "${rawDir}" resolved to "${dir}" but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/). Use \`strictResourceDir: true\` to fail boot.`);
|
|
766
|
-
discoveryYieldedZero = true;
|
|
1042
|
+
if (modules.length) fastify.log.debug(`${modules.length} module(s) composed (in dependency order): ${modules.map((m) => m.name).join(" → ")}`);
|
|
1043
|
+
if (config.bootstrap?.length) {
|
|
1044
|
+
for (const init of config.bootstrap) await init(fastify);
|
|
1045
|
+
fastify.log.debug(`${config.bootstrap.length} bootstrap function(s) executed`);
|
|
767
1046
|
}
|
|
768
|
-
resolvedResources
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1047
|
+
let resolvedResources;
|
|
1048
|
+
if (isResourcesFactory(config.resources)) {
|
|
1049
|
+
try {
|
|
1050
|
+
resolvedResources = await config.resources(fastify);
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1053
|
+
fastify.log.error(`Resources factory threw during boot: ${msg}`);
|
|
1054
|
+
throw new Error(`[arc] resources factory threw: ${msg}. Check engine bootstrap order (did you forget a bootstrap step?) and that \`defineResource(...)\` calls inside the factory receive fully-booted adapters / repositories.`, { cause: err });
|
|
1055
|
+
}
|
|
1056
|
+
config = {
|
|
1057
|
+
...config,
|
|
1058
|
+
resources: resolvedResources
|
|
1059
|
+
};
|
|
1060
|
+
} else resolvedResources = config.resources;
|
|
1061
|
+
let discoveryRawDir;
|
|
1062
|
+
let discoveryPath;
|
|
1063
|
+
let discoveryYieldedZero = false;
|
|
1064
|
+
if (resolvedResources === void 0 && config.resourceDir) {
|
|
1065
|
+
const { loadResources } = await import("./loadResources-yawi9tAE.mjs").then((n) => n.n);
|
|
1066
|
+
const { resolve, dirname } = await import("node:path");
|
|
1067
|
+
const { fileURLToPath } = await import("node:url");
|
|
1068
|
+
const rawDir = config.resourceDir;
|
|
1069
|
+
const dir = rawDir.startsWith("file://") ? dirname(fileURLToPath(rawDir)) : resolve(rawDir);
|
|
1070
|
+
discoveryRawDir = rawDir;
|
|
1071
|
+
discoveryPath = dir;
|
|
1072
|
+
const discovered = await loadResources(dir, { logger: fastify.log });
|
|
1073
|
+
if (discovered.length === 0) {
|
|
1074
|
+
if (config.strictResourceDir) throw new Error(`[arc] loadResources: resourceDir "${rawDir}" resolved to "${dir}" but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/). Use \`strictResourceDir: true\` to fail boot.`);
|
|
1075
|
+
discoveryYieldedZero = true;
|
|
1076
|
+
}
|
|
1077
|
+
resolvedResources = discovered;
|
|
778
1078
|
}
|
|
779
|
-
|
|
780
|
-
if (modules.length) {
|
|
1079
|
+
const moduleResourceNames = /* @__PURE__ */ new Map();
|
|
781
1080
|
const moduleResources = [];
|
|
782
1081
|
for (const m of modules) {
|
|
783
1082
|
if (!m.resources) continue;
|
|
1083
|
+
let rs;
|
|
784
1084
|
try {
|
|
785
|
-
|
|
786
|
-
moduleResources.push(...rs);
|
|
1085
|
+
rs = typeof m.resources === "function" ? await m.resources(fastify) : m.resources;
|
|
787
1086
|
} catch (err) {
|
|
788
1087
|
const msg = err instanceof Error ? err.message : String(err);
|
|
789
1088
|
throw new Error(`[arc] module "${m.name}" resources factory threw: ${msg}. Check the module's bootstrap ran and its adapters received booted engines.`, { cause: err });
|
|
790
1089
|
}
|
|
1090
|
+
const names = /* @__PURE__ */ new Set();
|
|
1091
|
+
for (const r of rs) if (r.name != null) names.add(r.name);
|
|
1092
|
+
moduleResourceNames.set(m.name, names);
|
|
1093
|
+
moduleResources.push(...rs);
|
|
1094
|
+
}
|
|
1095
|
+
for (const m of modules) {
|
|
1096
|
+
if (!m.owns?.length) continue;
|
|
1097
|
+
const supplied = moduleResourceNames.get(m.name);
|
|
1098
|
+
const missing = m.owns.filter((name) => !supplied?.has(name));
|
|
1099
|
+
if (missing.length > 0) {
|
|
1100
|
+
const provided = supplied?.size ? [...supplied].join(", ") : "(none)";
|
|
1101
|
+
throw new Error(`[arc] module "${m.name}" declares owns: [${missing.map((n) => `"${n}"`).join(", ")}] but its own \`resources\` do not supply ${missing.length === 1 ? "that name" : "those names"}. \`owns\` DROPS the app-level resource of that name, so an unmet claim removes the route entirely and nothing serves it. Provide it in this module's \`resources\`, or drop the \`owns\` entry.
|
|
1102
|
+
Resources supplied by "${m.name}": ${provided}.`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (modules.length && resolvedResources && resolvedResources.length > 0) {
|
|
1106
|
+
const owned = /* @__PURE__ */ new Set();
|
|
1107
|
+
for (const m of modules) for (const name of m.owns ?? []) owned.add(name);
|
|
1108
|
+
if (owned.size > 0) {
|
|
1109
|
+
const before = resolvedResources.length;
|
|
1110
|
+
resolvedResources = resolvedResources.filter((r) => !(r.name != null && owned.has(r.name)));
|
|
1111
|
+
const dropped = before - resolvedResources.length;
|
|
1112
|
+
if (dropped > 0) fastify.log.debug(`[arc] ${dropped} app resource(s) superseded by module owns() — the module version registers instead`);
|
|
1113
|
+
}
|
|
791
1114
|
}
|
|
792
1115
|
if (moduleResources.length) resolvedResources = [...moduleResources, ...resolvedResources ?? []];
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1116
|
+
if (resolvedResources && resolvedResources.length > 0) {
|
|
1117
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1118
|
+
for (const resource of resolvedResources) if (resource.name) {
|
|
1119
|
+
if (seen.has(resource.name)) {
|
|
1120
|
+
const msg = `Duplicate resource name "${resource.name}" detected. This will cause route conflicts. Check your resources array and loadResources() output. Common cause: stale compiled files in dist/ alongside src/. Use \`strictResources: true\` to fail boot.`;
|
|
1121
|
+
if (config.strictResources) throw new Error(msg);
|
|
1122
|
+
fastify.log.warn(msg);
|
|
1123
|
+
}
|
|
1124
|
+
seen.add(resource.name);
|
|
801
1125
|
}
|
|
802
|
-
|
|
1126
|
+
const prefixed = [];
|
|
1127
|
+
const root = [];
|
|
1128
|
+
for (const resource of resolvedResources) if (resource.skipGlobalPrefix) root.push(resource);
|
|
1129
|
+
else prefixed.push(resource);
|
|
1130
|
+
for (const resource of root) await registerOne(fastify, resource, config.mountRoutes);
|
|
1131
|
+
if (prefixed.length) if (config.resourcePrefix) await fastify.register(async (scoped) => {
|
|
1132
|
+
for (const resource of prefixed) await registerOne(scoped, resource, config.mountRoutes);
|
|
1133
|
+
}, { prefix: config.resourcePrefix });
|
|
1134
|
+
else for (const resource of prefixed) await registerOne(fastify, resource, config.mountRoutes);
|
|
1135
|
+
const names = resolvedResources.map((r) => r.name ?? "?").join(", ");
|
|
1136
|
+
const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
|
|
1137
|
+
fastify.log.info(`${resolvedResources.length} resource(s) registered${prefix}: ${names}`);
|
|
1138
|
+
} else {
|
|
1139
|
+
const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
|
|
1140
|
+
const scanned = discoveryPath ? ` — resourceDir "${discoveryRawDir}" resolved to "${discoveryPath}"` : "";
|
|
1141
|
+
const hints = discoveryYieldedZero ? ` but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/). Use \`strictResourceDir: true\` to fail boot.` : "";
|
|
1142
|
+
fastify.log.warn(`0 resources registered${prefix}${scanned}${hints}`);
|
|
803
1143
|
}
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
else for (const resource of prefixed) await registerOne(fastify, resource, config.mountRoutes);
|
|
813
|
-
const names = resolvedResources.map((r) => r.name ?? "?").join(", ");
|
|
814
|
-
const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
|
|
815
|
-
fastify.log.info(`${resolvedResources.length} resource(s) registered${prefix}: ${names}`);
|
|
816
|
-
} else {
|
|
817
|
-
const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
|
|
818
|
-
const scanned = discoveryPath ? ` — resourceDir "${discoveryRawDir}" resolved to "${discoveryPath}"` : "";
|
|
819
|
-
const hints = discoveryYieldedZero ? ` but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/). Use \`strictResourceDir: true\` to fail boot.` : "";
|
|
820
|
-
fastify.log.warn(`0 resources registered${prefix}${scanned}${hints}`);
|
|
821
|
-
}
|
|
822
|
-
for (const m of modules) {
|
|
823
|
-
if (!m.afterResources) continue;
|
|
824
|
-
try {
|
|
825
|
-
await m.afterResources(fastify);
|
|
826
|
-
} catch (err) {
|
|
827
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
828
|
-
throw new Error(`[arc] module "${m.name}" afterResources() threw: ${msg}`, { cause: err });
|
|
1144
|
+
for (const m of modules) {
|
|
1145
|
+
if (!m.afterResources) continue;
|
|
1146
|
+
try {
|
|
1147
|
+
await m.afterResources(fastify);
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1150
|
+
throw new Error(`[arc] module "${m.name}" afterResources() threw: ${msg}`, { cause: err });
|
|
1151
|
+
}
|
|
829
1152
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
schedules: frozenJobs
|
|
841
|
-
});
|
|
842
|
-
if (fastify.arc) {
|
|
843
|
-
fastify.arc.scheduledJobs = frozenJobs;
|
|
844
|
-
fastify.arc.plugins.set("arc-schedules", {
|
|
845
|
-
name: "arc-schedules",
|
|
846
|
-
options: { scheduleCount: frozenJobs.length },
|
|
847
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1153
|
+
const scheduledJobs = await collectModuleScheduledJobs(fastify, modules);
|
|
1154
|
+
if (scheduledJobs.length > 0) {
|
|
1155
|
+
if (config.arcPlugins?.schedules === false) throw new Error("[arc] modules declare scheduledJobs but arcPlugins.schedules is false. Enable the scheduler or remove the declarations.");
|
|
1156
|
+
if (fastify.hasDecorator("getScheduleStats")) throw new Error("[arc] modules declare scheduledJobs but schedulesPlugin was already registered manually. Configure their runner through arcPlugins.schedules so Arc can compose one schedule table.");
|
|
1157
|
+
const frozenJobs = Object.freeze(scheduledJobs.map((job) => Object.freeze({ ...job })));
|
|
1158
|
+
const scheduleOptions = typeof config.arcPlugins?.schedules === "object" ? config.arcPlugins.schedules : {};
|
|
1159
|
+
const { default: schedulesPlugin } = await import("./schedules-DdtlICTf.mjs").then((n) => n.r);
|
|
1160
|
+
await fastify.register(schedulesPlugin, {
|
|
1161
|
+
...scheduleOptions,
|
|
1162
|
+
schedules: frozenJobs
|
|
848
1163
|
});
|
|
1164
|
+
if (fastify.arc) {
|
|
1165
|
+
fastify.arc.scheduledJobs = frozenJobs;
|
|
1166
|
+
fastify.arc.plugins.set("arc-schedules", {
|
|
1167
|
+
name: "arc-schedules",
|
|
1168
|
+
options: { scheduleCount: frozenJobs.length },
|
|
1169
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules);
|
|
1174
|
+
if (config.afterResources) {
|
|
1175
|
+
await config.afterResources(fastify);
|
|
1176
|
+
fastify.log.debug("afterResources hook executed");
|
|
849
1177
|
}
|
|
850
|
-
}
|
|
851
|
-
const eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules);
|
|
852
|
-
if (config.afterResources) try {
|
|
853
|
-
await config.afterResources(fastify);
|
|
854
|
-
fastify.log.debug("afterResources hook executed");
|
|
855
1178
|
} catch (err) {
|
|
856
|
-
const
|
|
857
|
-
for (const
|
|
1179
|
+
const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
|
|
1180
|
+
for (const unsubscribeError of unsubscribeErrors) fastify.log.error({ err: unsubscribeError }, "[arc] module event-handler rollback after boot failure failed");
|
|
1181
|
+
const closeErrors = await teardown.rollback();
|
|
1182
|
+
for (const closeError of closeErrors) fastify.log.error({ err: closeError }, "[arc] module onClose rollback after boot failure failed");
|
|
858
1183
|
throw err;
|
|
859
1184
|
}
|
|
860
1185
|
if (config.onReady) {
|
|
@@ -863,13 +1188,14 @@ async function registerResources(fastify, config, preResolvedModules) {
|
|
|
863
1188
|
await onReady(fastify);
|
|
864
1189
|
});
|
|
865
1190
|
}
|
|
866
|
-
const closers = modules.filter((m) => m.onClose).reverse();
|
|
867
1191
|
const appOnClose = config.onClose;
|
|
868
|
-
if (
|
|
1192
|
+
if (teardown.hasComposedModules() || appOnClose || eventUnsubscribes.length) fastify.addHook("onClose", async () => {
|
|
869
1193
|
const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
|
|
870
1194
|
for (const err of unsubscribeErrors) fastify.log.error({ err }, "[arc] module event-handler shutdown unsubscribe failed");
|
|
871
|
-
|
|
1195
|
+
const closeErrors = await teardown.closeAll();
|
|
1196
|
+
for (const err of closeErrors) fastify.log.error({ err }, "[arc] module onClose failed during shutdown");
|
|
872
1197
|
if (appOnClose) await appOnClose(fastify);
|
|
1198
|
+
if (closeErrors.length > 0) throw closeErrors[0];
|
|
873
1199
|
});
|
|
874
1200
|
}
|
|
875
1201
|
//#endregion
|
|
@@ -935,56 +1261,6 @@ function resolveCorsOptions(config) {
|
|
|
935
1261
|
};
|
|
936
1262
|
}
|
|
937
1263
|
//#endregion
|
|
938
|
-
//#region src/factory/security/pluginLoader.ts
|
|
939
|
-
const PLUGIN_REGISTRY = {
|
|
940
|
-
cors: {
|
|
941
|
-
package: "@fastify/cors",
|
|
942
|
-
loader: () => import("@fastify/cors").then((m) => m.default)
|
|
943
|
-
},
|
|
944
|
-
helmet: {
|
|
945
|
-
package: "@fastify/helmet",
|
|
946
|
-
loader: () => import("@fastify/helmet").then((m) => m.default)
|
|
947
|
-
},
|
|
948
|
-
rateLimit: {
|
|
949
|
-
package: "@fastify/rate-limit",
|
|
950
|
-
loader: () => import("@fastify/rate-limit").then((m) => m.default)
|
|
951
|
-
},
|
|
952
|
-
underPressure: {
|
|
953
|
-
package: "@fastify/under-pressure",
|
|
954
|
-
loader: () => import("@fastify/under-pressure").then((m) => m.default)
|
|
955
|
-
},
|
|
956
|
-
sensible: {
|
|
957
|
-
package: "@fastify/sensible",
|
|
958
|
-
loader: () => import("@fastify/sensible").then((m) => m.default)
|
|
959
|
-
},
|
|
960
|
-
multipart: {
|
|
961
|
-
package: "@fastify/multipart",
|
|
962
|
-
loader: () => import("@fastify/multipart").then((m) => m.default),
|
|
963
|
-
optional: true
|
|
964
|
-
},
|
|
965
|
-
rawBody: {
|
|
966
|
-
package: "fastify-raw-body",
|
|
967
|
-
loader: () => import("fastify-raw-body").then((m) => m.default),
|
|
968
|
-
optional: true
|
|
969
|
-
}
|
|
970
|
-
};
|
|
971
|
-
async function loadPlugin(name, logger) {
|
|
972
|
-
const entry = PLUGIN_REGISTRY[name];
|
|
973
|
-
if (!entry) throw new Error(`Unknown plugin: ${name}`);
|
|
974
|
-
try {
|
|
975
|
-
return await entry.loader();
|
|
976
|
-
} catch (error) {
|
|
977
|
-
const err = error;
|
|
978
|
-
const isModuleNotFound = err.message.includes("Cannot find module") || err.message.includes("Cannot find package") || err.message.includes("MODULE_NOT_FOUND") || err.message.includes("Could not resolve");
|
|
979
|
-
if (isModuleNotFound && entry.optional) {
|
|
980
|
-
logger?.warn(`Optional plugin '${name}' skipped (${entry.package} not installed)`);
|
|
981
|
-
return null;
|
|
982
|
-
}
|
|
983
|
-
if (isModuleNotFound) throw new Error(`Plugin '${name}' requires package '${entry.package}' which is not installed.\nInstall it with: npm install ${entry.package}\nOr disable this plugin by setting ${name}: false in createApp options.`);
|
|
984
|
-
throw new Error(`Failed to load plugin '${name}': ${err.message}`);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
//#endregion
|
|
988
1264
|
//#region src/factory/security/rateLimit.ts
|
|
989
1265
|
/** "Unlimited" as a number — @fastify/rate-limit's `max` fn must return one. */
|
|
990
1266
|
const UNLIMITED_MAX = Number.MAX_SAFE_INTEGER;
|
|
@@ -1308,8 +1584,26 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
|
|
|
1308
1584
|
* 10. afterResources() — post-registration wiring
|
|
1309
1585
|
* 11. onReady/onClose — lifecycle hooks
|
|
1310
1586
|
* ```
|
|
1587
|
+
*
|
|
1588
|
+
* **Boot is all-or-nothing.** If any phase throws, the partially-built
|
|
1589
|
+
* instance is closed before the error propagates — see the wrapper below.
|
|
1311
1590
|
*/
|
|
1312
1591
|
async function createApp(options) {
|
|
1592
|
+
let partial;
|
|
1593
|
+
try {
|
|
1594
|
+
return await buildApp(options, (instance) => {
|
|
1595
|
+
partial = instance;
|
|
1596
|
+
});
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
if (partial) try {
|
|
1599
|
+
await partial.close();
|
|
1600
|
+
} catch (closeErr) {
|
|
1601
|
+
partial.log?.error?.({ err: closeErr }, "[arc] cleanup of a partially-booted app failed; original boot error follows");
|
|
1602
|
+
}
|
|
1603
|
+
throw err;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
async function buildApp(options, onInstance) {
|
|
1313
1607
|
if (options.debug !== void 0 && options.debug !== false) configureArcLogger({ debug: options.debug });
|
|
1314
1608
|
validateAuthOptions(options);
|
|
1315
1609
|
const deferredWarnings = validateDistributedRuntime(options);
|
|
@@ -1350,6 +1644,7 @@ async function createApp(options) {
|
|
|
1350
1644
|
keywords: ["example", ...config.ajv?.keywords ?? []]
|
|
1351
1645
|
} }
|
|
1352
1646
|
});
|
|
1647
|
+
onInstance(fastify);
|
|
1353
1648
|
if (resolvedLogger !== false) configureArcLogger({
|
|
1354
1649
|
...options.debug !== void 0 && options.debug !== false ? { debug: options.debug } : {},
|
|
1355
1650
|
writer: createPinoWriter(fastify.log)
|
|
@@ -1365,6 +1660,7 @@ async function createApp(options) {
|
|
|
1365
1660
|
}
|
|
1366
1661
|
});
|
|
1367
1662
|
await registerSecurityPlugins(fastify, config);
|
|
1663
|
+
if (config.assets?.length) await registerAssetRoots(fastify, config.assets);
|
|
1368
1664
|
await registerUtilityPlugins(fastify, config);
|
|
1369
1665
|
const trackPlugin = (name, opts) => {
|
|
1370
1666
|
const arc = fastify.arc;
|
|
@@ -1387,7 +1683,7 @@ async function createApp(options) {
|
|
|
1387
1683
|
await registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers);
|
|
1388
1684
|
await registerResources(fastify, config, resolvedModules);
|
|
1389
1685
|
if (config.replyHelpers) {
|
|
1390
|
-
const { replyHelpersPlugin } = await import("./replyHelpers-
|
|
1686
|
+
const { replyHelpersPlugin } = await import("./replyHelpers-DIiZKGUt.mjs").then((n) => n.n);
|
|
1391
1687
|
await fastify.register(replyHelpersPlugin);
|
|
1392
1688
|
}
|
|
1393
1689
|
if (config.serializeBigInt) {
|
|
@@ -1438,4 +1734,4 @@ const ArcFactory = {
|
|
|
1438
1734
|
}
|
|
1439
1735
|
};
|
|
1440
1736
|
//#endregion
|
|
1441
|
-
export { edgePreset as a, testingPreset as c,
|
|
1737
|
+
export { resolveContribution as _, edgePreset as a, testingPreset as c, hasModule as d, orderModules as f, collectModuleWorkflows as g, collectModuleScheduledJobs as h, developmentPreset as i, workerPreset as l, collectModuleHealthChecks as m, createApp as n, getPreset as o, subscribeModuleEventHandlers as p, createApp_exports as r, productionPreset as s, ArcFactory as t, getModuleState as u, resolveModule as v };
|