@classytic/arc 2.30.0 → 2.32.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-B5DwufbB.mjs → BaseController-Dk34dxhI.mjs} +21 -19
- package/dist/{EventTransport-QmY7A258.d.mts → EventTransport-2uVV34WU.d.mts} +1 -0
- package/dist/{EventTransport-C6YA8xY9.mjs → EventTransport-BV10D0oG.mjs} +1 -0
- package/dist/{HookSystem-WkIougo6.mjs → HookSystem-vimw5D7Z.mjs} +2 -1
- package/dist/{QueryCache-DOMT_iq7.d.mts → QueryCache-DFA-QrU9.d.mts} +2 -1
- package/dist/{ResourceRegistry-y9Hnypus.mjs → ResourceRegistry-BVr7PgA0.mjs} +18 -15
- package/dist/{actionPermissions-CAphcmUG.mjs → actionPermissions-Dti7ebyv.mjs} +1 -0
- package/dist/audit/index.d.mts +3 -2
- package/dist/audit/index.mjs +2 -1
- 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 +11 -9
- package/dist/auth/redis-session.d.mts +2 -1
- package/dist/auth/redis-session.mjs +1 -0
- package/dist/{applyPermissionResult-Cf6zUCvb.mjs → authorizationDecision-__T3EJDA.mjs} +19 -10
- package/dist/{buildHandler-BYS3Vkfz.mjs → buildHandler-Dn7rzUJo.mjs} +6 -4
- package/dist/cache/index.d.mts +4 -3
- package/dist/cache/index.mjs +4 -3
- package/dist/{caching-_NJeDd1_.mjs → caching-DFJqHyFs.mjs} +2 -1
- package/dist/cleanup/index.d.mts +39 -16
- package/dist/cleanup/index.mjs +50 -38
- package/dist/cli/commands/describe.d.mts +2 -1
- package/dist/cli/commands/describe.mjs +4 -5
- 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-J99LGTJI.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 -6
- package/dist/{core-poalJLvp.mjs → core-BwwmNSp7.mjs} +3 -2
- package/dist/{createActionRouter-BxuywVRB.mjs → createActionRouter-DkJZWo0m.mjs} +14 -10
- package/dist/{createAggregationRouter-BiY-lGdY.mjs → createAggregationRouter-B1exOVD8.mjs} +10 -7
- package/dist/{createApp-CXrKFciH.mjs → createApp-Bv0m-Q6_.mjs} +625 -229
- package/dist/{defineAggregation-ufWjUwml.mjs → defineAggregation-gX4No1gt.mjs} +1 -0
- package/dist/{defineResource-CxWqDCAy.mjs → defineResource-XFPPUu2c.mjs} +84 -50
- 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-DNXXxgjl.mjs} +4 -3
- package/dist/encryption/index.d.mts +2 -1
- package/dist/encryption/index.mjs +5 -3
- package/dist/{errorHandler-Bq2OeEQL.mjs → errorHandler-B2AZUJUj.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-Bh1KK5ps.mjs} +1 -0
- package/dist/{eventPlugin-D2NNR4py.mjs → eventPlugin-Dk_TyXeE.mjs} +8 -6
- package/dist/{eventPlugin-Bg3cP2_u.d.mts → eventPlugin-GF5nnBIq.d.mts} +2 -1
- package/dist/events/index.d.mts +112 -44
- package/dist/events/index.mjs +177 -141
- package/dist/events/transports/redis-stream-entry.d.mts +2 -1
- package/dist/events/transports/redis-stream-entry.mjs +14 -9
- 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 +131 -7
- package/dist/fastify-augmentation-BppG5bMT.d.mts +179 -0
- package/dist/{fields-CQ_7KQWw.mjs → fields-DEi65pGj.mjs} +6 -10
- package/dist/{fields-D3lhgzhD.d.mts → fields-I_EeRWND.d.mts} +1 -0
- package/dist/{generate-D0wZupBg.mjs → generate-CRSG0jTv.mjs} +2 -1
- package/dist/{generate-BnsL9GBm.d.mts → generate-DCGuEbqI.d.mts} +1 -0
- package/dist/{headers-BvlQSDMB.mjs → headers-BXjQYbeC.mjs} +2 -1
- package/dist/{health-CB_m4S32.mjs → health-5XnTa2uo.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 +8 -5
- package/dist/idempotency/redis.d.mts +2 -1
- package/dist/idempotency/redis.mjs +1 -0
- package/dist/{index-CG4yqMDB.d.mts → index-C-481Zf2.d.mts} +74 -18
- package/dist/{index-Q5gzEIWY.d.mts → index-CpzxqePp.d.mts} +107 -32
- package/dist/{index-Cx7bro4R.d.mts → index-DXlHwvuE.d.mts} +5 -4
- package/dist/index-ZWhLTp_s.d.mts +290 -0
- package/dist/{index-Ch16aAah.d.mts → index-zq6VrjaB.d.mts} +122 -15
- package/dist/index.d.mts +9 -8
- package/dist/index.mjs +11 -10
- package/dist/{init-tlRSt667.mjs → init-BbfP8aky.mjs} +25 -27
- 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 +109 -33
- package/dist/integrations/mcp/index.d.mts +6 -6
- 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 +38 -34
- package/dist/{interface-BGZCzOGL.d.mts → interface-BOk3YnTz.d.mts} +1 -0
- package/dist/{interface-n5hpKg1d.mjs → interface-BP4IJCBO.mjs} +1 -0
- package/dist/{interface-Uk8ONJv7.d.mts → interface-DEWNOMza.d.mts} +1 -0
- package/dist/{jsonBody-BlzAoGCe.mjs → jsonBody-DDSR2HFl.mjs} +2 -1
- package/dist/{keys-Cd316u_y.mjs → keys-B-HBTGXl.mjs} +1 -0
- package/dist/{loadResources-DpU8pfjV.mjs → loadResources-DisEAmkm.mjs} +4 -2
- package/dist/{loadResourcesFromEntry-kvGT6EcO.mjs → loadResourcesFromEntry-D1MJp13f.mjs} +2 -1
- package/dist/logger/index.d.mts +1 -0
- package/dist/logger/index.mjs +1 -0
- package/dist/{mcpPlugin-nRzm5g2s.mjs → mcpPlugin-CpfVpKHE.mjs} +38 -38
- package/dist/{memory-CmnzXE5Z.mjs → memory-DsuWnj4T.mjs} +5 -4
- package/dist/{metrics-fdxI5mn_.mjs → metrics-DdQL8syd.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 -0
- package/dist/migrations/index.mjs +3 -2
- package/dist/module-Br81am6m.mjs +78 -0
- package/dist/{multipartBody-D0eBvkeq.mjs → multipartBody-BnkBLN7v.mjs} +3 -2
- package/dist/{openapi-B8Vkbxbn.mjs → openapi-DIRzvxC0.mjs} +6 -5
- package/dist/outbox-admin/index.d.mts +85 -0
- package/dist/outbox-admin/index.mjs +115 -0
- package/dist/permissions/index.d.mts +5 -4
- package/dist/permissions/index.mjs +6 -5
- package/dist/{permissions-Bfu_8y1U.mjs → permissions-Dl_m0NCe.mjs} +308 -42
- package/dist/{pipe-BOLmGIxa.mjs → pipe-ELHCP3zj.mjs} +3 -4
- package/dist/pipeline/index.d.mts +2 -1
- package/dist/pipeline/index.mjs +2 -1
- package/dist/{pipeline-BJnpkY9i.mjs → pipeline-CPA-jx0M.mjs} +17 -14
- 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-C2ZkKbsn.mjs} +1 -0
- package/dist/{preloadResources-DSY5HGpS.mjs → preloadResources-CDs87fTw.mjs} +1 -0
- package/dist/{preloadResources-DCfa2egz.d.mts → preloadResources-T1hmlTEZ.d.mts} +117 -12
- package/dist/presets/filesUpload.d.mts +6 -5
- package/dist/presets/filesUpload.mjs +10 -12
- package/dist/presets/index.d.mts +3 -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-CJOEXLuk.mjs → presets-Dntd_pUO.mjs} +6 -4
- package/dist/{projection-UBDgfMwu.mjs → projection-CjHhM5W1.mjs} +2 -1
- package/dist/{pushref-registry-BbcnpNTm.d.mts → pushref-registry-Dov9l4qE.d.mts} +1 -0
- package/dist/{queryCachePlugin-br4otiqF.d.mts → queryCachePlugin-BzqxconG.d.mts} +3 -2
- package/dist/{queryCachePlugin-DSGuSVLW.mjs → queryCachePlugin-Dhps-Ldt.mjs} +6 -5
- package/dist/{rateLimit-DrED-C4v.mjs → rateLimit-C7mmAGs3.mjs} +7 -3
- package/dist/{rateLimitKey-BoWRH3e7.mjs → rateLimitKey-CccEFBfk.mjs} +1 -0
- package/dist/{realtime-B9nmIY5V.mjs → realtime-DVvVZZFz.mjs} +20 -9
- 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-JkxIEBJq.mjs → registerRoute-B_QrjYZS.mjs} +10 -7
- package/dist/registry/index.d.mts +3 -2
- package/dist/registry/index.mjs +3 -2
- package/dist/{registry-BYXUUg21.mjs → registry-DqUmzA9W.mjs} +2 -1
- package/dist/{renewingLease-3cuQH7h8.mjs → renewingLease-BpP9wwj7.mjs} +1 -0
- package/dist/{replyHelpers-67tRawQZ.mjs → replyHelpers-DS-P0b32.mjs} +2 -1
- package/dist/{repositoryFilter-Dz8k3S4P.mjs → repositoryFilter-DotlxG3L.mjs} +1 -0
- package/dist/{requestContext-MrlJwECf.mjs → requestContext-B6vQF-sy.mjs} +1 -0
- package/dist/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-BhflRrBs.mjs} +1 -0
- package/dist/{schedules-CjOd2MG9.mjs → schedules-C2Q3uv2d.mjs} +3 -2
- package/dist/{schemaConverter-DJvodiHj.mjs → schemaConverter-Crjvczi_.mjs} +70 -17
- package/dist/{schemaIR-Ca7ySNPx.mjs → schemaIR-CnyKK_1j.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-afz_Ieek.mjs} +4 -3
- package/dist/{sessionManager-BHOM3N_0.d.mts → sessionManager-qimlKV0P.d.mts} +1 -0
- package/dist/{simpleEqualityMatcher-Cg05HPsn.mjs → simpleEqualityMatcher-C4KiMWUa.mjs} +1 -0
- package/dist/{sse-BQ1w0dfl.mjs → sse-B9P4Y-UI.mjs} +7 -5
- package/dist/{sseStream-BQ7Qaynp.mjs → sseStream-B3zb_d1h.mjs} +2 -1
- package/dist/{storage-DLUZ04kK.d.mts → storage-Cx9Sz3Ou.d.mts} +1 -0
- package/dist/{store-helpers-DY6xq4sM.mjs → store-helpers-DmEMHlPQ.mjs} +1 -0
- package/dist/{streaming-BQcW6_Kq.mjs → streaming-DPZdQmDz.mjs} +1 -0
- package/dist/{subscribe-helpers-DfzsivM6.mjs → subscribe-helpers-sp5xTDVA.mjs} +2 -1
- 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 +10 -5
- package/dist/testing/index.d.mts +166 -12
- package/dist/testing/index.mjs +315 -43
- package/dist/testing/outboxStoreContract.d.mts +12 -2
- package/dist/testing/outboxStoreContract.mjs +110 -20
- package/dist/testing/storageContract.d.mts +2 -1
- package/dist/testing/storageContract.mjs +11 -5
- package/dist/{tracing-CsGKQJGg.d.mts → tracing-Dro5eH6j.d.mts} +1 -0
- package/dist/{transactionContext-PNdoyLLm.mjs → transactionContext-BvLKvOIL.mjs} +1 -0
- package/dist/{typeGuards-C9KXUly6.mjs → typeGuards-B_5-hKjJ.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-ChvvVNP8.d.mts → types-B7XeLEgK.d.mts} +34 -2
- 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-D_n8L6VY.d.mts → types-CT5vu6M0.d.mts} +132 -17
- package/dist/{types-DjC-KUCz.d.mts → types-ChuShpCP.d.mts} +1 -0
- package/dist/{types-Bmmn-fO0.d.mts → types-CwcUQ610.d.mts} +2 -6
- package/dist/{types-CSyCxl2V.mjs → types-DXYX7dIR.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-BNsOXzOd.mjs} +1 -0
- package/dist/utils/index.d.mts +16 -7
- package/dist/utils/index.mjs +10 -9
- package/dist/{utils-taUVD4p3.mjs → utils-BtOha_Sp.mjs} +3 -2
- package/dist/{validate-Bk44F2yT.mjs → validate-PMCPR9El.mjs} +2 -1
- package/dist/{versioning-gKWyryT3.mjs → versioning-CKv2jz_U.mjs} +4 -3
- 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 +557 -539
- 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/fastify-augmentation-0WhPnUH7.d.mts +0 -76
- package/dist/index-CCzVR-N3.d.mts +0 -107
- package/dist/module-_-M7C5ES.mjs +0 -29
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
/*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
|
|
1
2
|
import { OutboxOwnershipError } from "@classytic/primitives/outbox";
|
|
2
|
-
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
3
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
4
|
//#region src/testing/outboxStoreContract.ts
|
|
4
5
|
let seq = 0;
|
|
5
6
|
/** A minimal well-formed event. Ids are unique per call. */
|
|
@@ -29,9 +30,16 @@ function runOutboxStoreContract(name, setup) {
|
|
|
29
30
|
afterAll(async () => {
|
|
30
31
|
if (teardown) await teardown();
|
|
31
32
|
});
|
|
33
|
+
/**
|
|
34
|
+
* Isolation for EVERY test, not just the ones that call `seed()`. Resetting inside
|
|
35
|
+
* `seed()` left any test that asserts on an empty store — or that only reads —
|
|
36
|
+
* running against its predecessor's rows.
|
|
37
|
+
*/
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
await reset();
|
|
40
|
+
});
|
|
32
41
|
/** Save n events, oldest first. */
|
|
33
42
|
async function seed(n, type) {
|
|
34
|
-
if (reset) await reset();
|
|
35
43
|
const events = [];
|
|
36
44
|
for (let i = 0; i < n; i++) {
|
|
37
45
|
const e = event(type);
|
|
@@ -43,10 +51,10 @@ function runOutboxStoreContract(name, setup) {
|
|
|
43
51
|
describe("save — rejects malformed events (invariant 1)", () => {
|
|
44
52
|
it("persists a well-formed event", async () => {
|
|
45
53
|
const [e] = await seed(1);
|
|
46
|
-
|
|
54
|
+
const pending = await store.getPending(10);
|
|
55
|
+
expect(pending.map(idOf)).toContain(idOf(e));
|
|
47
56
|
});
|
|
48
57
|
it("THROWS rather than persisting an event with no `type`", async () => {
|
|
49
|
-
if (reset) await reset();
|
|
50
58
|
await expect(store.save({
|
|
51
59
|
payload: {},
|
|
52
60
|
meta: { id: "no-type" }
|
|
@@ -54,7 +62,6 @@ function runOutboxStoreContract(name, setup) {
|
|
|
54
62
|
expect((await store.getPending(10)).length).toBe(0);
|
|
55
63
|
});
|
|
56
64
|
it("THROWS rather than persisting an event with no `meta.id`", async () => {
|
|
57
|
-
if (reset) await reset();
|
|
58
65
|
await expect(store.save({
|
|
59
66
|
type: "x",
|
|
60
67
|
payload: {},
|
|
@@ -66,7 +73,8 @@ function runOutboxStoreContract(name, setup) {
|
|
|
66
73
|
describe("getPending", () => {
|
|
67
74
|
it("returns events FIFO (oldest first)", async () => {
|
|
68
75
|
const saved = await seed(3);
|
|
69
|
-
|
|
76
|
+
const pending = await store.getPending(10);
|
|
77
|
+
expect(pending.slice(0, 3).map(idOf)).toEqual(saved.map(idOf));
|
|
70
78
|
});
|
|
71
79
|
it("honours the limit", async () => {
|
|
72
80
|
await seed(3);
|
|
@@ -85,10 +93,11 @@ function runOutboxStoreContract(name, setup) {
|
|
|
85
93
|
it("claims pending events", async ({ skip }) => {
|
|
86
94
|
if (!store.claimPending) return skip();
|
|
87
95
|
await seed(2);
|
|
88
|
-
|
|
96
|
+
const claimed = await store.claimPending({
|
|
89
97
|
consumerId: "c1",
|
|
90
98
|
limit: 10
|
|
91
|
-
})
|
|
99
|
+
});
|
|
100
|
+
expect(claimed.length).toBe(2);
|
|
92
101
|
});
|
|
93
102
|
it("two concurrent claimers NEVER receive the same event", async ({ skip }) => {
|
|
94
103
|
if (!store.claimPending) return skip();
|
|
@@ -102,7 +111,8 @@ function runOutboxStoreContract(name, setup) {
|
|
|
102
111
|
})]);
|
|
103
112
|
const idsA = a.map(idOf);
|
|
104
113
|
const idsB = b.map(idOf);
|
|
105
|
-
|
|
114
|
+
const overlap = idsA.filter((id) => idsB.includes(id));
|
|
115
|
+
expect(overlap).toEqual([]);
|
|
106
116
|
expect(idsA.length + idsB.length).toBeLessThanOrEqual(6);
|
|
107
117
|
});
|
|
108
118
|
it("does not re-claim an event whose lease is still live", async ({ skip }) => {
|
|
@@ -113,10 +123,11 @@ function runOutboxStoreContract(name, setup) {
|
|
|
113
123
|
limit: 10,
|
|
114
124
|
leaseMs: 6e4
|
|
115
125
|
});
|
|
116
|
-
|
|
126
|
+
const second = await store.claimPending({
|
|
117
127
|
consumerId: "c2",
|
|
118
128
|
limit: 10
|
|
119
|
-
})
|
|
129
|
+
});
|
|
130
|
+
expect(second).toEqual([]);
|
|
120
131
|
});
|
|
121
132
|
it("RECLAIMS an event whose lease expired (crashed worker)", async ({ skip }) => {
|
|
122
133
|
if (!store.claimPending) return skip();
|
|
@@ -126,18 +137,20 @@ function runOutboxStoreContract(name, setup) {
|
|
|
126
137
|
limit: 10,
|
|
127
138
|
leaseMs: 0
|
|
128
139
|
});
|
|
129
|
-
|
|
140
|
+
const reclaimed = await store.claimPending({
|
|
130
141
|
consumerId: "c2",
|
|
131
142
|
limit: 10
|
|
132
|
-
})
|
|
143
|
+
});
|
|
144
|
+
expect(reclaimed.length).toBe(1);
|
|
133
145
|
});
|
|
134
146
|
it("claims FIFO", async ({ skip }) => {
|
|
135
147
|
if (!store.claimPending) return skip();
|
|
136
148
|
const saved = await seed(3);
|
|
137
|
-
|
|
149
|
+
const claimed = await store.claimPending({
|
|
138
150
|
consumerId: "c1",
|
|
139
151
|
limit: 2
|
|
140
|
-
})
|
|
152
|
+
});
|
|
153
|
+
expect(claimed.map(idOf)).toEqual(saved.slice(0, 2).map(idOf));
|
|
141
154
|
});
|
|
142
155
|
});
|
|
143
156
|
describe("acknowledge", () => {
|
|
@@ -147,7 +160,6 @@ function runOutboxStoreContract(name, setup) {
|
|
|
147
160
|
expect((await store.getPending(10)).map(idOf)).not.toContain(idOf(e));
|
|
148
161
|
});
|
|
149
162
|
it("is a NO-OP for an unknown id — never an ownership error (invariant 4)", async () => {
|
|
150
|
-
if (reset) await reset();
|
|
151
163
|
await expect(store.acknowledge("does-not-exist")).resolves.not.toThrow();
|
|
152
164
|
});
|
|
153
165
|
it("THROWS OutboxOwnershipError when consumerId is not the lease owner (invariant 3)", async ({ skip }) => {
|
|
@@ -184,10 +196,11 @@ function runOutboxStoreContract(name, setup) {
|
|
|
184
196
|
});
|
|
185
197
|
if (!claimed) return skip();
|
|
186
198
|
await store.fail(idOf(claimed), { message: "boom" });
|
|
187
|
-
|
|
199
|
+
const again = await store.claimPending({
|
|
188
200
|
consumerId: "c2",
|
|
189
201
|
limit: 10
|
|
190
|
-
})
|
|
202
|
+
});
|
|
203
|
+
expect(again.map(idOf)).toContain(idOf(claimed));
|
|
191
204
|
});
|
|
192
205
|
it("THROWS OutboxOwnershipError on a consumerId mismatch (invariant 3)", async ({ skip }) => {
|
|
193
206
|
if (!store.fail || !store.claimPending) return skip();
|
|
@@ -210,10 +223,11 @@ function runOutboxStoreContract(name, setup) {
|
|
|
210
223
|
});
|
|
211
224
|
if (!claimed) return skip();
|
|
212
225
|
await store.fail(idOf(claimed), { message: "boom" }, { retryAt: new Date(Date.now() + 6e4) });
|
|
213
|
-
|
|
226
|
+
const again = await store.claimPending({
|
|
214
227
|
consumerId: "c2",
|
|
215
228
|
limit: 10
|
|
216
|
-
})
|
|
229
|
+
});
|
|
230
|
+
expect(again.map(idOf)).not.toContain(idOf(claimed));
|
|
217
231
|
});
|
|
218
232
|
it("`deadLetter: true` removes it from the pending stream", async ({ skip }) => {
|
|
219
233
|
if (!store.fail) return skip();
|
|
@@ -241,6 +255,82 @@ function runOutboxStoreContract(name, setup) {
|
|
|
241
255
|
expect((await store.getPending(10)).length).toBe(2);
|
|
242
256
|
});
|
|
243
257
|
});
|
|
258
|
+
/**
|
|
259
|
+
* `requeue`, `countByStatus` and `oldestPendingAgeMs` were a HOST's three extra
|
|
260
|
+
* methods before they were contract members — which is why they are pinned here.
|
|
261
|
+
* A dead-letter row an operator cannot re-drive, or a health number that lies, fails
|
|
262
|
+
* silently: the operator concludes the queue is fine, and the whole point of the
|
|
263
|
+
* table is that somebody eventually looks.
|
|
264
|
+
*/
|
|
265
|
+
describe("requeue", () => {
|
|
266
|
+
it("returns a dead-lettered event to pending and clears the attempt count", async ({ skip }) => {
|
|
267
|
+
if (!store.fail || !store.requeue) return skip();
|
|
268
|
+
const [e] = await seed(1);
|
|
269
|
+
const id = idOf(e);
|
|
270
|
+
await store.fail(id, { message: "fatal" }, { deadLetter: true });
|
|
271
|
+
expect((await store.getPending(10)).length).toBe(0);
|
|
272
|
+
expect(await store.requeue(id)).toBe(true);
|
|
273
|
+
const pending = await store.getPending(10);
|
|
274
|
+
expect(pending.length).toBe(1);
|
|
275
|
+
if (store.countByStatus) expect(await store.countByStatus("dead_letter")).toBe(0);
|
|
276
|
+
});
|
|
277
|
+
it("REFUSES a pending event — requeue is dead-letter-scoped", async ({ skip }) => {
|
|
278
|
+
if (!store.requeue) return skip();
|
|
279
|
+
const [e] = await seed(1);
|
|
280
|
+
expect(await store.requeue(idOf(e))).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
it("is FALSE, not an error, for an unknown id", async ({ skip }) => {
|
|
283
|
+
if (!store.requeue) return skip();
|
|
284
|
+
expect(await store.requeue("no-such-event")).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
describe("countByStatus", () => {
|
|
288
|
+
it("counts pending, and moves the count as events are delivered", async ({ skip }) => {
|
|
289
|
+
if (!store.countByStatus) return skip();
|
|
290
|
+
await seed(3);
|
|
291
|
+
expect(await store.countByStatus("pending")).toBe(3);
|
|
292
|
+
if (!store.claimPending) return skip();
|
|
293
|
+
const [claimed] = await store.claimPending({
|
|
294
|
+
consumerId: "c",
|
|
295
|
+
limit: 1,
|
|
296
|
+
leaseMs: 6e4
|
|
297
|
+
});
|
|
298
|
+
if (!claimed) return skip();
|
|
299
|
+
await store.acknowledge(idOf(claimed), { consumerId: "c" });
|
|
300
|
+
expect(await store.countByStatus("pending")).toBe(2);
|
|
301
|
+
expect(await store.countByStatus("delivered")).toBe(1);
|
|
302
|
+
});
|
|
303
|
+
it("counts dead-lettered rows — the number an alert fires on", async ({ skip }) => {
|
|
304
|
+
if (!store.countByStatus || !store.fail) return skip();
|
|
305
|
+
expect(await store.countByStatus("dead_letter")).toBe(0);
|
|
306
|
+
const [e] = await seed(1);
|
|
307
|
+
await store.fail(idOf(e), { message: "fatal" }, { deadLetter: true });
|
|
308
|
+
expect(await store.countByStatus("dead_letter")).toBe(1);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
describe("oldestPendingAgeMs", () => {
|
|
312
|
+
it("is null on an empty queue, and a non-negative age once something pends", async ({ skip }) => {
|
|
313
|
+
if (!store.oldestPendingAgeMs) return skip();
|
|
314
|
+
expect(await store.oldestPendingAgeMs()).toBeNull();
|
|
315
|
+
await seed(1);
|
|
316
|
+
const age = await store.oldestPendingAgeMs();
|
|
317
|
+
expect(age).not.toBeNull();
|
|
318
|
+
expect(age ?? -1).toBeGreaterThanOrEqual(0);
|
|
319
|
+
});
|
|
320
|
+
it("ignores DELIVERED rows — it answers 'how long undelivered', not 'how old'", async ({ skip }) => {
|
|
321
|
+
if (!store.oldestPendingAgeMs) return skip();
|
|
322
|
+
if (!store.claimPending) return skip();
|
|
323
|
+
await seed(1);
|
|
324
|
+
const [claimed] = await store.claimPending({
|
|
325
|
+
consumerId: "c",
|
|
326
|
+
limit: 1,
|
|
327
|
+
leaseMs: 6e4
|
|
328
|
+
});
|
|
329
|
+
if (!claimed) return skip();
|
|
330
|
+
await store.acknowledge(idOf(claimed), { consumerId: "c" });
|
|
331
|
+
expect(await store.oldestPendingAgeMs()).toBeNull();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
244
334
|
});
|
|
245
335
|
}
|
|
246
336
|
//#endregion
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
|
|
1
2
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
3
|
//#region src/testing/storageContract.ts
|
|
3
4
|
/**
|
|
@@ -88,7 +89,8 @@ function runStorageContract(name, setup) {
|
|
|
88
89
|
size: bytes.length
|
|
89
90
|
}, EMPTY_CTX);
|
|
90
91
|
const read = await storage.read(file.id, EMPTY_CTX);
|
|
91
|
-
|
|
92
|
+
const actual = await readAll(read);
|
|
93
|
+
expect(actual.equals(bytes)).toBe(true);
|
|
92
94
|
expect(read.contentType).toBe("application/octet-stream");
|
|
93
95
|
await storage.delete(file.id, EMPTY_CTX);
|
|
94
96
|
});
|
|
@@ -100,7 +102,8 @@ function runStorageContract(name, setup) {
|
|
|
100
102
|
mimeType: "application/octet-stream",
|
|
101
103
|
size: bytes.length
|
|
102
104
|
}, EMPTY_CTX);
|
|
103
|
-
|
|
105
|
+
const first = await storage.delete(file.id, EMPTY_CTX);
|
|
106
|
+
expect(first).toBe(true);
|
|
104
107
|
let second = "threw";
|
|
105
108
|
try {
|
|
106
109
|
second = await storage.delete(file.id, EMPTY_CTX);
|
|
@@ -168,8 +171,10 @@ function runStorageContract(name, setup) {
|
|
|
168
171
|
mimeType: "application/octet-stream",
|
|
169
172
|
size: bytes.length
|
|
170
173
|
}, EMPTY_CTX);
|
|
171
|
-
|
|
172
|
-
expect(
|
|
174
|
+
const before = await readAll(await storage.read(file.id, EMPTY_CTX));
|
|
175
|
+
expect(before.equals(bytes)).toBe(true);
|
|
176
|
+
const removed = await storage.delete(file.id, EMPTY_CTX);
|
|
177
|
+
expect(removed).toBe(true);
|
|
173
178
|
let rejected = false;
|
|
174
179
|
try {
|
|
175
180
|
if (!(await readAll(await storage.read(file.id, EMPTY_CTX))).equals(bytes)) rejected = true;
|
|
@@ -188,7 +193,8 @@ function runStorageContract(name, setup) {
|
|
|
188
193
|
}, EMPTY_CTX);
|
|
189
194
|
const result = await storage.read(file.id, EMPTY_CTX);
|
|
190
195
|
expect(result.kind === "stream" || result.kind === "buffer").toBe(true);
|
|
191
|
-
|
|
196
|
+
const actual = await readAll(result);
|
|
197
|
+
expect(actual.equals(bytes)).toBe(true);
|
|
192
198
|
await storage.delete(file.id, EMPTY_CTX);
|
|
193
199
|
});
|
|
194
200
|
it("read() with a mid-object range slices correctly (when adapter supports ranges)", async () => {
|
package/dist/types/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import { i as
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
5
|
-
|
|
1
|
+
|
|
2
|
+
import { $ as OpenApiSchemas, A as RequestIdOptions, An as BaseControllerOptions, At as RouteDefinition, Bt as RawRouteHandler, C as RequestContext, D as HealthCheck, Dt as ActionEntry, E as GracefulShutdownOptions, Et as ActionDefinition, F as FastifyWithDecorators, Ft as ControllerLike, G as OnTenantDeleteConfig, Gt as Authenticator, H as CrudController, I as MiddlewareHandler, It as FastifyHandler, J as PresetHook, Jt as TokenPair, K as ResolvedTenantPurge, Kt as AuthenticatorContext, L as RequestWithExtras, Lt as IController, M as EventsDecorator, Mt as RouteMethod, N as FastifyRequestExtras, Nt as RateLimitConfig, O as HealthOptions, Ot as ActionHandlerFn, P as FastifyWithAuth, Pt as ControllerHandler, Q as MiddlewareConfig, Rt as IControllerResponse, S as QueryParserInterface, T as CrudRouterOptions, Tt as ResourceCacheConfig, U as ResourceConfig, Ut as AuthHelpers, Vt as RouteHandler, W as ResourcePermissions, Wt as AuthPluginOptions, X as CrudRouteKey, Y as PresetResult, Z as CrudSchemas, _ as ControllerQueryOptions, a as InferAdapterDoc, at as ResourceExtensions, b as ParsedQuery, c as TypedController, d as PaginationResult, et as RouteSchemaOptions, f as IntrospectionData, g as ArcInternalMetadata, h as ResourceMetadata, i as ValidationResult, it as ResourceHooks, j as ArcDecorator, jt as RouteMcpConfig, k as IntrospectionPluginOptions, kt as ActionsMap, l as TypedRepository, m as RegistryStats, n as ConfigError, nt as FieldRule, o as InferDocType, ot as EventDefinition, p as RegistryEntry, q as PresetFunction, qt as JwtContext, r as ValidateOptions, rt as ResourceHookContext, s as InferResourceDoc, t as RouteHandlerMethod, tt as ArcFieldRule, u as TypedResourceConfig, v as LookupOption, w as ServiceContext, x as PopulateOption, y as OwnershipCheck, zt as IRequestContext } from "../index-CpzxqePp.mjs";
|
|
3
|
+
import { i as RequestScope } from "../types-ChuShpCP.mjs";
|
|
4
|
+
import { a as UserBase, d as JWTPayload, f as ObjectId, i as PermissionContext, l as AnyRecord, m as UserOrganization, n as DataPolicy, p as UserLike, r as PermissionCheck, t as AuthorizationDecision, u as ArcRequest } from "../types-B7XeLEgK.mjs";
|
|
5
|
+
import { n as ElevationOptions, t as ElevationEvent } from "../elevation-BXCfVcNG.mjs";
|
|
6
|
+
export type { ActionDefinition, ActionEntry, ActionHandlerFn, ActionsMap, AnyRecord, ArcDecorator, ArcFieldRule, ArcInternalMetadata, ArcRequest, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, AuthorizationDecision, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRouteKey, CrudRouterOptions, CrudSchemas, DataPolicy, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OnTenantDeleteConfig, OpenApiSchemas, OwnershipCheck, PaginationResult, ParsedQuery, PermissionCheck, PermissionContext, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryParserInterface, RateLimitConfig, RawRouteHandler, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResolvedTenantPurge, ResourceCacheConfig, ResourceConfig, ResourceExtensions, ResourceHookContext, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteDefinition, RouteHandler, RouteHandlerMethod, RouteMcpConfig, RouteMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult };
|
package/dist/types/index.mjs
CHANGED
package/dist/types/storage.d.mts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
import { a as StorageReadResult, i as StorageReadRange, n as StorageContext, o as StorageUploadInput, r as StorageFile, t as Storage } from "../storage-Cx9Sz3Ou.mjs";
|
|
2
3
|
export { Storage, StorageContext, StorageFile, StorageReadRange, StorageReadResult, StorageUploadInput };
|
package/dist/types/storage.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
import { i as RequestScope } from "./types-ChuShpCP.mjs";
|
|
2
3
|
import { FastifyRequest } from "fastify";
|
|
3
4
|
//#region src/types/base.d.ts
|
|
4
5
|
declare module "fastify" {
|
|
@@ -53,7 +54,7 @@ interface JWTPayload {
|
|
|
53
54
|
[key: string]: unknown;
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
|
-
* Typed Fastify request with Arc decorations. Use in `
|
|
57
|
+
* Typed Fastify request with Arc decorations. Use in `rawHandler` routes
|
|
57
58
|
* instead of `(req as any).user`.
|
|
58
59
|
*
|
|
59
60
|
* @example
|
|
@@ -241,8 +242,27 @@ type PermissionCheck<TDoc = Record<string, unknown>> = ((context: PermissionCont
|
|
|
241
242
|
interface PermissionCheckMeta {
|
|
242
243
|
/** Set by allowPublic() — marks the endpoint as publicly accessible */
|
|
243
244
|
_isPublic?: boolean;
|
|
245
|
+
/**
|
|
246
|
+
* Set by requireAuth() — a pure identity gate (must be signed in) that adds NO
|
|
247
|
+
* row-level or environmental condition. Marked so `allOf` does not mistake it
|
|
248
|
+
* for an opaque runtime branch and needlessly taint the composite `conditional`.
|
|
249
|
+
*/
|
|
250
|
+
_requiresAuth?: boolean;
|
|
244
251
|
/** Set by requireRoles() — the roles required for access */
|
|
245
252
|
_roles?: readonly string[];
|
|
253
|
+
/**
|
|
254
|
+
* Set by `requirePlatformRole()` — this gate consults PLATFORM roles only and
|
|
255
|
+
* can never be satisfied by an organization role.
|
|
256
|
+
*
|
|
257
|
+
* It exists because that property is otherwise unprovable from outside a
|
|
258
|
+
* check. `requireRoles(["ops"])` accepts an ORG role named `ops` by default,
|
|
259
|
+
* and `requireOrgRole("manager")` grants any org's manager — both return a
|
|
260
|
+
* bare allow with no policy, so a surface that must be platform-wide cannot
|
|
261
|
+
* tell them apart from a genuine operator gate by inspecting the decision.
|
|
262
|
+
* A global surface (see `jobsPlugin`'s `managementRoutes`) requires this
|
|
263
|
+
* marker at boot rather than trusting the caller's intent.
|
|
264
|
+
*/
|
|
265
|
+
_platformOnly?: boolean;
|
|
246
266
|
/** Set by requireOrgMembership() — org-level permission type */
|
|
247
267
|
_orgPermission?: string;
|
|
248
268
|
/** Set by requireOrgRole() — the org roles required for access */
|
|
@@ -290,6 +310,18 @@ interface PermissionCheckMeta {
|
|
|
290
310
|
scopes?: readonly string[];
|
|
291
311
|
dpop: boolean;
|
|
292
312
|
};
|
|
313
|
+
/**
|
|
314
|
+
* Set by `allOf(...)` when its surfaced role/scope meta is NECESSARY but NOT
|
|
315
|
+
* SUFFICIENT — the conjunction also contains an opaque runtime branch
|
|
316
|
+
* (ownership / custom / dynamic / flow-mode / quota …) or a contradictory
|
|
317
|
+
* constraint that flat metadata can't capture. `describePermission` propagates
|
|
318
|
+
* it as `conditional: true`, so `explainAccess` reports a role/scope MATCH as
|
|
319
|
+
* `conditional` (server decides) instead of a definitive — and unsound —
|
|
320
|
+
* `allow`. A role/scope MISMATCH is still a definitive `deny` (a necessary
|
|
321
|
+
* condition failed). Without this flag a composed gate could tell a UI "you can"
|
|
322
|
+
* when the runtime check will refuse.
|
|
323
|
+
*/
|
|
324
|
+
_conditional?: boolean;
|
|
293
325
|
}
|
|
294
326
|
//#endregion
|
|
295
327
|
export { UserBase as a, normalizeRoles as c, JWTPayload as d, ObjectId as f, PermissionContext as i, AnyRecord as l, UserOrganization as m, DataPolicy as n, getUserId as o, UserLike as p, PermissionCheck as r, getUserRoles as s, AuthorizationDecision as t, ArcRequest as u };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
import { PurgeActor, PurgeEvidence, PurgeResourceResult, PurgeStrategyKind as PurgeStrategyKind$1, PurgeVerificationSummary } from "@classytic/primitives/retention";
|
|
2
3
|
//#region src/cleanup/types.d.ts
|
|
3
4
|
/** Ambient context every recipe method receives. Host-injected — never global. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import { n as
|
|
1
|
+
|
|
2
|
+
import { n as DomainEvent, r as EventHandler } from "./EventTransport-2uVV34WU.mjs";
|
|
3
|
+
import { n as ErrorMapper } from "./errorHandler-CpYUJmPA.mjs";
|
|
3
4
|
import { FastifyInstance, FastifyPluginAsync } from "fastify";
|
|
4
5
|
import { LockAdapter } from "@classytic/repo-core/lock";
|
|
5
6
|
//#region src/plugins/health.d.ts
|
|
@@ -319,6 +320,26 @@ interface LoadResourcesOptions<TContext = unknown> {
|
|
|
319
320
|
*/
|
|
320
321
|
declare function loadResources<TContext = unknown>(dir: string, options?: LoadResourcesOptions<TContext>): Promise<ResourceLike[]>;
|
|
321
322
|
//#endregion
|
|
323
|
+
//#region src/factory/module/disposers.d.ts
|
|
324
|
+
/** Teardown callback registered by a setup phase via `defer`. */
|
|
325
|
+
type ModuleDisposer = () => void | Promise<void>;
|
|
326
|
+
/**
|
|
327
|
+
* Second argument to a module's setup phases (`plugins`, `bootstrap`,
|
|
328
|
+
* `afterResources`).
|
|
329
|
+
*/
|
|
330
|
+
interface ModuleSetupContext {
|
|
331
|
+
/**
|
|
332
|
+
* Register a teardown callback for the resource just acquired. Call it
|
|
333
|
+
* IMMEDIATELY after acquisition — that is what makes partial-init teardown
|
|
334
|
+
* exact rather than defensive.
|
|
335
|
+
*
|
|
336
|
+
* Disposers run LIFO, after this module's `onClose`, on both the rollback
|
|
337
|
+
* and shutdown paths, and exactly once. A throwing disposer is logged and
|
|
338
|
+
* never blocks the ones behind it.
|
|
339
|
+
*/
|
|
340
|
+
defer(disposer: ModuleDisposer): void;
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
322
343
|
//#region src/factory/module/types.d.ts
|
|
323
344
|
/**
|
|
324
345
|
* A module's contribution of some arm (event handlers, scheduled jobs, …).
|
|
@@ -409,6 +430,13 @@ interface ArcModule<TExports = unknown> {
|
|
|
409
430
|
* *importing* a dependent until its dependency is ready. Use a thunk to gate
|
|
410
431
|
* whether a module is imported at all (region/tier packs); use `dependsOn`
|
|
411
432
|
* to order modules that are already selected.
|
|
433
|
+
*
|
|
434
|
+
* **Convention: every HARD sibling dependency belongs here** — including
|
|
435
|
+
* ones read lazily. `lazyRequiredModuleExports` defers the export READ to
|
|
436
|
+
* first use (and eagerly validates the module is composed), but only
|
|
437
|
+
* `dependsOn` orders composition so the sibling's engine exists before
|
|
438
|
+
* yours initializes. Omit the edge only for genuinely optional
|
|
439
|
+
* integrations (`lazyModuleExports` / `getOptionalModuleExports`).
|
|
412
440
|
*/
|
|
413
441
|
readonly dependsOn?: readonly string[];
|
|
414
442
|
/**
|
|
@@ -422,8 +450,18 @@ interface ArcModule<TExports = unknown> {
|
|
|
422
450
|
* published ecosystem packages: `plugins` = "register infra"; `bootstrap` =
|
|
423
451
|
* "initialise engines (and return the public export)". Registering a plugin
|
|
424
452
|
* inside `bootstrap` still works — this slot just makes the two distinct.
|
|
453
|
+
*
|
|
454
|
+
* Receives `{ defer }` to register teardown for anything it acquires, and
|
|
455
|
+
* may RETURN a single disposer as shorthand for the one-resource case:
|
|
456
|
+
*
|
|
457
|
+
* ```ts
|
|
458
|
+
* plugins: async (fastify) => {
|
|
459
|
+
* const conn = await connect();
|
|
460
|
+
* return () => conn.close(); // equivalent to defer(...)
|
|
461
|
+
* },
|
|
462
|
+
* ```
|
|
425
463
|
*/
|
|
426
|
-
plugins?: (fastify: FastifyInstance) =>
|
|
464
|
+
plugins?: (fastify: FastifyInstance, context: ModuleSetupContext) => ModuleDisposer | undefined | Promise<ModuleDisposer | undefined>;
|
|
427
465
|
/**
|
|
428
466
|
* Domain error mappers this module ships (see `defineErrorMapper`). Merged
|
|
429
467
|
* into the app's error handler at boot, AFTER any host-declared mappers —
|
|
@@ -460,8 +498,23 @@ interface ArcModule<TExports = unknown> {
|
|
|
460
498
|
* // arc-invoice bootstrap, composed AFTER arc-accounting:
|
|
461
499
|
* const acct = getModuleExports<AccountingEngine>(fastify, "accounting");
|
|
462
500
|
* ```
|
|
501
|
+
*
|
|
502
|
+
* The return value is the module's export, so teardown here goes through
|
|
503
|
+
* `defer` (the second argument) rather than a returned disposer — register
|
|
504
|
+
* each resource the moment it exists and partial-init cleanup becomes exact
|
|
505
|
+
* instead of a chain of `?.` guards:
|
|
506
|
+
*
|
|
507
|
+
* ```ts
|
|
508
|
+
* bootstrap: async (fastify, { defer }) => {
|
|
509
|
+
* const client = await openClient();
|
|
510
|
+
* defer(() => client.close());
|
|
511
|
+
* const engine = await createEngine(client); // if this throws, only
|
|
512
|
+
* defer(() => engine.stop()); // the client is released
|
|
513
|
+
* return engine;
|
|
514
|
+
* },
|
|
515
|
+
* ```
|
|
463
516
|
*/
|
|
464
|
-
bootstrap?: (fastify: FastifyInstance) => TExports | Promise<TExports>;
|
|
517
|
+
bootstrap?: (fastify: FastifyInstance, context: ModuleSetupContext) => TExports | Promise<TExports>;
|
|
465
518
|
/**
|
|
466
519
|
* App-level resource NAMES this module authoritatively SUPERSEDES. When the
|
|
467
520
|
* host also lists (or `resourceDir`-discovers) a resource of the same name,
|
|
@@ -476,17 +529,42 @@ interface ArcModule<TExports = unknown> {
|
|
|
476
529
|
* Semantics:
|
|
477
530
|
* - Purely a supersede-the-app-fork switch — it filters the *app* resource
|
|
478
531
|
* list, never the module's own `resources` (those always register).
|
|
479
|
-
* -
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
* - Idempotent + tolerant: an `owns` entry with no matching app resource is
|
|
483
|
-
* a silent no-op — a module may pre-declare a name before any fork exists.
|
|
532
|
+
* - Idempotent + tolerant on the APP side: an `owns` entry with no matching
|
|
533
|
+
* app resource is a silent no-op — a module may pre-declare a name before
|
|
534
|
+
* any fork exists.
|
|
484
535
|
* - Collected across ALL modules; the app-side supersession is their union.
|
|
485
536
|
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
537
|
+
* **Prefer `owns: "provided"`** (2.32) — arc derives the list from the
|
|
538
|
+
* resources this module actually returned, so the claim and the reality
|
|
539
|
+
* cannot disagree:
|
|
540
|
+
*
|
|
541
|
+
* ```ts
|
|
542
|
+
* defineModule({ name: "order", owns: "provided", resources: () => [...] })
|
|
543
|
+
* ```
|
|
544
|
+
*
|
|
545
|
+
* That is the whole arm for most modules. Resources are resolved exactly
|
|
546
|
+
* once, names are validated (duplicates within a module are rejected), the
|
|
547
|
+
* effective `owns` is derived, and supersession runs afterward — one atomic
|
|
548
|
+
* phase arc owns, rather than a rule each module re-states.
|
|
549
|
+
*
|
|
550
|
+
* **The explicit-array form is ENFORCED at boot: if you `owns` a name,
|
|
551
|
+
* PROVIDE it** — in THIS module's own `resources`. Arc fails boot when a
|
|
552
|
+
* claimed name is not among them, naming the module, the unmet names, and
|
|
553
|
+
* what it did supply. Without that check, a typo or a resource deleted
|
|
554
|
+
* without updating the list booted "successfully" with the app's route
|
|
555
|
+
* silently removed and nothing serving it. Two real cases, both silent 404s:
|
|
556
|
+
* a POS module claiming a checkout resource it mounted only when the host
|
|
557
|
+
* supplied a pipeline, and a device gateway claiming a name it never
|
|
558
|
+
* provided at all.
|
|
559
|
+
*
|
|
560
|
+
* Keep the explicit array only when the list is genuinely NOT the module's
|
|
561
|
+
* own resource set — e.g. pre-declaring a name before the fork exists.
|
|
562
|
+
*
|
|
563
|
+
* A sibling module supplying the name does NOT satisfy the claim — ownership
|
|
564
|
+
* is local to the module that declares it, which is the colocation this arm
|
|
565
|
+
* exists for.
|
|
488
566
|
*/
|
|
489
|
-
readonly owns?: readonly string[];
|
|
567
|
+
readonly owns?: readonly string[] | "provided";
|
|
490
568
|
/**
|
|
491
569
|
* The module's resources. Array or factory — the factory form runs AFTER all
|
|
492
570
|
* bootstraps, so `defineResource(...)` receives fully-booted engines. Flows
|
|
@@ -498,8 +576,12 @@ interface ArcModule<TExports = unknown> {
|
|
|
498
576
|
* Post-registration wiring — runs in the `afterResources` phase, once this
|
|
499
577
|
* module's routes are mounted. Use for cross-resource event subscriptions the
|
|
500
578
|
* module owns.
|
|
579
|
+
*
|
|
580
|
+
* Receives `{ defer }`, and may return a disposer, on the same terms as
|
|
581
|
+
* `plugins` — handy for wiring that must be unwound (a subscription, a
|
|
582
|
+
* listener) without hoisting a handle into module scope.
|
|
501
583
|
*/
|
|
502
|
-
afterResources?: (fastify: FastifyInstance) =>
|
|
584
|
+
afterResources?: (fastify: FastifyInstance, context: ModuleSetupContext) => ModuleDisposer | undefined | Promise<ModuleDisposer | undefined>;
|
|
503
585
|
/**
|
|
504
586
|
* Readiness checks this module contributes to the app's
|
|
505
587
|
* `healthPlugin` (`/_health/ready`). Collected across ALL modules in
|
|
@@ -569,9 +651,42 @@ interface ArcModule<TExports = unknown> {
|
|
|
569
651
|
*/
|
|
570
652
|
readonly scheduledJobs?: ModuleContribution<ScheduleDefinition>;
|
|
571
653
|
/**
|
|
572
|
-
* Teardown —
|
|
573
|
-
*
|
|
574
|
-
*
|
|
654
|
+
* Teardown — destroy engines, stop timers, flush the module's outbox.
|
|
655
|
+
* Modules close in REVERSE composition order (last composed, first closed),
|
|
656
|
+
* mirroring init, BEFORE the app-level `onClose` and before infra plugins'
|
|
657
|
+
* own close hooks (so teardown still sees a live DB/Redis).
|
|
658
|
+
*
|
|
659
|
+
* Boot is transactional: if any phase fails mid-boot — including THIS
|
|
660
|
+
* module's own `plugins`/`bootstrap` — arc immediately closes every module
|
|
661
|
+
* that entered an init phase, this closer included, in reverse order, then
|
|
662
|
+
* rethrows the original boot error. A closer runs AT MOST ONCE across the
|
|
663
|
+
* rollback and shutdown paths, and one throwing closer never blocks the
|
|
664
|
+
* rest (best-effort; failures are logged).
|
|
665
|
+
*
|
|
666
|
+
* Because rollback fires after a PARTIAL init — your `bootstrap` opened a
|
|
667
|
+
* client and threw on the next line, or your `plugins` ran but `bootstrap`
|
|
668
|
+
* never did — write closers defensively. Guard everything init may not have
|
|
669
|
+
* reached: `client?.close()`, not `client.close()`.
|
|
670
|
+
*
|
|
671
|
+
* ```ts
|
|
672
|
+
* let client: Client | undefined;
|
|
673
|
+
* defineModule({
|
|
674
|
+
* name: "billing",
|
|
675
|
+
* bootstrap: async () => {
|
|
676
|
+
* client = await openClient(); // allocated…
|
|
677
|
+
* await migrate(client); // …and this may throw
|
|
678
|
+
* },
|
|
679
|
+
* onClose: async () => { await client?.close(); }, // still runs
|
|
680
|
+
* });
|
|
681
|
+
* ```
|
|
682
|
+
*
|
|
683
|
+
* Prefer `defer` (the setup phases' second argument) for anything acquired
|
|
684
|
+
* step-by-step: it registers each teardown at the point of acquisition, so
|
|
685
|
+
* the runtime knows exactly what was reached and the `?.` guards above
|
|
686
|
+
* disappear. `onClose` remains the right slot for tearing down what
|
|
687
|
+
* `bootstrap` RETURNED — the module's engine — and runs FIRST, ahead of the
|
|
688
|
+
* module's deferred disposers (see `disposers.ts` for the ordering rule).
|
|
689
|
+
* The two compose; either alone is complete.
|
|
575
690
|
*/
|
|
576
691
|
onClose?: (fastify: FastifyInstance) => void | Promise<void>;
|
|
577
692
|
}
|
|
@@ -618,4 +733,4 @@ type ArcModuleInput<TExports = unknown> = ArcModule<TExports> | Promise<ArcModul
|
|
|
618
733
|
*/
|
|
619
734
|
interface ArcModuleRegistry {}
|
|
620
735
|
//#endregion
|
|
621
|
-
export {
|
|
736
|
+
export { HealthCheck as _, ModuleContribution as a, healthPlugin as b, LoadResourcesOptions as c, loadResources as d, ScheduleDefinition as f, schedulesPlugin as g, _default as h, EventHandlerDefinition as i, ResourceLike as l, SchedulesPluginOptions as m, ArcModuleInput as n, ModuleDisposer as o, ScheduleStats as p, ArcModuleRegistry as r, ModuleSetupContext as s, ArcModule as t, ResourceModule as u, HealthOptions as v, _default$1 as y };
|