@classytic/arc 2.40.0 → 2.41.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/{BaseController-BqJtbiUA.mjs → BaseController-DYJRZ0EJ.mjs} +18 -12
- package/dist/{EventTransport-BPOQsEer.d.mts → EventTransport-CbUKDzZZ.d.mts} +25 -0
- package/dist/{EventTransport-BUkTwY_x.mjs → EventTransport-n7qO5Cz3.mjs} +11 -2
- package/dist/{HookSystem-96nzZLmk.mjs → HookSystem-CxvZBo77.mjs} +2 -2
- package/dist/{ResourceRegistry-B9WnuG2Q.mjs → ResourceRegistry-Cu7uTJp9.mjs} +2 -2
- package/dist/{actionPermissions-4OGbe3Hf.mjs → actionPermissions-DPwPWPOV.mjs} +7 -6
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +10 -3
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +12 -6
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-D_LFvMlm.mjs → authorizationDecision-2YY_Kaj1.mjs} +2 -2
- package/dist/{base-C-NL2fTi.d.mts → base-CxrV1LmG.d.mts} +10 -0
- package/dist/{buildHandler-BG_CHXqQ.mjs → buildHandler-CUhjGHGZ.mjs} +4 -4
- package/dist/cache/index.d.mts +4 -2
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-Bqcn3Brl.mjs → caching-DsWDjIxx.mjs} +2 -2
- package/dist/cleanup/index.d.mts +2 -2
- package/dist/cleanup/index.mjs +4 -4
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +3 -3
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -2
- package/dist/cli/commands/init.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-BQ2FbhPB.mjs → constants-BxLgNFF7.mjs} +1 -1
- package/dist/context/index.mjs +36 -11
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +7 -7
- package/dist/{core-CsZR037F.mjs → core-iNGqYerO.mjs} +3 -3
- package/dist/{createActionRouter-BR93gCKg.mjs → createActionRouter-BBlbPs3k.mjs} +7 -7
- package/dist/{createAggregationRouter-CF40Aw95.mjs → createAggregationRouter-Dv5SuK2x.mjs} +7 -7
- package/dist/{createMcpServer-BVTs2S5G.d.mts → createMcpServer-CbDWkZ1T.d.mts} +1 -1
- package/dist/{defineAggregation-BDA2Tw7v.mjs → defineAggregation-UyeurGkp.mjs} +1 -1
- package/dist/{defineResource-DaIySrQr.mjs → defineResource-DzGT-2LP.mjs} +18 -18
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +3 -3
- package/dist/{elevation-CZy6n50b.mjs → elevation-BF-vhGxY.mjs} +4 -4
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-By9keRvX.mjs → errorHandler-CIMkCOWm.mjs} +3 -3
- package/dist/{errors-BHD3L8of.mjs → errors-C_YhELhe.mjs} +1 -1
- package/dist/{eventPlugin-DfHaKEWQ.d.mts → eventPlugin-B52abJ8W.d.mts} +1 -1
- package/dist/{eventPlugin-DB26V7uX.mjs → eventPlugin-CmXaPWLv.mjs} +5 -5
- package/dist/events/index.d.mts +4 -4
- package/dist/events/index.mjs +5 -5
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +15 -6
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +2 -2
- package/dist/factory/index.d.mts +5 -5
- package/dist/factory/index.mjs +5 -5
- package/dist/{fastify-augmentation-HncZMmze.d.mts → fastify-augmentation-Cq2nHVaW.d.mts} +3 -3
- package/dist/{fields-fA-Dkou5.mjs → fields-BTyPLHK0.mjs} +1 -1
- package/dist/{generate-BSSdtiOy.mjs → generate-D_YmiCNf.mjs} +2 -2
- package/dist/{headers-BFEEshDS.mjs → headers-CNECDE6q.mjs} +2 -2
- package/dist/{health-BlGHjkqZ.mjs → health-CxQzbJmr.mjs} +10 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +2 -2
- package/dist/idempotency/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +4 -4
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-BtYoMip1.d.mts → index-BHdZD8zK.d.mts} +10 -2
- package/dist/{index-DbMglURO.d.mts → index-CFfLKMH4.d.mts} +1 -1
- package/dist/{index-eyg5WEcy.d.mts → index-CbJiYrfv.d.mts} +1 -1
- package/dist/{index-SCsOJxWB.d.mts → index-D4u76eQW.d.mts} +3 -3
- package/dist/{index-D3sIPcD4.d.mts → index-DHbyPo5j.d.mts} +6 -2
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +11 -11
- package/dist/{init-pl88PHzb.mjs → init-Y2MlSN9n.mjs} +1 -1
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +2 -2
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs/index.d.mts +1 -1
- package/dist/integrations/jobs/index.mjs +2 -2
- package/dist/integrations/mcp/index.d.mts +4 -4
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.mjs +3 -3
- package/dist/integrations/webhooks.d.mts +2 -2
- package/dist/integrations/webhooks.mjs +3 -3
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.mjs +12 -1
- package/dist/{interface-DjtFnZxH.mjs → interface-BIpgzbZl.mjs} +1 -1
- package/dist/{jsonBody-Dx6jH_Nb.mjs → jsonBody-Cq9KYl2Z.mjs} +2 -2
- package/dist/{keys-Ds3ekBiu.mjs → keys-Cmv4cBRa.mjs} +1 -1
- package/dist/{loadResources-E5UFlRrT.mjs → loadResources-CblF7xt2.mjs} +2 -2
- package/dist/{loadResourcesFromEntry-C2jrJbWG.mjs → loadResourcesFromEntry-99fIMC3p.mjs} +2 -2
- package/dist/logger/index.mjs +1 -1
- package/dist/{mcpPlugin-D-3OLxNz.mjs → mcpPlugin-xF3Gw-HW.mjs} +15 -15
- package/dist/{memory-CNvjxzB2.mjs → memory-CpDZ9NPw.mjs} +2 -2
- package/dist/{metrics-P37sNV5H.mjs → metrics-BaKnq8ag.mjs} +2 -2
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-CNcwCKUJ.mjs → module-BkQEesNQ.mjs} +1 -1
- package/dist/{multipartBody-Bl_gU3Wg.mjs → multipartBody-C92aJ8am.mjs} +1 -1
- package/dist/{openapi-BYrL8zvY.mjs → openapi-C3LFRzPq.mjs} +6 -6
- package/dist/outbox-admin/index.d.mts +2 -2
- package/dist/outbox-admin/index.mjs +4 -4
- package/dist/{outbox-module-CzsKRBvO.d.mts → outbox-module-BHdUnuuK.d.mts} +6 -2
- package/dist/{outbox-module-6DMC_WNu.mjs → outbox-module-dsCyLxXH.mjs} +9 -6
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +5 -5
- package/dist/{permissions-C0GpDsB_.mjs → permissions-DiHXy8Ck.mjs} +12 -6
- package/dist/{pipe-4_XamfxI.mjs → pipe-CBVmUJ-j.mjs} +2 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +2 -2
- package/dist/plugins/index.d.mts +6 -36
- package/dist/plugins/index.mjs +33 -22
- package/dist/plugins/response-cache.mjs +9 -2
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-BaQ62hOW.mjs → pluralize-2_U6QZQg.mjs} +1 -1
- package/dist/{preloadResources-BAx5s9EF.mjs → preloadResources-Bbp4QJ8V.mjs} +33 -26
- package/dist/{preloadResources-DVgPCNcD.d.mts → preloadResources-CjS2OfpU.d.mts} +13 -8
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +2 -2
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +38 -1
- package/dist/presets/multiTenant.mjs +24 -6
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-CAaqVVcW.mjs → presets-BdlugYTc.mjs} +4 -4
- package/dist/{projection-qbCOEvsh.mjs → projection-Uvgp-EEj.mjs} +2 -2
- package/dist/{queryCachePlugin-2Pp9w-8D.mjs → queryCachePlugin-B5F09bYr.mjs} +11 -5
- package/dist/{rateLimitKey-umrS6-vu.mjs → rateLimitKey-CK-EU2L8.mjs} +1 -1
- package/dist/{realtime-zzoNLDtj.mjs → realtime-FnnD0n3Z.mjs} +9 -9
- package/dist/{redis-CduhKsJi.d.mts → redis-Bzi1y3F9.d.mts} +10 -2
- package/dist/{redis-stream-DSsPyYzd.d.mts → redis-stream-BPKscsTB.d.mts} +6 -6
- package/dist/{registerRoute-_6zHiz0x.mjs → registerRoute-DRsSHPSM.mjs} +6 -6
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +3 -3
- package/dist/{registry-CEpoRpLP.mjs → registry-D3y8VEJ1.mjs} +2 -2
- package/dist/{renewingLease-B5Grm0pI.mjs → renewingLease-B3VYh5aU.mjs} +1 -1
- package/dist/{replyHelpers-9h-a592u.mjs → replyHelpers-DPHsdw-5.mjs} +2 -2
- package/dist/{requestContext-D4PuGhEw.mjs → requestContext-cSVKnYPk.mjs} +1 -1
- package/dist/{rolldown-runtime-DzE6ikSH.mjs → rolldown-runtime-DjDuyglE.mjs} +1 -1
- package/dist/{runtimeCapabilities-Bo7rSKjm.mjs → runtimeCapabilities-DSA_dH1_.mjs} +31 -5
- package/dist/{schedules-Dm21pqHU.mjs → schedules-BktJb3Y3.mjs} +4 -4
- package/dist/{schemaConverter-DENbixyA.mjs → schemaConverter-BLCSmNbr.mjs} +1 -1
- package/dist/{schemaIR-C2qUNzC6.mjs → schemaIR-K16Bb5BX.mjs} +2 -2
- package/dist/schemas/index.mjs +1 -1
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.mjs +5 -5
- package/dist/{scope-CdYxQVxt.mjs → scope-jSxuBTFM.mjs} +4 -4
- package/dist/{simpleEqualityMatcher-Cldubvul.mjs → simpleEqualityMatcher-BiOnyflP.mjs} +1 -1
- package/dist/{sse-DbyXoQun.mjs → sse-CH7Slxc1.mjs} +5 -5
- package/dist/{sseStream-BfZtL5-m.mjs → sseStream-CN-Lh1Tu.mjs} +2 -2
- package/dist/{store-helpers-BfyOd5i6.mjs → store-helpers-9DX9-1j_.mjs} +1 -1
- package/dist/{streaming-CFwCzEvU.mjs → streaming-dvppnxsr.mjs} +1 -1
- package/dist/{subscribe-helpers-KgVEcn8L.mjs → subscribe-helpers-AVYPgqTl.mjs} +2 -2
- package/dist/sync/changelog.mjs +1 -1
- package/dist/testing/cleanupStoreContract.mjs +1 -1
- package/dist/testing/index.d.mts +5 -5
- package/dist/testing/index.mjs +10 -10
- package/dist/testing/outboxStoreContract.mjs +1 -1
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/{transactionContext-46KyUADP.mjs → transactionContext-DCWMI6hV.mjs} +1 -1
- package/dist/{typeGuards-t_CVgRXN.mjs → typeGuards-D7tHFdH6.mjs} +1 -1
- package/dist/types/index.d.mts +2 -2
- package/dist/types/index.mjs +1 -1
- package/dist/types/storage.mjs +1 -1
- package/dist/{types-CNQyOrLk.mjs → types-CDy3-zw1.mjs} +4 -4
- package/dist/{types-9rvxn5Ym.d.mts → types-CWQn9-IC.d.mts} +1 -1
- package/dist/{types-BVlowlm7.d.mts → types-DfVDvkdP.d.mts} +19 -1
- package/dist/usage/index.d.mts +58 -2
- package/dist/usage/index.mjs +55 -4
- package/dist/{userHelpers-DLueLQdQ.mjs → userHelpers-DDMOZ_Pm.mjs} +1 -1
- package/dist/utils/index.d.mts +107 -5
- package/dist/utils/index.mjs +12 -12
- package/dist/{utils-CUFBrhq-.mjs → utils-B-zj_b__.mjs} +50 -9
- package/dist/{validate-DV3hRbsz.mjs → validate-R0G_JrnK.mjs} +2 -2
- package/dist/{versioning-P3V6ATyC.d.mts → versioning-D6JIK8Tk.d.mts} +58 -2
- package/dist/{versioning-BXwmj5SD.mjs → versioning-DuVWcu-F.mjs} +3 -3
- package/dist/{workScope-CoGpuzDX.mjs → workScope-BhsWde4k.mjs} +3 -4
- package/package.json +574 -574
- package/skills/arc/SKILL.md +8 -4
- package/skills/arc/references/production.md +94 -9
package/skills/arc/SKILL.md
CHANGED
|
@@ -725,7 +725,7 @@ await app.audit.custom('order', req.params.id, 'refund', { reason }, { user });
|
|
|
725
725
|
|
|
726
726
|
## Usage counters + plan limits (2.22)
|
|
727
727
|
|
|
728
|
-
`@classytic/arc/usage` — `usagePlugin` decorates `fastify.usage` with per-actor, per-UTC-month counters (`record`/`summary`/`period`/`actorOf`; actor chain org → user → client → ip). Auto-tracks `api.requests` (default on) + `api.egress.bytes` (opt-in); recording is fail-safe (a throwing `UsageStore` never fails a request
|
|
728
|
+
`@classytic/arc/usage` — `usagePlugin` decorates `fastify.usage` with per-actor, per-UTC-month counters (`record`/`summary`/`period`/`actorOf`; actor chain org → user → client → ip). Auto-tracks `api.requests` (default on) + `api.egress.bytes` (opt-in); recording is fail-safe (a throwing `UsageStore` never fails a request). Stores: `MemoryUsageStore` (default, single-process) and `RedisUsageStore` (2.41 — `HINCRBY`, the right shape for a hot per-request counter) both from `@classytic/arc/usage`; or a kit's (`@classytic/mongokit/usage`) when counters belong beside domain data. `RedisUsageStore` keeps counters forever by default — `retentionSeconds` is opt-in, because expiring billing history on an unchosen schedule is silent (`summary()` returns `{}` for an expired bucket and a never-written one alike). Pair with `createApp({ rateLimit: { plan: { resolve, limits: { free: { max: 60 }, enterprise: false }, default: 'free' } } })` for per-plan ceilings — `false` = unlimited, unknown/throwing resolvers fall back to `default` then global `max`, and buckets follow the tenant key chain by default. Caveat: the limiter runs before route auth, so `resolve` sees the raw request.
|
|
729
729
|
|
|
730
730
|
## Runtime capability registry (2.33)
|
|
731
731
|
|
|
@@ -737,12 +737,16 @@ import { declareRuntimeCapability } from '@classytic/arc/utils';
|
|
|
737
737
|
declareRuntimeCapability(fastify, {
|
|
738
738
|
subsystem: 'billing.sequence-cache',
|
|
739
739
|
durability: 'memory', // 'memory' | 'shared'
|
|
740
|
-
|
|
741
|
-
|
|
740
|
+
detail: 'invoice numbering cached per process',
|
|
741
|
+
// accepted: true, // ONLY when per-process is intended by design
|
|
742
|
+
// // (logged, never fatal). Must be stated by the
|
|
743
|
+
// // declarant — the audit never infers it.
|
|
742
744
|
});
|
|
743
745
|
```
|
|
744
746
|
|
|
745
|
-
`createApp`'s constructor-time guard only sees what it is passed; state a host wires inside `plugins()` was invisible to it and lived on a checklist. This makes the checklist enforcement.
|
|
747
|
+
`createApp`'s constructor-time guard only sees what it is passed; state a host wires inside `plugins()` was invisible to it and lived on a checklist. This makes the checklist enforcement. Declare from anywhere a registration reaches — `plugins()`, a module phase, or your own encapsulated `fastify.register()`; all land in the one audit (2.41).
|
|
748
|
+
|
|
749
|
+
**Since 2.41 every memory-backed default arc ships declares itself**: `auth.sessions`, `usage.store`, `audit.store`, `cache.query`, `permissions.dynamic-cache` (pass `fastify:` to the matrix — it has no handle of its own), `websocket.adapter`, `websocket.pushref-store`, `webhooks.store`. `http.response-cache` declares `accepted` (per-replica by design). So a distributed app that booted before 2.41 can now fail on a default it was silently running replica-local — the error names each one, and `references/production.md` has the store-by-store fix table.
|
|
746
750
|
|
|
747
751
|
## Adapters
|
|
748
752
|
|
|
@@ -46,23 +46,50 @@ await fastify.register(requestIdPlugin, {
|
|
|
46
46
|
|
|
47
47
|
## Graceful Shutdown Plugin
|
|
48
48
|
|
|
49
|
+
Registered by default (`arcPlugins.gracefulShutdown`). Pass an options object to configure it inline from `createApp`.
|
|
50
|
+
|
|
49
51
|
```typescript
|
|
50
52
|
import { gracefulShutdownPlugin } from '@classytic/arc/plugins';
|
|
51
53
|
|
|
52
54
|
await fastify.register(gracefulShutdownPlugin, {
|
|
53
|
-
timeout: 30000,
|
|
55
|
+
timeout: 30000, // hard cap; drainDelayMs counts against it
|
|
56
|
+
drainDelayMs: 10000, // lame-duck window — see below
|
|
54
57
|
signals: ['SIGTERM', 'SIGINT'],
|
|
55
58
|
logEvents: true,
|
|
56
59
|
onShutdown: async () => {
|
|
57
60
|
await mongoose.disconnect();
|
|
58
61
|
await redis.quit();
|
|
59
62
|
},
|
|
63
|
+
onForceExit: () => {}, // tests: stop a timeout from killing the runner
|
|
60
64
|
});
|
|
61
65
|
|
|
62
|
-
// Sequence:
|
|
63
|
-
//
|
|
66
|
+
// Sequence: signal → shutdownState.draining = true (/ready starts 503ing)
|
|
67
|
+
// → wait drainDelayMs (still serving) → fastify.close() (stops accepting,
|
|
68
|
+
// drains in-flight) → onShutdown() → exit naturally
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**`fastify.close()` runs BEFORE `onShutdown`** — so `onShutdown` is where you release what arc doesn't own (DB connections, your own Redis client), and it runs after in-flight requests are done. Don't close anything a request might still need.
|
|
72
|
+
|
|
73
|
+
### `drainDelayMs` — the rolling-deploy 502
|
|
74
|
+
|
|
75
|
+
Default `0`, which is the pre-2.41 behaviour: close the moment SIGTERM arrives. That is the classic rolling-deploy 502 — a load balancer keeps routing to an instance until its readiness probe has failed **for several intervals**, and by then the instance already stopped accepting.
|
|
76
|
+
|
|
77
|
+
`drainDelayMs` is the window between "start failing readiness" and "stop accepting":
|
|
78
|
+
|
|
79
|
+
- `/health/ready` → `503 { status: 'draining' }` immediately on signal
|
|
80
|
+
- `/health/live` → stays `200` (a draining process is alive — that's the point; a 503 here gets the pod **killed**, not drained)
|
|
81
|
+
- everything already routed here is still served for the whole window
|
|
82
|
+
|
|
83
|
+
Set it **above** your LB's deregistration window — typically 5000–15000ms. On Kubernetes, also keep `terminationGracePeriodSeconds` > `drainDelayMs + timeout`, or the kubelet SIGKILLs you mid-drain.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
createApp({
|
|
87
|
+
arcPlugins: { gracefulShutdown: { drainDelayMs: 10_000, timeout: 30_000 } },
|
|
88
|
+
});
|
|
64
89
|
```
|
|
65
90
|
|
|
91
|
+
**Do not also write `process.on('SIGTERM', () => app.close())`.** The plugin already owns those signals; a second handler closes the server immediately and skips the drain entirely — you get the 502s back while the config says you fixed them.
|
|
92
|
+
|
|
66
93
|
## Audit Plugin
|
|
67
94
|
|
|
68
95
|
Change tracking. Pass a `RepositoryLike` for persistence, or omit for in-memory dev:
|
|
@@ -464,21 +491,79 @@ const app = await createApp({
|
|
|
464
491
|
auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
|
|
465
492
|
cors: { origin: process.env.ALLOWED_ORIGINS?.split(',') ?? [], credentials: true },
|
|
466
493
|
rateLimit: { max: 100, timeWindow: '1 minute' },
|
|
467
|
-
arcPlugins: {
|
|
494
|
+
arcPlugins: {
|
|
495
|
+
queryCache: true,
|
|
496
|
+
// Behind a load balancer: fail readiness first, keep serving while it
|
|
497
|
+
// deregisters, then close. No manual process.on('SIGTERM') — the plugin
|
|
498
|
+
// owns the signals, and a second handler skips the drain.
|
|
499
|
+
gracefulShutdown: { drainDelayMs: 10_000 },
|
|
500
|
+
},
|
|
468
501
|
});
|
|
469
|
-
|
|
470
|
-
process.on('SIGTERM', () => app.close());
|
|
471
|
-
process.on('SIGINT', () => app.close());
|
|
472
502
|
```
|
|
473
503
|
|
|
474
504
|
## Distributed Runtime
|
|
475
505
|
|
|
476
|
-
`runtime: 'distributed'` only
|
|
506
|
+
`runtime: 'distributed'` is the "more than one replica" switch. It refuses to boot when something in the app holds state that only exists in **this** process. Two checks, one policy:
|
|
507
|
+
|
|
508
|
+
**1. Constructor-time guard** — what `createApp` can see in its own options, checked before any boot cost:
|
|
477
509
|
|
|
478
510
|
- `stores.events` — always required
|
|
479
511
|
- `stores.cache` — only when `arcPlugins.caching` enabled
|
|
480
512
|
- `stores.queryCache` — only when `arcPlugins.queryCache` enabled
|
|
481
|
-
- `stores.idempotency` —
|
|
513
|
+
- `stores.idempotency` — warn only (per-resource opt-in)
|
|
514
|
+
- `rateLimit` — needs a shared store: `{ redis: client }` or `{ store: CustomStore }`. Both forms count; `rateLimit: false` opts out.
|
|
515
|
+
- `arcPlugins.schedules` — needs `lock` (a `LockAdapter`, for a fleet where any replica may arm schedules) **or** `singleReplica: true` (exactly one replica arms them — the `role: 'scheduler'` deployment pinned at 1). Without one, boot fails: otherwise every replica fires every tick, and duplicate side effects are two invoices, not a stale cache. `singleReplica` is a claim about your deployment — scale that deployment past one replica and it becomes false.
|
|
516
|
+
|
|
517
|
+
**2. Capability audit** (end of boot) — everything wired inside `plugins()`, `bootstrap[]`, or a module phase, which the guard above cannot inspect. Subsystems declare via `declareRuntimeCapability`; an undeclared-shared memory default **fails the boot in one error naming every violator**, so you fix them in one pass rather than one restart each.
|
|
518
|
+
|
|
519
|
+
What declares itself, and what fixes it:
|
|
520
|
+
|
|
521
|
+
| Subsystem | Default | Shared option |
|
|
522
|
+
|---|---|---|
|
|
523
|
+
| `auth.sessions` | `MemorySessionStore` | `RedisSessionStore` (`@classytic/arc/auth/redis`) |
|
|
524
|
+
| `usage.store` | `MemoryUsageStore` | `RedisUsageStore` (`@classytic/arc/usage`) or `@classytic/mongokit/usage` |
|
|
525
|
+
| `audit.store` | `MemoryAuditStore` | `repository: yourRepo` (any kit) |
|
|
526
|
+
| `cache.query` | `MemoryCacheStore` | `RedisCacheStore` (`@classytic/arc/cache`) |
|
|
527
|
+
| `permissions.dynamic-cache` | internal memory | `cacheStore:` — **and pass `fastify:`**, the matrix has no handle of its own so it is invisible to the audit without it |
|
|
528
|
+
| `websocket.adapter` | `LocalWebSocketAdapter` | Redis adapter (`@classytic/arc/integrations/websocket-redis`) |
|
|
529
|
+
| `websocket.pushref-store` | memory | Redis pushRef store |
|
|
530
|
+
| `webhooks.store` | memory | shared store, or durable mode |
|
|
531
|
+
| `http.response-cache` | memory | **nothing — declared `accepted`.** Per-replica by design; correctness comes from short TTLs. Logged, never a failure. |
|
|
532
|
+
|
|
533
|
+
### Circuit breakers count per replica unless you say otherwise
|
|
534
|
+
|
|
535
|
+
`CircuitBreaker` state is in-process. With 50 pods and `failureThreshold: 5`, a dead downstream absorbs **250 real requests** before anything opens — each pod rediscovers the same outage independently. Share the failure window:
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
import { CircuitBreaker, RedisCircuitBreakerState } from '@classytic/arc/utils';
|
|
539
|
+
|
|
540
|
+
const shared = new RedisCircuitBreakerState({ redis }); // fixed window, default 60s
|
|
541
|
+
|
|
542
|
+
const payments = new CircuitBreaker(charge, {
|
|
543
|
+
name: 'stripe.charges', // REQUIRED with sharedState — the key every replica counts under
|
|
544
|
+
failureThreshold: 5,
|
|
545
|
+
sharedState: shared,
|
|
546
|
+
});
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
First replica trips at the threshold; every other trips on its **next** failure. It shares the failure *signal*, not the state machine — each replica still owns its own OPEN/HALF_OPEN probing, which avoids needing consensus over who probes. Nothing touches the network on the success path, the failure path never *waits* on the store, and a store that throws degrades to local counting rather than disabling the breaker.
|
|
550
|
+
|
|
551
|
+
Hosts declare their own too — a replica-local cache you built in `plugins()` belongs in the audit:
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
import { declareRuntimeCapability } from '@classytic/arc/utils';
|
|
555
|
+
|
|
556
|
+
declareRuntimeCapability(fastify, {
|
|
557
|
+
subsystem: 'billing.sequence-cache',
|
|
558
|
+
durability: 'memory', // 'memory' | 'shared'
|
|
559
|
+
detail: 'invoice numbering cached per process',
|
|
560
|
+
// accepted: true, // only when per-process is INTENDED
|
|
561
|
+
});
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Declare from anywhere a registration reaches — `plugins()`, a module phase, or inside your own encapsulated `fastify.register()`; all land in the one audit. `accepted: true` is for state that is per-process **by design**; it must be stated by the declarant, never inferred.
|
|
565
|
+
|
|
566
|
+
**Migration note:** a distributed app that booted fine before 2.41 can now fail on a default it was silently running replica-local (usage counters and audit trails were the common ones). The boot error names each. That is the bug surfacing, not a new one.
|
|
482
567
|
|
|
483
568
|
## Under-Pressure
|
|
484
569
|
|