@classytic/arc 2.41.1 → 2.44.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/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
- package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-DZWDsaPa.mjs} +22 -17
- package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-yjEgn7Cm.mjs} +2 -2
- package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
- package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-DJxHUpE4.mjs} +44 -13
- package/dist/acquire-BozJ-9yt.mjs +35 -0
- package/dist/actionPermissions-5N04zBL4.mjs +772 -0
- package/dist/audit/index.d.mts +46 -8
- package/dist/audit/index.mjs +48 -26
- package/dist/auth/audit.d.mts +8 -8
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +46 -16
- package/dist/auth/index.mjs +134 -156
- package/dist/auth/redis-session.d.mts +4 -5
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-wX3SA9w_.mjs} +7 -102
- package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
- package/dist/boundedMap-DWiKW7lP.mjs +57 -0
- package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-Bswu_vNb.mjs} +4 -4
- package/dist/cache/index.d.mts +31 -43
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-DbCBxlgw.mjs → caching-Drp68DIS.mjs} +3 -3
- package/dist/claimed-DYvQoZLv.mjs +8 -0
- package/dist/cleanup/index.d.mts +18 -22
- package/dist/cleanup/index.mjs +27 -73
- package/dist/cli/commands/describe.d.mts +7 -5
- package/dist/cli/commands/describe.mjs +7 -4
- package/dist/cli/commands/docs.d.mts +2 -3
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.d.mts +2 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -3
- package/dist/cli/commands/init.mjs +2 -3
- package/dist/cli/commands/introspect.d.mts +2 -3
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-D5Ubn1fe.mjs → constants-B2QHb6u6.mjs} +4 -2
- package/dist/context/index.d.mts +11 -14
- package/dist/context/index.mjs +18 -38
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +8 -8
- package/dist/{core-5lazhva9.mjs → core-COcqo0Cp.mjs} +15 -5
- package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-zG13rpPJ.mjs} +97 -39
- package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-CWRz96Vr.mjs} +11 -8
- package/dist/{preloadResources-f3boenVZ.mjs → createApp-BGLYtErt.mjs} +305 -394
- package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-DSztofa9.d.mts} +1 -1
- package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation--3xguLkk.mjs} +1 -1
- package/dist/{defineResource-DxkMTjRn.mjs → defineResource-gUp9ug66.mjs} +118 -1024
- package/dist/discovery/index.d.mts +6 -7
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +11 -5
- package/dist/docs/index.mjs +4 -4
- package/dist/{elevation-CkvG0LG2.mjs → elevation-BG4gnMsJ.mjs} +41 -12
- package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
- package/dist/encryption/index.d.mts +5 -5
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-COHw3N1b.mjs} +26 -5
- package/dist/{errors-BH7YU_9O.mjs → errors-CW4anJBy.mjs} +58 -23
- package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
- package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
- package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-BweHAVws.mjs} +82 -21
- package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-N4-omQFR.mjs} +151 -17
- package/dist/events/index.d.mts +43 -18
- package/dist/events/index.mjs +49 -53
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +139 -157
- package/dist/events/transports/redis.d.mts +5 -6
- package/dist/events/transports/redis.mjs +2 -2
- package/dist/factory/index.d.mts +42 -27
- package/dist/factory/index.mjs +213 -32
- package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DRlCIWE9.d.mts} +11 -25
- package/dist/{fields-BHrkoF5y.mjs → fields-A5crhsJb.mjs} +1 -1
- package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
- package/dist/filter-merge-DGM-GQ1J.mjs +99 -0
- package/dist/{generate-BLRf2m3y.mjs → generate-BXKo2Gc8.mjs} +2 -2
- package/dist/{headers-BIXWKrzQ.mjs → headers-DKZQKL-Z.mjs} +2 -2
- package/dist/health-BYAwA6YS.mjs +22 -0
- package/dist/{health-ZRHeDHXH.mjs → health-CrqUp3sz.mjs} +115 -55
- package/dist/hooks/index.d.mts +2 -2
- package/dist/hooks/index.mjs +2 -3
- package/dist/idempotency/index.d.mts +36 -10
- package/dist/idempotency/index.mjs +230 -111
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +77 -12
- package/dist/{index-D4u76eQW.d.mts → index-Beg3aNnK.d.mts} +23 -101
- package/dist/{preloadResources-CjS2OfpU.d.mts → index-BkV55WlV.d.mts} +37 -38
- package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
- package/dist/index-CUJvoyw3.d.mts +510 -0
- package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
- package/dist/{index-DHbyPo5j.d.mts → index-WH7xgvxS.d.mts} +304 -254
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +11 -11
- package/dist/{init-CpLkjU18.mjs → init-BPzK5Lnr.mjs} +1 -1
- package/dist/integrations/event-gateway.d.mts +11 -7
- package/dist/integrations/event-gateway.mjs +4 -4
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs/index.d.mts +1 -1
- package/dist/integrations/jobs/index.mjs +18 -5
- package/dist/integrations/mcp/index.d.mts +102 -89
- package/dist/integrations/mcp/index.mjs +19 -4
- package/dist/integrations/mcp/testing.d.mts +8 -9
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +85 -144
- package/dist/integrations/streamline.mjs +131 -55
- package/dist/integrations/webhooks.d.mts +83 -143
- package/dist/integrations/webhooks.mjs +36 -80
- package/dist/integrations/websocket-pushref-redis.d.mts +5 -6
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.d.mts +5 -6
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.d.mts +3 -3
- package/dist/integrations/websocket.mjs +2 -2
- package/dist/{interface-DEWNOMza.d.mts → interface-JJzwH4Ro.d.mts} +34 -2
- package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-YIq-vGLv.mjs} +2 -2
- package/dist/{keys-cpZCtcIE.mjs → keys-CpoiZ8hU.mjs} +7 -18
- package/dist/{loadResources-BPfWiI5s.mjs → loadResources-6jO52-RR.mjs} +3 -3
- package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-MP8xEppu.mjs} +2 -2
- package/dist/logger/index.d.mts +9 -17
- package/dist/logger/index.mjs +2 -153
- package/dist/logger-D4V1zGLI.mjs +160 -0
- package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-srPc7ItN.mjs} +446 -170
- package/dist/{memory-Iotz0cCx.mjs → memory-CTa5pOCW.mjs} +36 -34
- package/dist/{metrics-NMsmpRmY.mjs → metrics-DsZrvnpB.mjs} +39 -72
- package/dist/middleware/index.d.mts +5 -5
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.d.mts +13 -13
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-DpLJG2_N.mjs → module--L0iBIZz.mjs} +8 -8
- package/dist/mongodb-memory-server-CHhYSq5y.mjs +35539 -0
- package/dist/mongoose-CYJCLyi-.mjs +67451 -0
- package/dist/multiTenant-CAtYlW6H.d.mts +104 -0
- package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-Cc7eZ9cq.mjs} +46 -7
- package/dist/node-D8UZv_8c.mjs +41236 -0
- package/dist/{openapi-CmFf3yq_.mjs → openapi-CMegfsm8.mjs} +48 -10
- package/dist/outbox-admin/index.d.mts +5 -5
- package/dist/outbox-admin/index.mjs +6 -18
- package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Ryk270jd.mjs} +203 -33
- package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
- package/dist/permissions/index.d.mts +5 -4
- package/dist/permissions/index.mjs +8 -6
- package/dist/{permissions-D8TBH44E.mjs → permissions-C5ijIz5Q.mjs} +149 -33
- package/dist/{pipe-FMWNheSO.mjs → pipe-D81zMtWs.mjs} +2 -2
- package/dist/pipeline/index.d.mts +7 -7
- package/dist/pipeline/index.mjs +2 -2
- package/dist/plan-CJGMKoui.mjs +565 -0
- package/dist/{BaseController-CYEwjyjN.mjs → plugin-DALd9dcF.mjs} +1269 -346
- package/dist/plugins/index.d.mts +17 -14
- package/dist/plugins/index.mjs +44 -22
- package/dist/plugins/response-cache.d.mts +5 -6
- package/dist/plugins/response-cache.mjs +8 -14
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-CXWZIzTv.mjs → pluralize-COUNYo3d.mjs} +1 -1
- package/dist/presets/filesUpload.d.mts +8 -8
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +35 -40
- package/dist/presets/index.mjs +4 -4
- package/dist/presets/multiTenant.d.mts +2 -107
- package/dist/presets/multiTenant.mjs +22 -162
- package/dist/presets/search.d.mts +9 -10
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-ZJSPgmT2.mjs → presets-CONf2Qem.mjs} +99 -107
- package/dist/{projection-BzIgzoKZ.mjs → projection-wFoLqdss.mjs} +2 -2
- package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
- package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-BATECpyh.mjs} +12 -14
- package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
- package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-DVEZ_UpV.mjs} +1 -1
- package/dist/{realtime-8tIQWZz4.mjs → realtime-xyubMKSB.mjs} +25 -21
- package/dist/{redis-Bzi1y3F9.d.mts → redis-BDgv9Glw.d.mts} +17 -24
- package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
- package/dist/redisShapes-CQksY67q.d.mts +55 -0
- package/dist/registry/index.d.mts +15 -10
- package/dist/registry/index.mjs +4 -4
- package/dist/{registry-DNkgGQcK.mjs → registry-Bdzg-hdm.mjs} +13 -10
- package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-BFJxGPuh.mjs} +1 -1
- package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-Bnc6lJyy.mjs} +2 -2
- package/dist/{requestContext-cDkAUr-P.mjs → requestContext-C10UeFe5.mjs} +1 -1
- package/dist/requirement-Bu50QALH.mjs +35 -0
- package/dist/requirement-CbiLdQ1n.d.mts +22 -0
- package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
- package/dist/resourceRateLimit-urWPJxrt.mjs +180 -0
- package/dist/rolldown-runtime-MYrLNGnq.mjs +45 -0
- package/dist/routeValidator-DHtpzrzk.mjs +70 -0
- package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-DIMKMV2G.mjs} +1 -1
- package/dist/{schedules-j6uXXAeJ.mjs → schedules-CKTs5ys8.mjs} +46 -37
- package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BVjkQ5DD.mjs} +2 -2
- package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-BA0Hb21Y.mjs} +2 -2
- package/dist/schemas/index.d.mts +20 -10
- package/dist/schemas/index.mjs +8 -3
- package/dist/scim/index.d.mts +13 -13
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.d.mts +4 -4
- package/dist/scope/index.mjs +6 -6
- package/dist/{scope-BUi1xV1I.mjs → scope-DINaj7Ax.mjs} +4 -4
- package/dist/shutdown-LpLMH8T1.mjs +59 -0
- package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-DbwIM1f1.mjs} +1 -1
- package/dist/{sse-BeqVKQKu.mjs → sse-BzJFPX_1.mjs} +11 -15
- package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-DQChzGTs.mjs} +2 -2
- package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-Coj9RhOZ.mjs} +1 -1
- package/dist/{streaming-IcRFjWQU.mjs → streaming-JzB5CAga.mjs} +1 -1
- package/dist/subscriberIdentity-DeyppIif.mjs +28 -0
- package/dist/tenancy-DxV8OuX2.mjs +43 -0
- package/dist/tenantPreparation-CBvvAcAj.mjs +67 -0
- package/dist/testing/cleanupStoreContract.d.mts +4 -5
- package/dist/testing/cleanupStoreContract.mjs +1 -1
- package/dist/testing/index.d.mts +215 -197
- package/dist/testing/index.mjs +289 -214
- package/dist/testing/outboxStoreContract.d.mts +4 -5
- package/dist/testing/outboxStoreContract.mjs +137 -2
- package/dist/testing/requiredPorts.d.mts +16 -0
- package/dist/testing/requiredPorts.mjs +65 -0
- package/dist/testing/storageContract.d.mts +4 -5
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/timing-CntavTlR.mjs +46 -0
- package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-orvnGu4F.mjs} +1 -1
- package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-kEaP_WmO.mjs} +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/index.mjs +1 -1
- package/dist/types/storage.mjs +1 -1
- package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
- package/dist/{types-CWQn9-IC.d.mts → types-D3Ixsh00.d.mts} +22 -9
- package/dist/{types-CgenB1gv.mjs → types-fvO6Ovzx.mjs} +4 -4
- package/dist/usage/index.d.mts +5 -6
- package/dist/usage/index.mjs +5 -5
- package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-C76zD5pa.mjs} +1 -1
- package/dist/utils/index.d.mts +125 -264
- package/dist/utils/index.mjs +13 -12
- package/dist/{utils-CxcctrAh.mjs → utils-BpkVZ8rB.mjs} +315 -404
- package/dist/{validate-DrYWnOzI.mjs → validate-CWp-lsrm.mjs} +6 -14
- package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
- package/dist/{versioning-DKkT4hNJ.mjs → versioning-CHhL9P3o.mjs} +4 -4
- package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
- package/dist/{workScope-CxKqoIFn.mjs → workScope-n9a7d_lZ.mjs} +2 -2
- package/package.json +57 -59
- package/skills/arc/SKILL.md +13 -4
- package/skills/arc/references/auth.md +8 -5
- package/skills/arc/references/integrations.md +1 -1
- package/skills/arc/references/mcp.md +8 -6
- package/skills/arc/references/multi-tenancy.md +19 -14
- package/skills/arc/references/production.md +27 -10
- package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
- package/dist/index-CFfLKMH4.d.mts +0 -290
- package/dist/interface-YzIbF7Ii.mjs +0 -17
- package/dist/redis-stream-BPKscsTB.d.mts +0 -260
- package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
- package/dist/rolldown-runtime-eysyzFAF.mjs +0 -15
- package/dist/sync/changelog.d.mts +0 -3
- package/dist/sync/changelog.mjs +0 -3
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/*! @classytic/arc v2.44.0 | MIT | https://classytic.com */
|
|
2
|
+
import { createEventRouter, matchEventPattern } from "@classytic/primitives/events";
|
|
3
|
+
//#region src/factory/module/graph.ts
|
|
4
|
+
/**
|
|
5
|
+
* The module graph — which modules are active, which events each OWNS, which it consumes — and
|
|
6
|
+
* the boot-time checks that keep it honest.
|
|
7
|
+
*
|
|
8
|
+
* - A `kind: 'bridge'` module joins domains: it is ACTIVE only when every `dependsOn` module is
|
|
9
|
+
* composed, and dropped (with the reason recorded) otherwise — never a boot error. Switching a
|
|
10
|
+
* domain off therefore removes every bridge that touched it, with no gate code anywhere.
|
|
11
|
+
* - `publishes` names the facts a module owns. What it consumes is DERIVED from its
|
|
12
|
+
* `eventHandlers`, so it is never declared twice.
|
|
13
|
+
* - The graph is two incidence relations over modules × events — P (publishes) and C (consumes).
|
|
14
|
+
* `analyzeModuleGraph` resolves them ONCE (the pattern × event matching is the only real cost);
|
|
15
|
+
* verification, rendering and `dependentsOf` all read that one analysis.
|
|
16
|
+
*/
|
|
17
|
+
function moduleKind(m) {
|
|
18
|
+
return m.kind ?? "domain";
|
|
19
|
+
}
|
|
20
|
+
/** The event names a module owns. */
|
|
21
|
+
function publishedEvents(m) {
|
|
22
|
+
return (m.publishes ?? []).map((e) => typeof e === "string" ? e : e.name);
|
|
23
|
+
}
|
|
24
|
+
/** `order` owns `order:placed` and `order.placed` — `:` for new events, `.` for kernels' existing ones. */
|
|
25
|
+
function ownsEvent(namespace, event) {
|
|
26
|
+
return event.startsWith(`${namespace}:`) || event.startsWith(`${namespace}.`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Drop every bridge whose `dependsOn` is not fully composed — to a fixpoint, so a bridge that
|
|
30
|
+
* depends on a dropped bridge is dropped too. Domain and platform modules are never dropped:
|
|
31
|
+
* their missing dependencies stay a hard error in `orderModules`.
|
|
32
|
+
*/
|
|
33
|
+
function activateBridges(modules) {
|
|
34
|
+
let active = [...modules];
|
|
35
|
+
const skipped = [];
|
|
36
|
+
for (;;) {
|
|
37
|
+
const names = new Set(active.map((m) => m.name));
|
|
38
|
+
const drop = active.filter((m) => moduleKind(m) === "bridge" && (m.dependsOn ?? []).some((d) => !names.has(d)));
|
|
39
|
+
if (drop.length === 0) return {
|
|
40
|
+
active,
|
|
41
|
+
skipped
|
|
42
|
+
};
|
|
43
|
+
for (const m of drop) skipped.push({
|
|
44
|
+
name: m.name,
|
|
45
|
+
missing: (m.dependsOn ?? []).filter((d) => !names.has(d))
|
|
46
|
+
});
|
|
47
|
+
const dropped = new Set(drop.map((m) => m.name));
|
|
48
|
+
active = active.filter((m) => !dropped.has(m.name));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The events arc auto-emits for a resource (`<name>.created|updated|deleted`) — only for
|
|
53
|
+
* mutations that actually MOUNTED. Overstating would satisfy a consumer that never receives one.
|
|
54
|
+
*/
|
|
55
|
+
function crudEventsOf(r) {
|
|
56
|
+
if (!r.adapter || r.disableDefaultRoutes) return [];
|
|
57
|
+
const disabled = new Set(r.disabledRoutes ?? []);
|
|
58
|
+
return [
|
|
59
|
+
"create",
|
|
60
|
+
"update",
|
|
61
|
+
"delete"
|
|
62
|
+
].filter((op) => !disabled.has(op)).map((op) => `${r.name}.${op}d`);
|
|
63
|
+
}
|
|
64
|
+
function buildModuleGraph(modules, consumedByModule, skippedBridges, externalEvents = [], derived = /* @__PURE__ */ new Map()) {
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
modules: Object.freeze(modules.map((m) => Object.freeze({
|
|
67
|
+
name: m.name,
|
|
68
|
+
kind: moduleKind(m),
|
|
69
|
+
namespaces: Object.freeze([.../* @__PURE__ */ new Set([...m.eventNamespace === void 0 ? [m.name] : [m.eventNamespace].flat(), ...derived.get(m.name)?.namespaces ?? []])]),
|
|
70
|
+
dependsOn: Object.freeze([...m.dependsOn ?? []]),
|
|
71
|
+
publishes: Object.freeze([.../* @__PURE__ */ new Set([...publishedEvents(m), ...derived.get(m.name)?.publishes ?? []])]),
|
|
72
|
+
consumes: Object.freeze([...new Set(consumedByModule.get(m.name) ?? [])])
|
|
73
|
+
}))),
|
|
74
|
+
skippedBridges: Object.freeze([...skippedBridges]),
|
|
75
|
+
externalEvents: Object.freeze([...externalEvents])
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** Resolve the graph's incidence once — every check and view reads this. */
|
|
79
|
+
function analyzeModuleGraph(graph) {
|
|
80
|
+
const owners = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const m of graph.modules) for (const event of m.publishes) {
|
|
82
|
+
const list = owners.get(event);
|
|
83
|
+
if (list) list.push(m.name);
|
|
84
|
+
else owners.set(event, [m.name]);
|
|
85
|
+
}
|
|
86
|
+
const router = createEventRouter();
|
|
87
|
+
const reaches = /* @__PURE__ */ new Map();
|
|
88
|
+
const coupling = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const m of graph.modules) {
|
|
90
|
+
const byPattern = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const pattern of m.consumes) {
|
|
92
|
+
byPattern.set(pattern, []);
|
|
93
|
+
router.add(pattern, {
|
|
94
|
+
module: m.name,
|
|
95
|
+
pattern
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
reaches.set(m.name, byPattern);
|
|
99
|
+
coupling.set(m.name, /* @__PURE__ */ new Map());
|
|
100
|
+
}
|
|
101
|
+
for (const [event, publishers] of owners) for (const sub of router.route(event)) {
|
|
102
|
+
reaches.get(sub.module)?.get(sub.pattern)?.push(event);
|
|
103
|
+
const byPublisher = coupling.get(sub.module);
|
|
104
|
+
for (const publisher of publishers) {
|
|
105
|
+
if (publisher === sub.module) continue;
|
|
106
|
+
const list = byPublisher.get(publisher);
|
|
107
|
+
if (!list) byPublisher.set(publisher, [event]);
|
|
108
|
+
else if (list[list.length - 1] !== event) list.push(event);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
owners,
|
|
113
|
+
reaches,
|
|
114
|
+
coupling
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Every issue in the graph — empty when it is sound. */
|
|
118
|
+
function verifyModuleGraph(graph, analysis = analyzeModuleGraph(graph)) {
|
|
119
|
+
const issues = [];
|
|
120
|
+
for (const [event, owners] of analysis.owners) for (const extra of owners.slice(1)) issues.push({
|
|
121
|
+
code: "duplicate-owner",
|
|
122
|
+
module: extra,
|
|
123
|
+
event,
|
|
124
|
+
message: `event "${event}" is published by both "${owners[0]}" and "${extra}" — an event has exactly one owner.`
|
|
125
|
+
});
|
|
126
|
+
for (const m of graph.modules) {
|
|
127
|
+
for (const event of m.publishes) {
|
|
128
|
+
if (m.namespaces.some((ns) => ownsEvent(ns, event))) continue;
|
|
129
|
+
issues.push({
|
|
130
|
+
code: "outside-namespace",
|
|
131
|
+
module: m.name,
|
|
132
|
+
event,
|
|
133
|
+
message: `module "${m.name}" publishes "${event}" outside its namespace(s) ${m.namespaces.map((n) => `"${n}"`).join(", ")} — a module publishes its OWN facts; a process joining two domains is a bridge with its own namespace.`
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
for (const [pattern, hit] of analysis.reaches.get(m.name) ?? []) {
|
|
137
|
+
if (hit.length > 0) continue;
|
|
138
|
+
if (graph.externalEvents.some((ext) => ext === pattern || matchEventPattern(ext, pattern))) continue;
|
|
139
|
+
issues.push({
|
|
140
|
+
code: "no-publisher",
|
|
141
|
+
module: m.name,
|
|
142
|
+
event: pattern,
|
|
143
|
+
message: `module "${m.name}" consumes "${pattern}", which no active module publishes — declare the owner's \`publishes\`, list it in \`moduleGraph.externalEvents\`, or make "${m.name}" a bridge of the module that owns it.`
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return issues;
|
|
148
|
+
}
|
|
149
|
+
/** The events a module declares `durability: 'durable'` in its `publishes`. */
|
|
150
|
+
function durablePublishes(m) {
|
|
151
|
+
return (m.publishes ?? []).flatMap((e) => typeof e !== "string" && "durability" in e && e.durability === "durable" ? [e.name] : []);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A durable event's publisher must `dependsOn` a transactional outbox, and each of its handlers
|
|
155
|
+
* must declare `durability` (`'retry'` rethrows to the relay; `'boundary'` may swallow).
|
|
156
|
+
*/
|
|
157
|
+
function verifyEventDurability(modules, facts) {
|
|
158
|
+
const issues = [];
|
|
159
|
+
const durable = /* @__PURE__ */ new Set();
|
|
160
|
+
for (const m of modules) {
|
|
161
|
+
const events = durablePublishes(m);
|
|
162
|
+
for (const event of events) durable.add(event);
|
|
163
|
+
if (events.length === 0 || (m.dependsOn ?? []).some((d) => facts.outboxModules.has(d))) continue;
|
|
164
|
+
for (const event of events) issues.push({
|
|
165
|
+
code: "durable-without-outbox",
|
|
166
|
+
module: m.name,
|
|
167
|
+
event,
|
|
168
|
+
message: `module "${m.name}" publishes durable "${event}" but depends on no outbox with transactionalSave — its row cannot commit with the write.`
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (durable.size === 0) return issues;
|
|
172
|
+
for (const [moduleName, handlers] of facts.handlers) for (const h of handlers) {
|
|
173
|
+
if (h.durability !== void 0) continue;
|
|
174
|
+
for (const event of durable) {
|
|
175
|
+
if (!h.patterns.some((p) => matchEventPattern(p, event))) continue;
|
|
176
|
+
issues.push({
|
|
177
|
+
code: "durable-consumer-undeclared",
|
|
178
|
+
module: moduleName,
|
|
179
|
+
event,
|
|
180
|
+
message: `handler "${h.label}" consumes durable "${event}" without declaring durability ('retry' or 'boundary').`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return issues;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* What stops working if `name` is switched off: modules consuming events only it publishes
|
|
188
|
+
* (a column of the coupling matrix), and bridges that `dependsOn` it — which arc drops.
|
|
189
|
+
*/
|
|
190
|
+
function dependentsOf(graph, name, analysis = analyzeModuleGraph(graph)) {
|
|
191
|
+
const starved = [];
|
|
192
|
+
for (const [consumer, byPublisher] of analysis.coupling) {
|
|
193
|
+
const events = (byPublisher.get(name) ?? []).filter((e) => (analysis.owners.get(e) ?? []).length === 1);
|
|
194
|
+
if (events.length > 0) starved.push({
|
|
195
|
+
module: consumer,
|
|
196
|
+
events: [...new Set(events)]
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
starved,
|
|
201
|
+
droppedBridges: graph.modules.filter((m) => m.kind === "bridge" && m.dependsOn.includes(name)).map((m) => m.name)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The graph as Markdown — modules, the coupling between them, then every event with its owner
|
|
206
|
+
* and consumers. Generate it into the repo so people and agents read the map instead of grepping.
|
|
207
|
+
*/
|
|
208
|
+
function renderModuleGraph(graph, analysis = analyzeModuleGraph(graph)) {
|
|
209
|
+
const lines = [
|
|
210
|
+
"# Module graph",
|
|
211
|
+
"",
|
|
212
|
+
"| module | kind | depends on | publishes | consumes |",
|
|
213
|
+
"|---|---|---|---|---|"
|
|
214
|
+
];
|
|
215
|
+
for (const m of graph.modules) lines.push(`| ${m.name} | ${m.kind} | ${list(m.dependsOn)} | ${list(m.publishes)} | ${list(m.consumes)} |`);
|
|
216
|
+
if (graph.skippedBridges.length > 0) {
|
|
217
|
+
lines.push("", "## Inactive bridges", "", "| bridge | missing |", "|---|---|");
|
|
218
|
+
for (const b of graph.skippedBridges) lines.push(`| ${b.name} | ${list(b.missing)} |`);
|
|
219
|
+
}
|
|
220
|
+
lines.push("", "## Coupling — whose facts each module consumes", "", "| consumer | publisher | events |", "|---|---|---|");
|
|
221
|
+
for (const [consumer, byPublisher] of analysis.coupling) for (const [publisher, events] of byPublisher) lines.push(`| ${consumer} | ${publisher} | ${list([...new Set(events)])} |`);
|
|
222
|
+
lines.push("", "## Events", "", "| event | owner | consumers |", "|---|---|---|");
|
|
223
|
+
const consumersOf = /* @__PURE__ */ new Map();
|
|
224
|
+
for (const [consumer, byPattern] of analysis.reaches) for (const hit of byPattern.values()) for (const event of hit) consumersOf.set(event, (consumersOf.get(event) ?? /* @__PURE__ */ new Set()).add(consumer));
|
|
225
|
+
for (const [event, owners] of [...analysis.owners].sort(([a], [b]) => a < b ? -1 : 1)) lines.push(`| ${event} | ${owners.join(", ")} | ${list([...consumersOf.get(event) ?? []])} |`);
|
|
226
|
+
return `${lines.join("\n")}\n`;
|
|
227
|
+
}
|
|
228
|
+
function list(items) {
|
|
229
|
+
return items.length === 0 ? "—" : items.map((i) => `\`${i}\``).join(", ");
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Divide the graph by its coupling: Tarjan's strongly connected components over D = C·Pᵀ.
|
|
233
|
+
* A component of more than one module is an EVENT CYCLE — modules that consume each other's
|
|
234
|
+
* facts and so can only be deployed, disabled or reasoned about together. `layers` is the
|
|
235
|
+
* condensation in dependency order: each layer consumes only from layers before it, so a layer's
|
|
236
|
+
* modules can be verified, tested or split out in parallel.
|
|
237
|
+
*/
|
|
238
|
+
function eventTopology(graph, analysis = analyzeModuleGraph(graph)) {
|
|
239
|
+
const names = graph.modules.map((m) => m.name);
|
|
240
|
+
const edges = (n) => [...analysis.coupling.get(n)?.keys() ?? []];
|
|
241
|
+
const index = /* @__PURE__ */ new Map();
|
|
242
|
+
const low = /* @__PURE__ */ new Map();
|
|
243
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
244
|
+
const stack = [];
|
|
245
|
+
const components = [];
|
|
246
|
+
let counter = 0;
|
|
247
|
+
const visit = (v) => {
|
|
248
|
+
index.set(v, counter);
|
|
249
|
+
low.set(v, counter);
|
|
250
|
+
counter++;
|
|
251
|
+
stack.push(v);
|
|
252
|
+
onStack.add(v);
|
|
253
|
+
for (const w of edges(v)) if (!index.has(w)) {
|
|
254
|
+
visit(w);
|
|
255
|
+
low.set(v, Math.min(low.get(v), low.get(w)));
|
|
256
|
+
} else if (onStack.has(w)) low.set(v, Math.min(low.get(v), index.get(w)));
|
|
257
|
+
if (low.get(v) === index.get(v)) {
|
|
258
|
+
const component = [];
|
|
259
|
+
for (let w = stack.pop(); w !== void 0; w = stack.pop()) {
|
|
260
|
+
onStack.delete(w);
|
|
261
|
+
component.push(w);
|
|
262
|
+
if (w === v) break;
|
|
263
|
+
}
|
|
264
|
+
components.push(component.sort());
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
for (const n of names) if (!index.has(n)) visit(n);
|
|
268
|
+
const componentOf = /* @__PURE__ */ new Map();
|
|
269
|
+
components.forEach((c, i) => {
|
|
270
|
+
for (const n of c) componentOf.set(n, i);
|
|
271
|
+
});
|
|
272
|
+
const depth = components.map(() => 0);
|
|
273
|
+
components.forEach((c, i) => {
|
|
274
|
+
for (const n of c) for (const dep of edges(n)) {
|
|
275
|
+
const j = componentOf.get(dep);
|
|
276
|
+
if (j !== i) depth[i] = Math.max(depth[i], depth[j] + 1);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
const layers = [];
|
|
280
|
+
components.forEach((c, i) => {
|
|
281
|
+
const d = depth[i];
|
|
282
|
+
const layer = layers[d] ?? [];
|
|
283
|
+
layer.push(...c);
|
|
284
|
+
layers[d] = layer;
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
cycles: components.filter((c) => c.length > 1),
|
|
288
|
+
layers: layers.filter(Boolean).map((layer) => layer.sort())
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/factory/module/resolve.ts
|
|
293
|
+
function isLazyBridge(input) {
|
|
294
|
+
return typeof input === "object" && input !== null && !(input instanceof Promise) && typeof input.load === "function";
|
|
295
|
+
}
|
|
296
|
+
/** Replace each active lazy bridge's declaration with the module it loads. */
|
|
297
|
+
function loadActiveBridges(active, lazyByName) {
|
|
298
|
+
return Promise.all(active.map((m) => {
|
|
299
|
+
const lazy = lazyByName.get(m.name);
|
|
300
|
+
return lazy ? loadLazyBridge(lazy) : m;
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Resolve every input and activate bridges WITHOUT loading a lazy bridge — its declaration
|
|
305
|
+
* stands in for it. The shared first half of `resolveModules` and `planModules`.
|
|
306
|
+
*/
|
|
307
|
+
async function declareModules(inputs) {
|
|
308
|
+
const { active, skipped } = activateBridges(await Promise.all(inputs.map((input) => isLazyBridge(input) ? {
|
|
309
|
+
name: input.name,
|
|
310
|
+
kind: "bridge",
|
|
311
|
+
dependsOn: input.dependsOn,
|
|
312
|
+
...input.requires ? { requires: input.requires } : {}
|
|
313
|
+
} : resolveModule(input))));
|
|
314
|
+
return {
|
|
315
|
+
active,
|
|
316
|
+
skipped,
|
|
317
|
+
lazyByName: new Map(inputs.filter(isLazyBridge).map((b) => [b.name, b]))
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/** The loaded module must be the bridge that was declared — activation was decided on the declaration. */
|
|
321
|
+
async function loadLazyBridge(lazy) {
|
|
322
|
+
const m = await resolveModule(lazy.load);
|
|
323
|
+
const deps = [...m.dependsOn ?? []].sort().join(",");
|
|
324
|
+
if (m.name !== lazy.name || m.kind !== "bridge" || deps !== [...lazy.dependsOn].sort().join(",") || portsKey(m.requires) !== portsKey(lazy.requires)) throw new Error(`[arc] lazy bridge "${lazy.name}" loaded a module that does not match its declaration (got name "${m.name}", kind "${m.kind ?? "domain"}", dependsOn [${deps}], requires ${portsKey(m.requires)}). Activation and the requirement check were decided on the declared name + dependsOn + requires, so they must be identical.`);
|
|
325
|
+
return m;
|
|
326
|
+
}
|
|
327
|
+
/** A stable fingerprint of a `requires` declaration, for the declared-vs-loaded comparison. */
|
|
328
|
+
function portsKey(requires) {
|
|
329
|
+
const list = (names) => [...names ?? []].sort().join(",");
|
|
330
|
+
return `ports[${list(requires?.ports)}] optional[${list(requires?.optionalPorts)}] modules[${list(requires?.modules)}]`;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
|
|
334
|
+
* (this is where a dynamic `import()` fires); a promise (or plain module) is
|
|
335
|
+
* awaited. Called once per module at the start of the bootstrap phase.
|
|
336
|
+
*/
|
|
337
|
+
async function resolveModule(input) {
|
|
338
|
+
if (isLazyBridge(input)) return loadLazyBridge(input);
|
|
339
|
+
const resolved = typeof input === "function" ? await input() : await input;
|
|
340
|
+
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.");
|
|
341
|
+
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.");
|
|
342
|
+
return resolved;
|
|
343
|
+
}
|
|
344
|
+
/** Resolve a `ModuleContribution` to a concrete array (array passthrough or factory call). */
|
|
345
|
+
async function resolveContribution(contribution, fastify) {
|
|
346
|
+
if (!contribution) return [];
|
|
347
|
+
return typeof contribution === "function" ? await contribution(fastify) : contribution;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* `resolveContribution` with module attribution — the form every arm collector
|
|
351
|
+
* should use.
|
|
352
|
+
*
|
|
353
|
+
* A contribution factory closes over booted engines, so it throws for exactly
|
|
354
|
+
* the reasons a bootstrap does ("connection unavailable", "engine not ready").
|
|
355
|
+
* Bare, that message names neither the module nor the arm, which in a graph of
|
|
356
|
+
* twenty modules turns a one-line fix into a bisect. Every other module
|
|
357
|
+
* boundary (`plugins`, `bootstrap`, `resources`, `afterResources`) already
|
|
358
|
+
* wraps with the owning module's name; this keeps the arms consistent with
|
|
359
|
+
* them. The original error is preserved via `{ cause }`.
|
|
360
|
+
*/
|
|
361
|
+
async function resolveModuleArm(module, arm, contribution, fastify) {
|
|
362
|
+
let resolved;
|
|
363
|
+
try {
|
|
364
|
+
resolved = await resolveContribution(contribution, fastify);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
367
|
+
throw new Error(`[arc] module "${module.name}" ${arm} factory threw: ${msg}`, { cause: err });
|
|
368
|
+
}
|
|
369
|
+
if (!Array.isArray(resolved)) throw new TypeError(`[arc] module "${module.name}" ${arm} factory must return an array — received ${resolved === null ? "null" : typeof resolved}.`);
|
|
370
|
+
return resolved;
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/factory/module/order.ts
|
|
374
|
+
/**
|
|
375
|
+
* Order modules for composition — a STABLE topological sort. Every later phase
|
|
376
|
+
* (bootstrap, resources, afterResources, reverse-order onClose) iterates the
|
|
377
|
+
* returned list, so a module's dependencies are always composed before it.
|
|
378
|
+
*
|
|
379
|
+
* Edges: each module's `dependsOn`, plus an implicit edge from every
|
|
380
|
+
* `kind: 'aggregator'` module to every non-aggregator one. Among READY modules a
|
|
381
|
+
* `kind: 'platform'` one is emitted first; otherwise list order breaks ties, so
|
|
382
|
+
* declaring an edge never reorders an unrelated module.
|
|
383
|
+
*
|
|
384
|
+
* Throws on a duplicate name, a `dependsOn` name not composed, a self-reference,
|
|
385
|
+
* or a cycle (reporting the concrete `a → b → … → a` path).
|
|
386
|
+
*/
|
|
387
|
+
function orderModules(modules) {
|
|
388
|
+
const byName = /* @__PURE__ */ new Map();
|
|
389
|
+
for (const m of modules) {
|
|
390
|
+
if (byName.has(m.name)) throw new Error(`[arc] Duplicate module name "${m.name}" — composed twice; check your modules array.`);
|
|
391
|
+
byName.set(m.name, m);
|
|
392
|
+
}
|
|
393
|
+
for (const m of modules) for (const dep of m.dependsOn ?? []) {
|
|
394
|
+
if (dep === m.name) throw new Error(`[arc] module "${m.name}" dependsOn itself — remove the self-reference.`);
|
|
395
|
+
if (!byName.has(dep)) throw new Error(`[arc] module "${m.name}" dependsOn "${dep}", which is not composed. Add the "${dep}" module to createApp({ modules }) (before this one is fine — arc orders them), or drop the dependency. Composed modules: ${[...byName.keys()].join(", ") || "(none)"}.`);
|
|
396
|
+
}
|
|
397
|
+
const edges = effectiveEdges(modules);
|
|
398
|
+
const originalIndex = /* @__PURE__ */ new Map();
|
|
399
|
+
let nextIndex = 0;
|
|
400
|
+
for (const m of modules) originalIndex.set(m.name, nextIndex++);
|
|
401
|
+
const pendingDeps = new Map(modules.map((m) => [m.name, (edges.get(m.name) ?? []).length]));
|
|
402
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
403
|
+
for (const m of modules) for (const dep of edges.get(m.name) ?? []) {
|
|
404
|
+
const list = dependents.get(dep);
|
|
405
|
+
if (list) list.push(m.name);
|
|
406
|
+
else dependents.set(dep, [m.name]);
|
|
407
|
+
}
|
|
408
|
+
const rank = (m) => (m.kind === "platform" ? 0 : modules.length) + (originalIndex.get(m.name) ?? 0);
|
|
409
|
+
const ordered = [];
|
|
410
|
+
const remaining = new Set(modules.map((m) => m.name));
|
|
411
|
+
while (remaining.size > 0) {
|
|
412
|
+
let pick;
|
|
413
|
+
let pickRank = Number.POSITIVE_INFINITY;
|
|
414
|
+
for (const name of remaining) {
|
|
415
|
+
if (pendingDeps.get(name) !== 0) continue;
|
|
416
|
+
const candidate = byName.get(name);
|
|
417
|
+
if (!candidate) continue;
|
|
418
|
+
const r = rank(candidate);
|
|
419
|
+
if (r < pickRank) {
|
|
420
|
+
pick = candidate;
|
|
421
|
+
pickRank = r;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (pick === void 0) throw new Error(describeModuleCycle(remaining, edges));
|
|
425
|
+
ordered.push(pick);
|
|
426
|
+
remaining.delete(pick.name);
|
|
427
|
+
for (const dependent of dependents.get(pick.name) ?? []) pendingDeps.set(dependent, (pendingDeps.get(dependent) ?? 1) - 1);
|
|
428
|
+
}
|
|
429
|
+
return ordered;
|
|
430
|
+
}
|
|
431
|
+
/** Declared `dependsOn` plus each aggregator's implicit edge to every non-aggregator module. */
|
|
432
|
+
function effectiveEdges(modules) {
|
|
433
|
+
const fleet = modules.filter((m) => m.kind !== "aggregator").map((m) => m.name);
|
|
434
|
+
return new Map(modules.map((m) => [m.name, m.kind === "aggregator" ? [.../* @__PURE__ */ new Set([...m.dependsOn ?? [], ...fleet])] : [...m.dependsOn ?? []]]));
|
|
435
|
+
}
|
|
436
|
+
/** Walk the still-unordered subgraph for one concrete cycle path. */
|
|
437
|
+
function describeModuleCycle(remaining, edges) {
|
|
438
|
+
const stack = [];
|
|
439
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
440
|
+
const done = /* @__PURE__ */ new Set();
|
|
441
|
+
let cycle = null;
|
|
442
|
+
const walk = (name) => {
|
|
443
|
+
if (cycle) return;
|
|
444
|
+
stack.push(name);
|
|
445
|
+
onStack.add(name);
|
|
446
|
+
for (const dep of edges.get(name) ?? []) {
|
|
447
|
+
if (!remaining.has(dep)) continue;
|
|
448
|
+
if (onStack.has(dep)) {
|
|
449
|
+
cycle = [...stack.slice(stack.indexOf(dep)), dep];
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!done.has(dep)) walk(dep);
|
|
453
|
+
if (cycle) return;
|
|
454
|
+
}
|
|
455
|
+
onStack.delete(name);
|
|
456
|
+
stack.pop();
|
|
457
|
+
done.add(name);
|
|
458
|
+
};
|
|
459
|
+
for (const name of remaining) {
|
|
460
|
+
if (!done.has(name)) walk(name);
|
|
461
|
+
if (cycle) break;
|
|
462
|
+
}
|
|
463
|
+
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, declare the softer direction as \`requires.modules\` (presence only, no ordering edge), or wire it through an event/port. A module that dependsOn a \`kind: 'aggregator'\` module is always a cycle.`;
|
|
464
|
+
}
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region src/factory/module/plan.ts
|
|
467
|
+
/** A port counts as supplied when it is an own key whose value is neither `undefined` nor `null`. */
|
|
468
|
+
function isSupplied(ports, name) {
|
|
469
|
+
return Object.hasOwn(ports, name) && ports[name] !== void 0 && ports[name] !== null;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Every problem in an ACTIVE module list (bridges already activated): duplicate names,
|
|
473
|
+
* self/missing `dependsOn`, unmet `requires.modules`, unsupplied `requires.ports`. Cycles are
|
|
474
|
+
* `orderModules`' to report, with their path.
|
|
475
|
+
*/
|
|
476
|
+
function compositionProblems(modules, ports = {}) {
|
|
477
|
+
const supplied = ports;
|
|
478
|
+
const composed = /* @__PURE__ */ new Set();
|
|
479
|
+
const problems = [];
|
|
480
|
+
const add = (code, module, target, message) => problems.push({
|
|
481
|
+
code,
|
|
482
|
+
module,
|
|
483
|
+
target,
|
|
484
|
+
message
|
|
485
|
+
});
|
|
486
|
+
for (const m of modules) {
|
|
487
|
+
if (composed.has(m.name)) add("duplicate-module", m.name, m.name, `Duplicate module name "${m.name}" — it is composed twice; remove the duplicate from createApp({ modules })`);
|
|
488
|
+
composed.add(m.name);
|
|
489
|
+
}
|
|
490
|
+
for (const m of modules) {
|
|
491
|
+
for (const dep of m.dependsOn ?? []) if (dep === m.name) add("self-dependency", m.name, dep, `module "${m.name}" dependsOn itself — remove the self-reference`);
|
|
492
|
+
else if (!composed.has(dep)) add("missing-dependency", m.name, dep, `module "${m.name}" dependsOn "${dep}", which is not composed — add the "${dep}" module to createApp({ modules }), or drop the dependency`);
|
|
493
|
+
for (const dep of m.requires?.modules ?? []) if (!composed.has(dep)) add("missing-module", m.name, dep, `module "${m.name}" requires "${dep}" to be composed — add the "${dep}" module to createApp({ modules })`);
|
|
494
|
+
for (const port of m.requires?.ports ?? []) if (!isSupplied(supplied, port)) add("missing-port", m.name, port, `module "${m.name}" requires port "${port}" — supply createApp({ ports: { ${port} } })`);
|
|
495
|
+
}
|
|
496
|
+
return problems;
|
|
497
|
+
}
|
|
498
|
+
/** Throw ONE error listing every composition problem, or return. */
|
|
499
|
+
function assertComposable(modules, ports) {
|
|
500
|
+
const problems = compositionProblems(modules, ports);
|
|
501
|
+
if (problems.length === 0) return;
|
|
502
|
+
throw new Error(`[arc] cannot compose the module graph — nothing was booted (${problems.length} problem${problems.length === 1 ? "" : "s"}):\n` + problems.map((p) => ` - ${p.message}`).join("\n"));
|
|
503
|
+
}
|
|
504
|
+
/** The ports handed to one module's setup phases — only those it declared, frozen. */
|
|
505
|
+
function portsForModule(module, ports = {}) {
|
|
506
|
+
const supplied = ports;
|
|
507
|
+
const own = {};
|
|
508
|
+
for (const name of [...module.requires?.ports ?? [], ...module.requires?.optionalPorts ?? []]) if (isSupplied(supplied, name)) own[name] = supplied[name];
|
|
509
|
+
return Object.freeze(own);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Dry-run a composition: resolve inputs, activate bridges, order, check requirements — the same
|
|
513
|
+
* functions `createApp` calls, so the plan fails where boot would and orders as boot would.
|
|
514
|
+
*
|
|
515
|
+
* Thunks ARE invoked (their result is the module); a `LazyBridge` is decided on its declaration
|
|
516
|
+
* and never loaded.
|
|
517
|
+
*/
|
|
518
|
+
async function planModules(inputs, options = {}) {
|
|
519
|
+
const { active, skipped } = await declareModules(inputs);
|
|
520
|
+
assertComposable(active, options.ports);
|
|
521
|
+
const ordered = orderModules(active);
|
|
522
|
+
return Object.freeze({
|
|
523
|
+
order: Object.freeze(ordered.map((m) => m.name)),
|
|
524
|
+
activeBridges: Object.freeze(ordered.filter((m) => m.kind === "bridge").map((m) => m.name)),
|
|
525
|
+
skippedBridges: Object.freeze([...skipped])
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
/** Boot-time composition: `planModules`' steps, with active lazy bridges loaded. */
|
|
529
|
+
async function composeModules(inputs, ports) {
|
|
530
|
+
const { active, skipped, lazyByName } = await declareModules(inputs);
|
|
531
|
+
assertComposable(active, ports);
|
|
532
|
+
return {
|
|
533
|
+
modules: orderModules(await loadActiveBridges(active, lazyByName)),
|
|
534
|
+
skipped
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* The modules `roots` need to boot: their `dependsOn` and `requires.modules` closure, plus every
|
|
539
|
+
* `kind: 'platform'` module (and its own closure). Returned in INPUT order, so ordering the result
|
|
540
|
+
* matches ordering the full list.
|
|
541
|
+
*
|
|
542
|
+
* Bridges are not pulled in — a bridge joins domains, it is not needed by them. An aggregator
|
|
543
|
+
* root brings only its declared edges: it reads whatever is composed.
|
|
544
|
+
*
|
|
545
|
+
* Throws on an unknown root, or an edge to a module that is not in `modules`.
|
|
546
|
+
*/
|
|
547
|
+
function resolveModuleClosure(modules, roots) {
|
|
548
|
+
const byName = new Map(modules.map((m) => [m.name, m]));
|
|
549
|
+
const unknown = roots.filter((r) => !byName.has(r));
|
|
550
|
+
if (unknown.length > 0) throw new Error(`[arc] resolveModuleClosure: unknown root module(s) ${unknown.map((r) => `"${r}"`).join(", ")}. Modules: ${[...byName.keys()].join(", ") || "(none)"}.`);
|
|
551
|
+
const included = /* @__PURE__ */ new Set();
|
|
552
|
+
const visit = (name, from) => {
|
|
553
|
+
if (included.has(name)) return;
|
|
554
|
+
const m = byName.get(name);
|
|
555
|
+
if (!m) throw new Error(`[arc] resolveModuleClosure: module "${from}" needs "${name}", which is not in the module list.`);
|
|
556
|
+
included.add(name);
|
|
557
|
+
for (const dep of m.dependsOn ?? []) visit(dep, name);
|
|
558
|
+
for (const dep of m.requires?.modules ?? []) visit(dep, name);
|
|
559
|
+
};
|
|
560
|
+
for (const root of roots) visit(root, void 0);
|
|
561
|
+
for (const m of modules) if (m.kind === "platform") visit(m.name, void 0);
|
|
562
|
+
return modules.filter((m) => included.has(m.name));
|
|
563
|
+
}
|
|
564
|
+
//#endregion
|
|
565
|
+
export { dependentsOf as _, portsForModule as a, verifyEventDurability as b, declareModules as c, resolveModule as d, resolveModuleArm as f, crudEventsOf as g, buildModuleGraph as h, planModules as i, isLazyBridge as l, analyzeModuleGraph as m, composeModules as n, resolveModuleClosure as o, activateBridges as p, compositionProblems as r, orderModules as s, assertComposable as t, resolveContribution as u, eventTopology as v, verifyModuleGraph as x, renderModuleGraph as y };
|