@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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { n as getUserRoles } from "./userHelpers-
|
|
3
|
-
import { n as convertRouteSchema } from "./schemaConverter-
|
|
4
|
-
import { t as resolveActionPermission } from "./actionPermissions-
|
|
5
|
-
import { t as pluralize } from "./pluralize-
|
|
6
|
-
import { t as buildActionBodySchema } from "./createActionRouter-
|
|
1
|
+
/*! @classytic/arc v2.44.0 | MIT | https://classytic.com */
|
|
2
|
+
import { n as getUserRoles } from "./userHelpers-C76zD5pa.mjs";
|
|
3
|
+
import { n as convertRouteSchema } from "./schemaConverter-BVjkQ5DD.mjs";
|
|
4
|
+
import { t as resolveActionPermission } from "./actionPermissions-5N04zBL4.mjs";
|
|
5
|
+
import { t as pluralize } from "./pluralize-COUNYo3d.mjs";
|
|
6
|
+
import { t as buildActionBodySchema } from "./createActionRouter-zG13rpPJ.mjs";
|
|
7
7
|
import fp from "fastify-plugin";
|
|
8
8
|
//#region src/docs/openapi/canonical-schemas.ts
|
|
9
9
|
/**
|
|
@@ -133,7 +133,17 @@ function buildPaginatedListSchema(itemRef) {
|
|
|
133
133
|
total: { type: "integer" },
|
|
134
134
|
pages: { type: "integer" },
|
|
135
135
|
hasNext: { type: "boolean" },
|
|
136
|
-
hasPrev: { type: "boolean" }
|
|
136
|
+
hasPrev: { type: "boolean" },
|
|
137
|
+
totalIsEstimate: {
|
|
138
|
+
type: "boolean",
|
|
139
|
+
description: "true when `total` is an estimate or a cap (render as e.g. '10,000+')."
|
|
140
|
+
},
|
|
141
|
+
countedAt: {
|
|
142
|
+
type: "string",
|
|
143
|
+
format: "date-time",
|
|
144
|
+
nullable: true,
|
|
145
|
+
description: "When `total` was counted; null when no count was taken."
|
|
146
|
+
}
|
|
137
147
|
}
|
|
138
148
|
},
|
|
139
149
|
{
|
|
@@ -161,6 +171,17 @@ function buildPaginatedListSchema(itemRef) {
|
|
|
161
171
|
type: "string",
|
|
162
172
|
nullable: true,
|
|
163
173
|
description: "Opaque cursor for the next page, or null when `hasMore` is false. Round-trip verbatim as the `after` query param."
|
|
174
|
+
},
|
|
175
|
+
prev: {
|
|
176
|
+
type: "string",
|
|
177
|
+
nullable: true,
|
|
178
|
+
description: "Opaque cursor for the previous page, or null on the first page. Round-trip verbatim as the `before` query param."
|
|
179
|
+
},
|
|
180
|
+
hasPrev: { type: "boolean" },
|
|
181
|
+
end: {
|
|
182
|
+
type: "string",
|
|
183
|
+
nullable: true,
|
|
184
|
+
description: "Cursor at this page's last row — resume a feed with `after` from here."
|
|
164
185
|
}
|
|
165
186
|
}
|
|
166
187
|
},
|
|
@@ -379,7 +400,7 @@ function createOperation(resource, operation, summary, extras, requiresAuthOverr
|
|
|
379
400
|
const requiresAuth = requiresAuthOverride !== void 0 ? requiresAuthOverride : typeof operationPermission === "function" && !isPublic;
|
|
380
401
|
const permAnnotation = describePermissionForOpenApi(operationPermission);
|
|
381
402
|
const descParts = [];
|
|
382
|
-
if (permAnnotation) descParts.push(`**Permission**: ${permAnnotation.type === "public" ? "Public" : permAnnotation.type === "requireRoles" ? `Requires roles: ${(permAnnotation.roles ?? []).join(", ")}` : "Requires authentication"}`);
|
|
403
|
+
if (permAnnotation) descParts.push(`**Permission**: ${permAnnotation.type === "public" ? permAnnotation.rowFiltered ? "Public (row-filtered: a policy narrows which records anyone can see)" : "Public" : permAnnotation.type === "requireRoles" ? `Requires roles: ${(permAnnotation.roles ?? []).join(", ")}` : "Requires authentication"}`);
|
|
383
404
|
if (resource.presets && resource.presets.length > 0) descParts.push(`**Presets**: ${resource.presets.join(", ")}`);
|
|
384
405
|
const applicableSteps = (resource.pipelineSteps ?? []).filter((s) => {
|
|
385
406
|
if (!s.operations) return true;
|
|
@@ -398,6 +419,7 @@ function createOperation(resource, operation, summary, extras, requiresAuthOverr
|
|
|
398
419
|
type: s.type,
|
|
399
420
|
name: s.name
|
|
400
421
|
})) },
|
|
422
|
+
...resource.idempotency?.required.some((o) => o === operation) && { "x-arc-idempotency": { required: true } },
|
|
401
423
|
responses: buildErrorResponses({
|
|
402
424
|
requiresAuth,
|
|
403
425
|
permRoles: permAnnotation?.roles
|
|
@@ -421,7 +443,10 @@ function createOperation(resource, operation, summary, extras, requiresAuthOverr
|
|
|
421
443
|
function describePermissionForOpenApi(check) {
|
|
422
444
|
if (!check || typeof check !== "function") return void 0;
|
|
423
445
|
const fn = check;
|
|
424
|
-
if (fn._isPublic === true) return
|
|
446
|
+
if (fn._isPublic === true) return fn._rowPolicy ? {
|
|
447
|
+
type: "public",
|
|
448
|
+
rowFiltered: true
|
|
449
|
+
} : { type: "public" };
|
|
425
450
|
const result = { type: "requireAuth" };
|
|
426
451
|
if (Array.isArray(fn._roles) && fn._roles.length > 0) {
|
|
427
452
|
result.type = "requireRoles";
|
|
@@ -573,6 +598,8 @@ function emitMount(paths, resource, basePath, additionalSecurity, subsetActions,
|
|
|
573
598
|
});
|
|
574
599
|
return typeof effective === "function" && !effective._isPublic;
|
|
575
600
|
});
|
|
601
|
+
const requiredActions = subsetActions.filter((a) => a.idempotency?.required === true).map((a) => a.name);
|
|
602
|
+
const handlerDedup = Object.fromEntries(subsetActions.flatMap((a) => a.idempotency?.required === true && a.idempotency.dedup === "handler" ? [[a.name, String(a.idempotency.owner)]] : []));
|
|
576
603
|
if (!paths[actionPath]) paths[actionPath] = {};
|
|
577
604
|
paths[actionPath].post = createOperation(resource, "action", `Perform action (${actionEnum.join(" / ")})`, {
|
|
578
605
|
...hasId ? { parameters: [{
|
|
@@ -583,6 +610,10 @@ function emitMount(paths, resource, basePath, additionalSecurity, subsetActions,
|
|
|
583
610
|
description: "Resource ID"
|
|
584
611
|
}] } : {},
|
|
585
612
|
description: descLines.join("\n"),
|
|
613
|
+
...requiredActions.length > 0 && { "x-arc-idempotency": {
|
|
614
|
+
requiredActions,
|
|
615
|
+
...Object.keys(handlerDedup).length > 0 ? { handlerDedup } : {}
|
|
616
|
+
} },
|
|
586
617
|
requestBody: {
|
|
587
618
|
required: true,
|
|
588
619
|
content: { "application/json": { schema: bodySchema } }
|
|
@@ -883,6 +914,13 @@ function appendCustomRoutePaths(paths, resource, basePath, additionalSecurity) {
|
|
|
883
914
|
content: { "application/json": { schema } }
|
|
884
915
|
};
|
|
885
916
|
}
|
|
917
|
+
if (route.idempotency?.required === true) extras["x-arc-idempotency"] = {
|
|
918
|
+
required: true,
|
|
919
|
+
...route.idempotency.dedup === "handler" ? {
|
|
920
|
+
dedup: "handler",
|
|
921
|
+
owner: route.idempotency.owner
|
|
922
|
+
} : {}
|
|
923
|
+
};
|
|
886
924
|
paths[fullPath][method] = createOperation(resource, handlerName, route.summary ?? handlerName, extras, requiresAuthForRoute, additionalSecurity);
|
|
887
925
|
}
|
|
888
926
|
}
|
|
@@ -1004,4 +1042,4 @@ var openapi_default = fp(openApiPlugin, {
|
|
|
1004
1042
|
fastify: "5.x"
|
|
1005
1043
|
});
|
|
1006
1044
|
//#endregion
|
|
1007
|
-
export {
|
|
1045
|
+
export { openapi_default as n, buildOpenApiSpec as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
import { l as PermissionCheck } from "../base-
|
|
3
|
-
import {
|
|
2
|
+
import { l as PermissionCheck } from "../base-CZyixEQR.mjs";
|
|
3
|
+
import { n as ArcModule, u as ModuleName } from "../types-BJ-2XqVF.mjs";
|
|
4
4
|
import { OutboxStore } from "@classytic/primitives/outbox";
|
|
5
5
|
//#region src/outbox-admin/module.d.ts
|
|
6
6
|
/**
|
|
@@ -36,7 +36,7 @@ interface OutboxAdminModuleDeps {
|
|
|
36
36
|
* Name the outbox module was composed under, used to resolve the relay and
|
|
37
37
|
* declared as a `dependsOn` edge. Default `outbox`.
|
|
38
38
|
*/
|
|
39
|
-
outboxModuleName?:
|
|
39
|
+
outboxModuleName?: ModuleName;
|
|
40
40
|
/** Resource names this module supersedes. */
|
|
41
41
|
owns?: readonly string[];
|
|
42
42
|
/** Cap on `?limit`. Default 500 — a dead-letter list is for triage, not export. */
|
|
@@ -80,6 +80,6 @@ interface OutboxAdminModuleDeps {
|
|
|
80
80
|
*/
|
|
81
81
|
relayLagUnhealthyMs?: number;
|
|
82
82
|
}
|
|
83
|
-
declare function createOutboxAdminModule(deps: OutboxAdminModuleDeps): ArcModule<void>;
|
|
83
|
+
export declare function createOutboxAdminModule(deps: OutboxAdminModuleDeps): ArcModule<void>;
|
|
84
84
|
//#endregion
|
|
85
|
-
export {
|
|
85
|
+
export type { OutboxAdminModuleDeps, OutboxAdminPermissions, OutboxAdminStore };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { i as NotFoundError, u as ValidationError } from "../errors-
|
|
3
|
-
import { t as defineResource } from "../defineResource-
|
|
4
|
-
import { n as getModuleExports, t as defineModule } from "../module
|
|
1
|
+
/*! @classytic/arc v2.44.0 | MIT | https://classytic.com */
|
|
2
|
+
import { i as NotFoundError, u as ValidationError } from "../errors-CW4anJBy.mjs";
|
|
3
|
+
import { t as defineResource } from "../defineResource-gUp9ug66.mjs";
|
|
4
|
+
import { n as getModuleExports, t as defineModule } from "../module--L0iBIZz.mjs";
|
|
5
|
+
import { r as readOutboxHealth } from "../health-BYAwA6YS.mjs";
|
|
5
6
|
//#region src/outbox-admin/module.ts
|
|
6
7
|
/**
|
|
7
8
|
* Strip the domain payload and the stack unless explicitly opted in.
|
|
@@ -51,20 +52,7 @@ function createOutboxAdminModule(deps) {
|
|
|
51
52
|
path: "/health",
|
|
52
53
|
summary: "Outbox health — pending, dead-letter and relay lag",
|
|
53
54
|
permissions: view,
|
|
54
|
-
rawHandler: async (_req, reply) =>
|
|
55
|
-
const [pending, deadLetter, relayLagMs] = await Promise.all([
|
|
56
|
-
deps.store.countByStatus("pending"),
|
|
57
|
-
deps.store.countByStatus("dead_letter"),
|
|
58
|
-
deps.store.oldestPendingAgeMs()
|
|
59
|
-
]);
|
|
60
|
-
const healthy = deadLetter === 0 && (relayLagMs === null || relayLagMs < lagUnhealthyMs);
|
|
61
|
-
return reply.send({
|
|
62
|
-
pending,
|
|
63
|
-
deadLetter,
|
|
64
|
-
relayLagMs,
|
|
65
|
-
healthy
|
|
66
|
-
});
|
|
67
|
-
}
|
|
55
|
+
rawHandler: async (_req, reply) => reply.send(await readOutboxHealth(deps.store, lagUnhealthyMs))
|
|
68
56
|
},
|
|
69
57
|
{
|
|
70
58
|
method: "GET",
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { t as transactionContext } from "./transactionContext-
|
|
3
|
-
import { n as requestContext } from "./requestContext-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
/*! @classytic/arc v2.44.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as transactionContext } from "./transactionContext-orvnGu4F.mjs";
|
|
3
|
+
import { n as requestContext } from "./requestContext-C10UeFe5.mjs";
|
|
4
|
+
import { n as evictToSize, r as touch } from "./boundedMap-DWiKW7lP.mjs";
|
|
5
|
+
import { i as resolveEventTransport } from "./EventTransport-DZWDsaPa.mjs";
|
|
6
|
+
import { t as emitObserved } from "./eventPlugin-BweHAVws.mjs";
|
|
7
|
+
import { n as canReportHealth, r as readOutboxHealth } from "./health-BYAwA6YS.mjs";
|
|
8
|
+
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "./store-helpers-Coj9RhOZ.mjs";
|
|
9
|
+
import { t as claimedAttempts } from "./claimed-DYvQoZLv.mjs";
|
|
6
10
|
import { and, anyOf, eq, lte, ne, or } from "@classytic/repo-core/filter";
|
|
7
11
|
import { randomBytes } from "node:crypto";
|
|
8
|
-
import { InvalidOutboxEventError, OutboxOwnershipError } from "@classytic/primitives/outbox";
|
|
12
|
+
import { InvalidOutboxEventError, OutboxConflictError, OutboxOwnershipError, outboxPayloadFingerprint } from "@classytic/primitives/outbox";
|
|
9
13
|
import { update } from "@classytic/repo-core/update";
|
|
10
14
|
import { hostname } from "node:os";
|
|
11
15
|
//#region src/events/outbox/backoff.ts
|
|
@@ -35,9 +39,9 @@ import { hostname } from "node:os";
|
|
|
35
39
|
* @example Tuning for a faster transport
|
|
36
40
|
* ```typescript
|
|
37
41
|
* exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
|
|
38
|
-
* // attempt=1 →
|
|
39
|
-
* // attempt=2 →
|
|
40
|
-
* // attempt=3 →
|
|
42
|
+
* // attempt=1 → 250–325ms (+0–30%: jitter only ever DELAYS, never schedules early)
|
|
43
|
+
* // attempt=2 → 500–650ms
|
|
44
|
+
* // attempt=3 → 1000–1300ms
|
|
41
45
|
* // attempt=10 → capped at 10_000ms
|
|
42
46
|
* ```
|
|
43
47
|
*/
|
|
@@ -54,6 +58,12 @@ function exponentialBackoff(options) {
|
|
|
54
58
|
}
|
|
55
59
|
//#endregion
|
|
56
60
|
//#region src/events/repository-outbox-adapter.ts
|
|
61
|
+
/** A row read back from the repository that carries its event. */
|
|
62
|
+
function isStoredRow(value) {
|
|
63
|
+
if (typeof value !== "object" || value === null) return false;
|
|
64
|
+
const row = value;
|
|
65
|
+
return typeof row.type === "string" && typeof row.event === "object" && row.event !== null;
|
|
66
|
+
}
|
|
57
67
|
const DEFAULT_LEASE_MS$1 = 3e4;
|
|
58
68
|
const DEFAULT_CLAIM_LIMIT = 100;
|
|
59
69
|
const DEFAULT_PURGE_BATCH = 500;
|
|
@@ -110,10 +120,10 @@ function repositoryAsOutboxStore(repository, options) {
|
|
|
110
120
|
limit
|
|
111
121
|
}));
|
|
112
122
|
if (candidates.length === 0) return [];
|
|
113
|
-
|
|
123
|
+
const claimed = (await Promise.all(candidates.map((candidate) => {
|
|
114
124
|
const claimNow = /* @__PURE__ */ new Date();
|
|
115
125
|
const leaseExpiresAt = new Date(claimNow.getTime() + leaseMs);
|
|
116
|
-
return r.findOneAndUpdate(and(eq(idField, candidate
|
|
126
|
+
return r.findOneAndUpdate(and(eq(idField, candidate[idField]), claimableFilter(claimNow)), update({
|
|
117
127
|
set: {
|
|
118
128
|
leaseOwner: consumerId,
|
|
119
129
|
leaseExpiresAt
|
|
@@ -123,9 +133,22 @@ function repositoryAsOutboxStore(repository, options) {
|
|
|
123
133
|
fenceToken: 1
|
|
124
134
|
}
|
|
125
135
|
}), { returnDocument: "after" });
|
|
126
|
-
}))).filter((doc) => doc !== null)
|
|
136
|
+
}))).filter((doc) => doc !== null);
|
|
137
|
+
await Promise.all(claimed.filter((doc) => !isWellFormed(doc.event)).map((doc) => r.findOneAndUpdate(and(eq(idField, doc[idField]), eq("leaseOwner", consumerId), eq("fenceToken", doc.fenceToken ?? 0)), update({ set: {
|
|
138
|
+
status: "dead_letter",
|
|
139
|
+
leaseOwner: null,
|
|
140
|
+
leaseExpiresAt: null,
|
|
141
|
+
lastFailedAt: /* @__PURE__ */ new Date(),
|
|
142
|
+
firstFailedAt: doc.firstFailedAt ?? /* @__PURE__ */ new Date(),
|
|
143
|
+
lastError: {
|
|
144
|
+
message: "outbox row holds no event with a type and meta.id",
|
|
145
|
+
code: "MALFORMED_EVENT"
|
|
146
|
+
}
|
|
147
|
+
} }), { returnDocument: "after" })));
|
|
148
|
+
return claimed.filter((doc) => isWellFormed(doc.event)).map((doc) => ({
|
|
127
149
|
event: doc.event,
|
|
128
|
-
fencingToken: doc.fenceToken ?? 0
|
|
150
|
+
fencingToken: doc.fenceToken ?? 0,
|
|
151
|
+
...typeof doc.attempts === "number" ? { attempts: doc.attempts } : {}
|
|
129
152
|
}));
|
|
130
153
|
};
|
|
131
154
|
return {
|
|
@@ -157,7 +180,7 @@ function repositoryAsOutboxStore(repository, options) {
|
|
|
157
180
|
firstFailedAt: null,
|
|
158
181
|
lastFailedAt: null,
|
|
159
182
|
lastError: null,
|
|
160
|
-
dedupeKey: options?.dedupeKey
|
|
183
|
+
dedupeKey: options?.dedupeKey ? `${event.type}:${options.dedupeKey}` : null,
|
|
161
184
|
partitionKey: options?.partitionKey ?? null,
|
|
162
185
|
headers: options?.headers ? { ...options.headers } : null,
|
|
163
186
|
createdAt: now
|
|
@@ -165,8 +188,11 @@ function repositoryAsOutboxStore(repository, options) {
|
|
|
165
188
|
try {
|
|
166
189
|
await r.create(doc, options?.session ? { session: options.session } : void 0);
|
|
167
190
|
} catch (err) {
|
|
168
|
-
if (isDuplicateKeyError(err))
|
|
169
|
-
|
|
191
|
+
if (!isDuplicateKeyError(err)) throw err;
|
|
192
|
+
const existing = await safeGetOne(doc.dedupeKey === null ? eq(idField, event.meta.id) : or(eq(idField, event.meta.id), eq("dedupeKey", doc.dedupeKey)));
|
|
193
|
+
if (!isStoredRow(existing)) throw err;
|
|
194
|
+
if (existing.type === event.type && outboxPayloadFingerprint(existing.event) === outboxPayloadFingerprint(event)) return;
|
|
195
|
+
throw new OutboxConflictError(event.meta.id, event.type);
|
|
170
196
|
}
|
|
171
197
|
},
|
|
172
198
|
async getPending(limit) {
|
|
@@ -183,6 +209,15 @@ function repositoryAsOutboxStore(repository, options) {
|
|
|
183
209
|
return (await claimPendingFenced(options)).map((c) => c.event);
|
|
184
210
|
},
|
|
185
211
|
claimPendingFenced,
|
|
212
|
+
...typeof repository.updateMany === "function" ? { async acknowledgeMany(eventIds) {
|
|
213
|
+
if (eventIds.length === 0) return;
|
|
214
|
+
await repository.updateMany?.(and(anyOf(idField, [...eventIds]), ne("status", "delivered")), update({ set: {
|
|
215
|
+
status: "delivered",
|
|
216
|
+
deliveredAt: /* @__PURE__ */ new Date(),
|
|
217
|
+
leaseOwner: null,
|
|
218
|
+
leaseExpiresAt: null
|
|
219
|
+
} }));
|
|
220
|
+
} } : {},
|
|
186
221
|
async acknowledge(eventId, options) {
|
|
187
222
|
const now = /* @__PURE__ */ new Date();
|
|
188
223
|
const baseFilter = and(eq(idField, eventId), ne("status", "delivered"));
|
|
@@ -375,6 +410,7 @@ var EventOutbox = class {
|
|
|
375
410
|
_consumerId;
|
|
376
411
|
_leaseMs;
|
|
377
412
|
_onError;
|
|
413
|
+
_onPublished;
|
|
378
414
|
_usePublishMany;
|
|
379
415
|
_sessionProvider;
|
|
380
416
|
_traceContextProvider;
|
|
@@ -384,9 +420,11 @@ var EventOutbox = class {
|
|
|
384
420
|
* process; resets on restart. Populated as failures occur and cleared on
|
|
385
421
|
* successful ack or dead-letter transition; additionally hard-capped at
|
|
386
422
|
* {@link MAX_TRACKED_ATTEMPTS} (oldest-write evicted first) so events
|
|
387
|
-
* resolved by another relay worker can't accumulate forever.
|
|
388
|
-
*
|
|
389
|
-
*
|
|
423
|
+
* resolved by another relay worker can't accumulate forever.
|
|
424
|
+
*
|
|
425
|
+
* A FALLBACK only: when the store reports `attempts` on the fenced claim
|
|
426
|
+
* (`repositoryAsOutboxStore` and primitives' `MemoryOutboxStore` do), the
|
|
427
|
+
* durable count wins — see the failure path in `relayBatch`.
|
|
390
428
|
*/
|
|
391
429
|
_attempts = /* @__PURE__ */ new Map();
|
|
392
430
|
constructor(opts) {
|
|
@@ -398,6 +436,7 @@ var EventOutbox = class {
|
|
|
398
436
|
this._consumerId = opts.consumerId ?? `relay-${Math.random().toString(36).slice(2, 10)}`;
|
|
399
437
|
this._leaseMs = opts.leaseMs ?? DEFAULT_LEASE_MS;
|
|
400
438
|
this._onError = opts.onError;
|
|
439
|
+
this._onPublished = opts.onPublished;
|
|
401
440
|
this._usePublishMany = opts.usePublishMany ?? true;
|
|
402
441
|
this._failurePolicy = opts.failurePolicy;
|
|
403
442
|
this._sessionProvider = opts.sessionProvider;
|
|
@@ -516,13 +555,18 @@ var EventOutbox = class {
|
|
|
516
555
|
if (!this._transport) return empty;
|
|
517
556
|
const claimStartedAt = Date.now();
|
|
518
557
|
const fenceTokens = /* @__PURE__ */ new Map();
|
|
558
|
+
const durableAttempts = /* @__PURE__ */ new Map();
|
|
519
559
|
let pending;
|
|
520
560
|
if (typeof this._store.claimPendingFenced === "function") pending = (await this._store.claimPendingFenced({
|
|
521
561
|
limit: this._batchSize,
|
|
522
562
|
consumerId: this._consumerId,
|
|
523
563
|
leaseMs: this._leaseMs
|
|
524
564
|
})).map((c) => {
|
|
525
|
-
if (c.event?.meta?.id)
|
|
565
|
+
if (c.event?.meta?.id) {
|
|
566
|
+
fenceTokens.set(c.event.meta.id, c.fencingToken);
|
|
567
|
+
const attempts = claimedAttempts(c);
|
|
568
|
+
if (attempts !== void 0) durableAttempts.set(c.event.meta.id, attempts);
|
|
569
|
+
}
|
|
526
570
|
return c.event;
|
|
527
571
|
});
|
|
528
572
|
else if (this._store.claimPending) pending = await this._store.claimPending({
|
|
@@ -555,8 +599,7 @@ var EventOutbox = class {
|
|
|
555
599
|
const canFail = typeof this._store.fail === "function";
|
|
556
600
|
let publishOutcomes;
|
|
557
601
|
if (canPublishMany && valid.length > 0) try {
|
|
558
|
-
|
|
559
|
-
publishOutcomes = new Map(result);
|
|
602
|
+
publishOutcomes = batchOutcomes(valid, await this._transport.publishMany?.(valid));
|
|
560
603
|
} catch (batchErr) {
|
|
561
604
|
publishOutcomes = /* @__PURE__ */ new Map();
|
|
562
605
|
const err = batchErr instanceof Error ? batchErr : new Error(String(batchErr));
|
|
@@ -572,6 +615,12 @@ var EventOutbox = class {
|
|
|
572
615
|
if (!canFail) break;
|
|
573
616
|
}
|
|
574
617
|
}
|
|
618
|
+
if (this._onPublished) for (const event of valid) {
|
|
619
|
+
if (publishOutcomes.get(event.meta.id) !== null) continue;
|
|
620
|
+
try {
|
|
621
|
+
this._onPublished(event);
|
|
622
|
+
} catch {}
|
|
623
|
+
}
|
|
575
624
|
let stopBatch = false;
|
|
576
625
|
for (const event of valid) {
|
|
577
626
|
if (stopBatch) break;
|
|
@@ -583,13 +632,10 @@ var EventOutbox = class {
|
|
|
583
632
|
stopBatch = true;
|
|
584
633
|
continue;
|
|
585
634
|
}
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
this._attempts
|
|
589
|
-
|
|
590
|
-
const oldest = this._attempts.keys().next().value;
|
|
591
|
-
if (oldest !== void 0) this._attempts.delete(oldest);
|
|
592
|
-
}
|
|
635
|
+
const localAttempts = (this._attempts.get(event.meta.id) ?? 0) + 1;
|
|
636
|
+
const attempts = Math.max(localAttempts, durableAttempts.get(event.meta.id) ?? 0);
|
|
637
|
+
touch(this._attempts, event.meta.id, attempts);
|
|
638
|
+
evictToSize(this._attempts, MAX_TRACKED_ATTEMPTS);
|
|
593
639
|
const fenceToken = fenceTokens.get(event.meta.id);
|
|
594
640
|
let failOpts = {
|
|
595
641
|
consumerId: this._consumerId,
|
|
@@ -679,6 +725,22 @@ var EventOutbox = class {
|
|
|
679
725
|
return this._store.purge(olderThanMs);
|
|
680
726
|
}
|
|
681
727
|
};
|
|
728
|
+
/**
|
|
729
|
+
* A `publishMany` result must name exactly the events it was given. A missing outcome would read as
|
|
730
|
+
* success and be ACKNOWLEDGED unpublished, so any mismatch fails the whole batch — nothing is acked.
|
|
731
|
+
*/
|
|
732
|
+
function batchOutcomes(events, result) {
|
|
733
|
+
const problem = batchContractProblem(events, result);
|
|
734
|
+
if (!problem) return new Map(result);
|
|
735
|
+
const err = /* @__PURE__ */ new Error(`[arc outbox] transport publishMany ${problem} — the whole batch is failed, none acknowledged`);
|
|
736
|
+
return new Map(events.map((e) => [e.meta.id, err]));
|
|
737
|
+
}
|
|
738
|
+
function batchContractProblem(events, result) {
|
|
739
|
+
if (!(result instanceof Map)) return "returned no outcome map";
|
|
740
|
+
const ids = new Set(events.map((e) => e.meta.id));
|
|
741
|
+
for (const id of ids) if (!result.has(id)) return `returned no outcome for event '${id}'`;
|
|
742
|
+
for (const id of result.keys()) if (!ids.has(id)) return `returned an outcome for unknown event '${id}'`;
|
|
743
|
+
}
|
|
682
744
|
function normalizeError(err) {
|
|
683
745
|
if (err instanceof Error) return {
|
|
684
746
|
message: err.message,
|
|
@@ -723,6 +785,10 @@ function normalizeError(err) {
|
|
|
723
785
|
*/
|
|
724
786
|
/** Default relay cadence. Frequent enough that a retry is not a user-visible delay. */
|
|
725
787
|
const DEFAULT_RELAY_EVERY_MS = 5e3;
|
|
788
|
+
/** How long the readiness check reuses a relay-health reading. See `health.probeCacheMs`. */
|
|
789
|
+
const DEFAULT_PROBE_CACHE_MS = 5e3;
|
|
790
|
+
/** How often an unhealthy outbox is logged. See `health.alertEveryMs`. */
|
|
791
|
+
const DEFAULT_HEALTH_ALERT_EVERY_MS = 3e5;
|
|
726
792
|
/**
|
|
727
793
|
* One relay identity per RELAY INSTANCE — `<name>:<hostname>:<pid>:<random>`.
|
|
728
794
|
*
|
|
@@ -777,6 +843,35 @@ function mintConsumerId(name) {
|
|
|
777
843
|
function createOutboxModule(options) {
|
|
778
844
|
const name = options.name ?? "outbox";
|
|
779
845
|
const relayEveryMs = options.relayEveryMs ?? DEFAULT_RELAY_EVERY_MS;
|
|
846
|
+
const lagUnhealthyMs = (options.health === false ? void 0 : options.health?.relayLagUnhealthyMs) ?? 3e5;
|
|
847
|
+
const probeCacheMs = (options.health === false ? void 0 : options.health?.probeCacheMs) ?? DEFAULT_PROBE_CACHE_MS;
|
|
848
|
+
if (!Number.isFinite(probeCacheMs) || probeCacheMs < 0) throw new Error(`Outbox module "${name}": health.probeCacheMs must be a non-negative number, got ${String(probeCacheMs)}.`);
|
|
849
|
+
const alertEveryMs = options.health === false ? 0 : options.health?.alertEveryMs ?? DEFAULT_HEALTH_ALERT_EVERY_MS;
|
|
850
|
+
if (!Number.isFinite(alertEveryMs) || alertEveryMs < 0) throw new Error(`Outbox module "${name}": health.alertEveryMs must be a non-negative number, got ${String(alertEveryMs)}.`);
|
|
851
|
+
let log;
|
|
852
|
+
/** The unmeasurable-store warning is said once, not every alert tick. */
|
|
853
|
+
let warnedUnmeasurable = false;
|
|
854
|
+
/**
|
|
855
|
+
* The readiness probe's reading, reused for `probeCacheMs` and shared by
|
|
856
|
+
* concurrent probes (single-flight). A failed or unmeasurable reading is
|
|
857
|
+
* cached as unhealthy for the same window — retrying a failing store on
|
|
858
|
+
* every probe is load on the thing that is already failing.
|
|
859
|
+
*/
|
|
860
|
+
let probeReading;
|
|
861
|
+
let probeInFlight;
|
|
862
|
+
const probeHealthy = (read) => {
|
|
863
|
+
if (probeReading && Date.now() - probeReading.at < probeCacheMs) return Promise.resolve(probeReading.healthy);
|
|
864
|
+
probeInFlight ??= read().then((h) => h.healthy, () => false).then((healthy) => {
|
|
865
|
+
probeReading = {
|
|
866
|
+
at: Date.now(),
|
|
867
|
+
healthy
|
|
868
|
+
};
|
|
869
|
+
return healthy;
|
|
870
|
+
}).finally(() => {
|
|
871
|
+
probeInFlight = void 0;
|
|
872
|
+
});
|
|
873
|
+
return probeInFlight;
|
|
874
|
+
};
|
|
780
875
|
/**
|
|
781
876
|
* Built at bootstrap and reused by the schedule.
|
|
782
877
|
*
|
|
@@ -785,9 +880,20 @@ function createOutboxModule(options) {
|
|
|
785
880
|
* depends on for exactly-once publishing.
|
|
786
881
|
*/
|
|
787
882
|
let exports;
|
|
883
|
+
/** Passes THIS module started (nudge, schedule) that have not settled — `drain()` waits on them. */
|
|
884
|
+
const inFlightPasses = /* @__PURE__ */ new Set();
|
|
885
|
+
const trackPass = (pass) => {
|
|
886
|
+
inFlightPasses.add(pass);
|
|
887
|
+
const untrack = () => {
|
|
888
|
+
inFlightPasses.delete(pass);
|
|
889
|
+
};
|
|
890
|
+
pass.then(untrack, untrack);
|
|
891
|
+
return pass;
|
|
892
|
+
};
|
|
788
893
|
return {
|
|
789
894
|
name,
|
|
790
895
|
bootstrap: (fastify) => {
|
|
896
|
+
log = fastify.log;
|
|
791
897
|
const store = typeof options.store === "function" ? options.store(fastify) : options.store;
|
|
792
898
|
/**
|
|
793
899
|
* The RAW transport, never `fastify.events`.
|
|
@@ -811,6 +917,7 @@ function createOutboxModule(options) {
|
|
|
811
917
|
const relay = new EventOutbox({
|
|
812
918
|
store,
|
|
813
919
|
transport,
|
|
920
|
+
onPublished: (event) => emitObserved(fastify.events, event),
|
|
814
921
|
...options.batchSize !== void 0 ? { batchSize: options.batchSize } : {},
|
|
815
922
|
...options.leaseMs !== void 0 ? { leaseMs: options.leaseMs } : {},
|
|
816
923
|
consumerId: options.consumerId ?? mintConsumerId(name),
|
|
@@ -835,22 +942,57 @@ function createOutboxModule(options) {
|
|
|
835
942
|
const requestDrain = () => {
|
|
836
943
|
if (drainQueued) return;
|
|
837
944
|
drainQueued = true;
|
|
945
|
+
let passDone = () => {};
|
|
946
|
+
trackPass(new Promise((resolve) => passDone = resolve));
|
|
838
947
|
setImmediate(() => {
|
|
839
948
|
drainQueued = false;
|
|
840
949
|
requestContext.storage.exit(() => {
|
|
841
950
|
relay.relayBatch().catch((err) => {
|
|
842
951
|
fastify.log.error({ err }, `outbox relay "${name}" requested drain failed`);
|
|
843
|
-
});
|
|
952
|
+
}).finally(passDone);
|
|
844
953
|
});
|
|
845
954
|
});
|
|
846
955
|
};
|
|
956
|
+
const drain = ({ maxPasses = 100 } = {}) => requestContext.storage.exit(async () => {
|
|
957
|
+
const total = {
|
|
958
|
+
passes: 0,
|
|
959
|
+
relayed: 0,
|
|
960
|
+
publishFailed: 0,
|
|
961
|
+
deadLettered: 0,
|
|
962
|
+
drained: false
|
|
963
|
+
};
|
|
964
|
+
while (total.passes < maxPasses) {
|
|
965
|
+
if (inFlightPasses.size > 0) await Promise.allSettled([...inFlightPasses]);
|
|
966
|
+
const r = await relay.relayBatch();
|
|
967
|
+
total.passes++;
|
|
968
|
+
total.relayed += r.relayed;
|
|
969
|
+
total.publishFailed += r.publishFailed;
|
|
970
|
+
total.deadLettered += r.deadLettered;
|
|
971
|
+
if (r.attempted === 0 && inFlightPasses.size === 0) {
|
|
972
|
+
total.drained = true;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return total;
|
|
977
|
+
});
|
|
978
|
+
const health = async () => {
|
|
979
|
+
if (!canReportHealth(store)) throw new Error(`Outbox module "${name}": relay health needs BOTH countByStatus and oldestPendingAgeMs on the store, and it is missing at least one.`);
|
|
980
|
+
return readOutboxHealth(store, lagUnhealthyMs);
|
|
981
|
+
};
|
|
847
982
|
exports = {
|
|
848
983
|
store,
|
|
849
984
|
relay,
|
|
850
|
-
requestDrain
|
|
985
|
+
requestDrain,
|
|
986
|
+
drain,
|
|
987
|
+
health
|
|
851
988
|
};
|
|
852
989
|
return exports;
|
|
853
990
|
},
|
|
991
|
+
...options.health === false ? {} : { healthChecks: [{
|
|
992
|
+
name: `${name}.relay`,
|
|
993
|
+
critical: false,
|
|
994
|
+
check: () => exports ? probeHealthy(exports.health) : Promise.resolve(false)
|
|
995
|
+
}] },
|
|
854
996
|
/**
|
|
855
997
|
* The relay runs on the module's OWN schedule arm, so the host stops owning the tick — and
|
|
856
998
|
* a host that forgets to start one cannot exist.
|
|
@@ -861,9 +1003,37 @@ function createOutboxModule(options) {
|
|
|
861
1003
|
every: relayEveryMs,
|
|
862
1004
|
runOnStart: options.runOnStart ?? true,
|
|
863
1005
|
handler: async () => {
|
|
864
|
-
await exports
|
|
1006
|
+
if (exports) await trackPass(exports.relay.relay());
|
|
1007
|
+
}
|
|
1008
|
+
}, ...alertEveryMs > 0 ? [{
|
|
1009
|
+
name: `${name}.health`,
|
|
1010
|
+
kind: "relay",
|
|
1011
|
+
every: alertEveryMs,
|
|
1012
|
+
handler: async () => {
|
|
1013
|
+
if (!exports || !log) return;
|
|
1014
|
+
if (!canReportHealth(exports.store)) {
|
|
1015
|
+
if (!warnedUnmeasurable) {
|
|
1016
|
+
warnedUnmeasurable = true;
|
|
1017
|
+
log.warn({
|
|
1018
|
+
marker: "OUTBOX_HEALTH_UNMEASURABLE",
|
|
1019
|
+
outbox: name
|
|
1020
|
+
}, `outbox "${name}" store lacks countByStatus or oldestPendingAgeMs — relay stalls cannot be alerted on`);
|
|
1021
|
+
}
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const h = await exports.health();
|
|
1025
|
+
if (h.deadLetter > 0) log.warn({
|
|
1026
|
+
marker: "OUTBOX_DEAD_LETTER",
|
|
1027
|
+
outbox: name,
|
|
1028
|
+
...h
|
|
1029
|
+
}, `outbox "${name}" has dead-lettered events — inspect and replay them`);
|
|
1030
|
+
if (h.relayLagMs !== null && h.relayLagMs >= lagUnhealthyMs) log.error({
|
|
1031
|
+
marker: "OUTBOX_RELAY_LAG",
|
|
1032
|
+
outbox: name,
|
|
1033
|
+
...h
|
|
1034
|
+
}, `outbox "${name}" relay is behind — events are committed but not delivered`);
|
|
865
1035
|
}
|
|
866
|
-
}]
|
|
1036
|
+
}] : []]
|
|
867
1037
|
};
|
|
868
1038
|
}
|
|
869
1039
|
//#endregion
|