@classytic/arc 2.40.0 → 2.41.1
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-CYEwjyjN.mjs} +20 -13
- package/dist/{EventTransport-BPOQsEer.d.mts → EventTransport-CbUKDzZZ.d.mts} +25 -0
- package/dist/{EventTransport-BUkTwY_x.mjs → EventTransport-DSqD_Xsg.mjs} +11 -2
- package/dist/{HookSystem-96nzZLmk.mjs → HookSystem-CMF94SEA.mjs} +2 -2
- package/dist/{ResourceRegistry-B9WnuG2Q.mjs → ResourceRegistry-MGpXoxHZ.mjs} +2 -2
- package/dist/{actionPermissions-4OGbe3Hf.mjs → actionPermissions-BoPGQ5-4.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-DawKGoXb.mjs} +2 -2
- package/dist/{base-C-NL2fTi.d.mts → base-CxrV1LmG.d.mts} +10 -0
- package/dist/{buildHandler-BG_CHXqQ.mjs → buildHandler-CvxKOc0m.mjs} +4 -4
- package/dist/cache/index.d.mts +4 -2
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-Bqcn3Brl.mjs → caching-DbCBxlgw.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-D5Ubn1fe.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-5lazhva9.mjs} +3 -3
- package/dist/{createActionRouter-BR93gCKg.mjs → createActionRouter-DNqoaK81.mjs} +7 -7
- package/dist/{createAggregationRouter-CF40Aw95.mjs → createAggregationRouter-BcbQJf10.mjs} +7 -7
- package/dist/{createMcpServer-BVTs2S5G.d.mts → createMcpServer-CbDWkZ1T.d.mts} +1 -1
- package/dist/{defineAggregation-BDA2Tw7v.mjs → defineAggregation-D_eJjm5A.mjs} +1 -1
- package/dist/{defineResource-DaIySrQr.mjs → defineResource-DxkMTjRn.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-CkvG0LG2.mjs} +4 -4
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-By9keRvX.mjs → errorHandler-D1GHKGlV.mjs} +3 -3
- package/dist/{errors-BHD3L8of.mjs → errors-BH7YU_9O.mjs} +1 -1
- package/dist/{eventPlugin-DfHaKEWQ.d.mts → eventPlugin-B52abJ8W.d.mts} +1 -1
- package/dist/{eventPlugin-DB26V7uX.mjs → eventPlugin-zZY-unhr.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-BHrkoF5y.mjs} +1 -1
- package/dist/{generate-BSSdtiOy.mjs → generate-BLRf2m3y.mjs} +2 -2
- package/dist/{headers-BFEEshDS.mjs → headers-BIXWKrzQ.mjs} +2 -2
- package/dist/{health-BlGHjkqZ.mjs → health-ZRHeDHXH.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-CpLkjU18.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-YzIbF7Ii.mjs} +1 -1
- package/dist/{jsonBody-Dx6jH_Nb.mjs → jsonBody-ELR9lfeU.mjs} +2 -2
- package/dist/{keys-Ds3ekBiu.mjs → keys-cpZCtcIE.mjs} +1 -1
- package/dist/{loadResources-E5UFlRrT.mjs → loadResources-BPfWiI5s.mjs} +2 -2
- package/dist/{loadResourcesFromEntry-C2jrJbWG.mjs → loadResourcesFromEntry-ixtfL3J2.mjs} +2 -2
- package/dist/logger/index.mjs +1 -1
- package/dist/{mcpPlugin-D-3OLxNz.mjs → mcpPlugin-BX5PujI0.mjs} +15 -15
- package/dist/{memory-CNvjxzB2.mjs → memory-Iotz0cCx.mjs} +2 -2
- package/dist/{metrics-P37sNV5H.mjs → metrics-NMsmpRmY.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-DpLJG2_N.mjs} +1 -1
- package/dist/{multipartBody-Bl_gU3Wg.mjs → multipartBody-DtRWM6_U.mjs} +1 -1
- package/dist/{openapi-BYrL8zvY.mjs → openapi-CmFf3yq_.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-D2z5WkDY.mjs} +9 -6
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +5 -5
- package/dist/{permissions-C0GpDsB_.mjs → permissions-D8TBH44E.mjs} +12 -6
- package/dist/{pipe-4_XamfxI.mjs → pipe-FMWNheSO.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-CXWZIzTv.mjs} +1 -1
- package/dist/{preloadResources-DVgPCNcD.d.mts → preloadResources-CjS2OfpU.d.mts} +13 -8
- package/dist/{preloadResources-BAx5s9EF.mjs → preloadResources-f3boenVZ.mjs} +33 -26
- 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-ZJSPgmT2.mjs} +4 -4
- package/dist/{projection-qbCOEvsh.mjs → projection-BzIgzoKZ.mjs} +2 -2
- package/dist/{queryCachePlugin-2Pp9w-8D.mjs → queryCachePlugin-DZHPOm9p.mjs} +11 -5
- package/dist/{rateLimitKey-umrS6-vu.mjs → rateLimitKey-BvOvcBDi.mjs} +1 -1
- package/dist/{realtime-zzoNLDtj.mjs → realtime-8tIQWZz4.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-Bsnk6NiZ.mjs} +6 -6
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +3 -3
- package/dist/{registry-CEpoRpLP.mjs → registry-DNkgGQcK.mjs} +2 -2
- package/dist/{renewingLease-B5Grm0pI.mjs → renewingLease-yF8BdW4b.mjs} +1 -1
- package/dist/{replyHelpers-9h-a592u.mjs → replyHelpers-CNdvMfM4.mjs} +2 -2
- package/dist/{requestContext-D4PuGhEw.mjs → requestContext-cDkAUr-P.mjs} +1 -1
- package/dist/{rolldown-runtime-DzE6ikSH.mjs → rolldown-runtime-eysyzFAF.mjs} +1 -1
- package/dist/{runtimeCapabilities-Bo7rSKjm.mjs → runtimeCapabilities-CkhS6jn6.mjs} +31 -5
- package/dist/{schedules-Dm21pqHU.mjs → schedules-j6uXXAeJ.mjs} +4 -4
- package/dist/{schemaConverter-DENbixyA.mjs → schemaConverter-BVA7cFRG.mjs} +1 -1
- package/dist/{schemaIR-C2qUNzC6.mjs → schemaIR-CTvPDoPB.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-BUi1xV1I.mjs} +4 -4
- package/dist/{simpleEqualityMatcher-Cldubvul.mjs → simpleEqualityMatcher-C9bzq2gO.mjs} +1 -1
- package/dist/{sse-DbyXoQun.mjs → sse-BeqVKQKu.mjs} +5 -5
- package/dist/{sseStream-BfZtL5-m.mjs → sseStream-Ppzo-WUH.mjs} +2 -2
- package/dist/{store-helpers-BfyOd5i6.mjs → store-helpers-dW71XVWL.mjs} +1 -1
- package/dist/{streaming-CFwCzEvU.mjs → streaming-IcRFjWQU.mjs} +1 -1
- package/dist/{subscribe-helpers-KgVEcn8L.mjs → subscribe-helpers-DatDkcuG.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-CPbdlnVw.mjs} +1 -1
- package/dist/{typeGuards-t_CVgRXN.mjs → typeGuards-BLjPgVBm.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-9rvxn5Ym.d.mts → types-CWQn9-IC.d.mts} +1 -1
- package/dist/{types-CNQyOrLk.mjs → types-CgenB1gv.mjs} +4 -4
- 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-ChRvkuSZ.mjs} +1 -1
- package/dist/utils/index.d.mts +107 -5
- package/dist/utils/index.mjs +12 -12
- package/dist/{utils-CUFBrhq-.mjs → utils-CxcctrAh.mjs} +50 -9
- package/dist/{validate-DV3hRbsz.mjs → validate-DrYWnOzI.mjs} +2 -2
- package/dist/{versioning-P3V6ATyC.d.mts → versioning-D6JIK8Tk.d.mts} +58 -2
- package/dist/{versioning-BXwmj5SD.mjs → versioning-DKkT4hNJ.mjs} +3 -3
- package/dist/{workScope-CoGpuzDX.mjs → workScope-CxKqoIFn.mjs} +3 -4
- package/package.json +5 -5
- package/skills/arc/SKILL.md +8 -4
- package/skills/arc/references/production.md +94 -9
package/dist/usage/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { i as RequestScope } from "../types-Ddl9OjmE.mjs";
|
|
3
|
-
import { l as PermissionCheck, u as PermissionContext } from "../base-
|
|
3
|
+
import { l as PermissionCheck, u as PermissionContext } from "../base-CxrV1LmG.mjs";
|
|
4
4
|
import { UsageBucket, UsageStore, usagePeriod } from "@classytic/repo-core/usage";
|
|
5
5
|
import { FastifyPluginAsync, FastifyRequest } from "fastify";
|
|
6
6
|
//#region src/usage/requireQuota.d.ts
|
|
@@ -40,6 +40,62 @@ declare class MemoryUsageStore implements UsageStore {
|
|
|
40
40
|
clear(): void;
|
|
41
41
|
}
|
|
42
42
|
//#endregion
|
|
43
|
+
//#region src/usage/stores/redis.d.ts
|
|
44
|
+
/**
|
|
45
|
+
* The three commands this store needs. Fixed arity with an `unknown[]` tail
|
|
46
|
+
* where ioredis overloads one — see the note in `idempotency/stores/redis.ts`
|
|
47
|
+
* for why a narrower tail makes a real `Redis` unassignable.
|
|
48
|
+
*/
|
|
49
|
+
interface UsageRedisLike {
|
|
50
|
+
hincrby(key: string, field: string, increment: number): Promise<number>;
|
|
51
|
+
hgetall(key: string): Promise<Record<string, string>>;
|
|
52
|
+
expire(key: string, seconds: number, ...args: unknown[]): Promise<number>;
|
|
53
|
+
/**
|
|
54
|
+
* Optional. When present, a retention-enabled increment sends `HINCRBY` and
|
|
55
|
+
* `EXPIRE` as ONE round trip instead of two. ioredis has it; a minimal
|
|
56
|
+
* wrapper may not, and the store falls back to two sequential calls.
|
|
57
|
+
*/
|
|
58
|
+
pipeline?(): {
|
|
59
|
+
hincrby(key: string, field: string, increment: number): unknown;
|
|
60
|
+
expire(key: string, seconds: number): unknown;
|
|
61
|
+
exec(): Promise<unknown>;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
interface RedisUsageStoreOptions {
|
|
65
|
+
/** Redis client (ioredis or compatible). */
|
|
66
|
+
redis: UsageRedisLike;
|
|
67
|
+
/**
|
|
68
|
+
* Key prefix (default: `'arc:usage:'`).
|
|
69
|
+
*
|
|
70
|
+
* Keys are `{prefix}{actor}:{period}`. `usagePeriod()` never emits a `:`,
|
|
71
|
+
* so the split is unambiguous for every canonical period; a host that
|
|
72
|
+
* passes its own period key should keep `:` out of it.
|
|
73
|
+
*/
|
|
74
|
+
prefix?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Seconds a bucket survives after its LAST write, or `undefined` (the
|
|
77
|
+
* default) to keep counters forever.
|
|
78
|
+
*
|
|
79
|
+
* The default is deliberate and it is not the usual Redis default. These
|
|
80
|
+
* counters are what quota decisions and invoices are computed from —
|
|
81
|
+
* expiring them by default would delete billing history on a schedule
|
|
82
|
+
* nobody chose, and the loss is silent because `summary()` answers `{}` for
|
|
83
|
+
* a bucket that never existed and one that expired alike. Set it once you
|
|
84
|
+
* know your retention window (e.g. `86400 * 400` to keep a year plus a
|
|
85
|
+
* grace period), and only if the counters here are not your system of
|
|
86
|
+
* record.
|
|
87
|
+
*/
|
|
88
|
+
retentionSeconds?: number;
|
|
89
|
+
}
|
|
90
|
+
/** Period-bucketed usage counters in Redis hashes. See the module header. */
|
|
91
|
+
declare class RedisUsageStore implements UsageStore {
|
|
92
|
+
#private;
|
|
93
|
+
readonly name = "redis";
|
|
94
|
+
constructor(options: RedisUsageStoreOptions);
|
|
95
|
+
increment(bucket: UsageBucket, amount: number): Promise<void>;
|
|
96
|
+
summary(actor: string, period: string): Promise<Record<string, number>>;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
43
99
|
//#region src/usage/usagePlugin.d.ts
|
|
44
100
|
interface UsageTrackOptions {
|
|
45
101
|
/** Count every completed request as `api.requests` (default: true). */
|
|
@@ -91,4 +147,4 @@ declare module "fastify" {
|
|
|
91
147
|
declare const usagePluginFn: FastifyPluginAsync<UsagePluginOptions>;
|
|
92
148
|
declare const _default: FastifyPluginAsync<UsagePluginOptions>;
|
|
93
149
|
//#endregion
|
|
94
|
-
export { MemoryUsageStore, type QuotaOptions, type UsageBucket, type UsageMeter, type UsagePluginOptions, type UsageStore, type UsageTrackOptions, _default as default, requireQuota, usagePeriod, usagePluginFn as usagePlugin };
|
|
150
|
+
export { MemoryUsageStore, type QuotaOptions, RedisUsageStore, type RedisUsageStoreOptions, type UsageBucket, type UsageMeter, type UsagePluginOptions, type UsageRedisLike, type UsageStore, type UsageTrackOptions, _default as default, requireQuota, usagePeriod, usagePluginFn as usagePlugin };
|
package/dist/usage/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { t as ArcError } from "../errors-
|
|
3
|
-
import { c as getOrgId, g as getUserId, i as getClientId } from "../types-
|
|
1
|
+
/*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
|
|
2
|
+
import { t as ArcError } from "../errors-BH7YU_9O.mjs";
|
|
3
|
+
import { c as getOrgId, g as getUserId, i as getClientId } from "../types-CgenB1gv.mjs";
|
|
4
|
+
import { n as declareRuntimeCapability } from "../runtimeCapabilities-CkhS6jn6.mjs";
|
|
4
5
|
import { usagePeriod } from "@classytic/repo-core/usage";
|
|
5
6
|
import fp from "fastify-plugin";
|
|
6
7
|
//#region src/usage/requireQuota.ts
|
|
@@ -85,6 +86,51 @@ var MemoryUsageStore = class {
|
|
|
85
86
|
}
|
|
86
87
|
};
|
|
87
88
|
//#endregion
|
|
89
|
+
//#region src/usage/stores/redis.ts
|
|
90
|
+
/** Period-bucketed usage counters in Redis hashes. See the module header. */
|
|
91
|
+
var RedisUsageStore = class {
|
|
92
|
+
name = "redis";
|
|
93
|
+
#redis;
|
|
94
|
+
#prefix;
|
|
95
|
+
#retentionSeconds;
|
|
96
|
+
constructor(options) {
|
|
97
|
+
this.#redis = options.redis;
|
|
98
|
+
this.#prefix = options.prefix ?? "arc:usage:";
|
|
99
|
+
const retention = options.retentionSeconds;
|
|
100
|
+
if (retention !== void 0 && (!Number.isFinite(retention) || retention <= 0)) throw new Error(`[arc-usage] RedisUsageStore: retentionSeconds must be a positive number, got ${String(retention)}. Omit it to keep counters forever.`);
|
|
101
|
+
this.#retentionSeconds = retention;
|
|
102
|
+
}
|
|
103
|
+
#key(actor, period) {
|
|
104
|
+
return `${this.#prefix}${actor}:${period}`;
|
|
105
|
+
}
|
|
106
|
+
async increment(bucket, amount) {
|
|
107
|
+
const key = this.#key(bucket.actor, bucket.period);
|
|
108
|
+
const ttl = this.#retentionSeconds;
|
|
109
|
+
if (ttl === void 0) {
|
|
110
|
+
await this.#redis.hincrby(key, bucket.kind, amount);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const pipeline = this.#redis.pipeline?.();
|
|
114
|
+
if (pipeline) {
|
|
115
|
+
pipeline.hincrby(key, bucket.kind, amount);
|
|
116
|
+
pipeline.expire(key, ttl);
|
|
117
|
+
await pipeline.exec();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await this.#redis.hincrby(key, bucket.kind, amount);
|
|
121
|
+
await this.#redis.expire(key, ttl);
|
|
122
|
+
}
|
|
123
|
+
async summary(actor, period) {
|
|
124
|
+
const raw = await this.#redis.hgetall(this.#key(actor, period));
|
|
125
|
+
const out = {};
|
|
126
|
+
for (const [kind, value] of Object.entries(raw ?? {})) {
|
|
127
|
+
const n = Number(value);
|
|
128
|
+
if (Number.isFinite(n)) out[kind] = n;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
//#endregion
|
|
88
134
|
//#region src/usage/usagePlugin.ts
|
|
89
135
|
function actorFromScope(scope) {
|
|
90
136
|
if (!scope) return void 0;
|
|
@@ -113,6 +159,11 @@ const usagePluginFn = async (fastify, opts) => {
|
|
|
113
159
|
return;
|
|
114
160
|
}
|
|
115
161
|
const store = opts.store ?? new MemoryUsageStore();
|
|
162
|
+
declareRuntimeCapability(fastify, {
|
|
163
|
+
subsystem: "usage.store",
|
|
164
|
+
durability: opts.store ? "shared" : "memory",
|
|
165
|
+
detail: "default MemoryUsageStore: counters are replica-local"
|
|
166
|
+
});
|
|
116
167
|
const track = {
|
|
117
168
|
requests: true,
|
|
118
169
|
egress: false,
|
|
@@ -166,4 +217,4 @@ var usagePlugin_default = fp(usagePluginFn, {
|
|
|
166
217
|
fastify: "5.x"
|
|
167
218
|
});
|
|
168
219
|
//#endregion
|
|
169
|
-
export { MemoryUsageStore, usagePlugin_default as default, requireQuota, usagePeriod, usagePluginFn as usagePlugin };
|
|
220
|
+
export { MemoryUsageStore, RedisUsageStore, usagePlugin_default as default, requireQuota, usagePeriod, usagePluginFn as usagePlugin };
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import { F as getHeader, I as requireSingleHeaderValue, P as ArcHeaders } from "../types-Ddl9OjmE.mjs";
|
|
3
|
-
import { f as getUserId, m as normalizeRoles, p as getUserRoles, t as AnyRecord } from "../base-
|
|
4
|
-
import { $ as OpenApiSchemas, S as QueryParserInterface, b as ParsedQuery } from "../index-
|
|
3
|
+
import { f as getUserId, m as normalizeRoles, p as getUserRoles, t as AnyRecord } from "../base-CxrV1LmG.mjs";
|
|
4
|
+
import { $ as OpenApiSchemas, S as QueryParserInterface, b as ParsedQuery } from "../index-DHbyPo5j.mjs";
|
|
5
5
|
import { n as ErrorMapper } from "../errorHandler--4rSMMvq.mjs";
|
|
6
6
|
import { a as NotFoundError, c as RateLimitError, d as ValidationError, f as createDomainError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorOptions, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-BGdVwP25.mjs";
|
|
7
7
|
import { scheduleBackground } from "@classytic/repo-core/cache";
|
|
@@ -54,6 +54,53 @@ type CircuitState = (typeof CircuitState)[keyof typeof CircuitState];
|
|
|
54
54
|
* Promise<any>` would silently disable type checking everywhere `T` flows.
|
|
55
55
|
*/
|
|
56
56
|
type AnyAsyncFn = (...args: never[]) => Promise<unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Cluster-wide failure counting for a circuit.
|
|
59
|
+
*
|
|
60
|
+
* ## What this shares, and what it deliberately does not
|
|
61
|
+
*
|
|
62
|
+
* It shares the failure SIGNAL, not the state machine. Each replica still owns
|
|
63
|
+
* its own CLOSED/OPEN/HALF_OPEN transitions and its own probe scheduling; what
|
|
64
|
+
* becomes cluster-wide is the count that decides when to trip.
|
|
65
|
+
*
|
|
66
|
+
* That is the whole of the problem worth solving. Per-replica counting means a
|
|
67
|
+
* downstream outage costs `failureThreshold × replicas` real requests before
|
|
68
|
+
* anything opens — at 5 and 50 that is 250 errors a failing payment gateway
|
|
69
|
+
* absorbs while every pod independently rediscovers the same outage. Sharing
|
|
70
|
+
* the count collapses it: the first replica trips at the threshold, and every
|
|
71
|
+
* other replica trips on its NEXT failure, because the shared window is
|
|
72
|
+
* already over the line.
|
|
73
|
+
*
|
|
74
|
+
* Sharing the full state machine instead would mean electing which replica
|
|
75
|
+
* probes in HALF_OPEN — consensus, plus a lock, plus a read on the hot path.
|
|
76
|
+
* The remaining gap without it is bounded and small: one extra failure per
|
|
77
|
+
* replica, once, versus `failureThreshold` per replica every time.
|
|
78
|
+
*
|
|
79
|
+
* ## Cost
|
|
80
|
+
*
|
|
81
|
+
* ONLY the failure path calls this. A successful call never touches the
|
|
82
|
+
* network, which matters because success is the common case and this wraps
|
|
83
|
+
* things like a payment charge. `reset` is called when a circuit closes.
|
|
84
|
+
*
|
|
85
|
+
* ## Failure of the store itself
|
|
86
|
+
*
|
|
87
|
+
* Never propagates. If the shared counter is unreachable the breaker falls
|
|
88
|
+
* back to its local count — degraded to the old behaviour, which is the
|
|
89
|
+
* correct direction: a Redis blip must not disable the protection that exists
|
|
90
|
+
* for a downstream outage, and the two often arrive together.
|
|
91
|
+
*/
|
|
92
|
+
interface CircuitBreakerSharedState {
|
|
93
|
+
/** Store name for diagnostics. */
|
|
94
|
+
readonly name: string;
|
|
95
|
+
/**
|
|
96
|
+
* Record one failure for `circuit` and return the CLUSTER-wide count within
|
|
97
|
+
* the current window. Implementations expire the window so a long-ago
|
|
98
|
+
* outage does not trip a healthy circuit.
|
|
99
|
+
*/
|
|
100
|
+
recordFailure(circuit: string): Promise<number>;
|
|
101
|
+
/** Clear the window — called when this replica closes the circuit. */
|
|
102
|
+
reset(circuit: string): Promise<void>;
|
|
103
|
+
}
|
|
57
104
|
interface CircuitBreakerOptions {
|
|
58
105
|
/**
|
|
59
106
|
* Number of failures before opening circuit
|
|
@@ -117,6 +164,19 @@ interface CircuitBreakerOptions {
|
|
|
117
164
|
* Name for logging/monitoring
|
|
118
165
|
*/
|
|
119
166
|
name?: string;
|
|
167
|
+
/**
|
|
168
|
+
* Cluster-wide failure counting — see {@link CircuitBreakerSharedState}.
|
|
169
|
+
*
|
|
170
|
+
* Omit for a single process, where the local count already IS the cluster
|
|
171
|
+
* count. Under a multi-replica deployment this is what stops every replica
|
|
172
|
+
* absorbing the full threshold before anything trips.
|
|
173
|
+
*
|
|
174
|
+
* Requires a stable {@link CircuitBreakerOptions.name}: it is the key every
|
|
175
|
+
* replica must agree on. A breaker with `sharedState` and no `name` throws
|
|
176
|
+
* at construction rather than silently counting under a generated id that no
|
|
177
|
+
* other replica shares.
|
|
178
|
+
*/
|
|
179
|
+
sharedState?: CircuitBreakerSharedState;
|
|
120
180
|
}
|
|
121
181
|
interface CircuitBreakerStats {
|
|
122
182
|
name?: string;
|
|
@@ -150,6 +210,7 @@ declare class CircuitBreaker<T extends AnyAsyncFn> {
|
|
|
150
210
|
private readonly onStateChange?;
|
|
151
211
|
private readonly onError?;
|
|
152
212
|
private readonly name;
|
|
213
|
+
private readonly sharedState?;
|
|
153
214
|
private readonly fn;
|
|
154
215
|
constructor(fn: T, options?: CircuitBreakerOptions);
|
|
155
216
|
/**
|
|
@@ -171,6 +232,10 @@ declare class CircuitBreaker<T extends AnyAsyncFn> {
|
|
|
171
232
|
* Handle failed call
|
|
172
233
|
*/
|
|
173
234
|
private onFailure;
|
|
235
|
+
/** Open the circuit and schedule the next probe. */
|
|
236
|
+
private trip;
|
|
237
|
+
/** Best-effort clear of the cluster failure window. Never throws. */
|
|
238
|
+
private clearSharedWindow;
|
|
174
239
|
/**
|
|
175
240
|
* Change circuit state
|
|
176
241
|
*/
|
|
@@ -254,6 +319,42 @@ declare class CircuitBreakerRegistry {
|
|
|
254
319
|
*/
|
|
255
320
|
declare function createCircuitBreakerRegistry(): CircuitBreakerRegistry;
|
|
256
321
|
//#endregion
|
|
322
|
+
//#region src/utils/circuitBreakerRedis.d.ts
|
|
323
|
+
/**
|
|
324
|
+
* The two commands this needs. Variadic tail is `unknown[]` for the same
|
|
325
|
+
* reason as every other `*Like` shape in arc — ioredis types its commands as
|
|
326
|
+
* overloads mixing literal tokens and an optional callback, which a narrower
|
|
327
|
+
* tail matches none of. Pinned by `tests/types/ioredis-assignability.test.ts`.
|
|
328
|
+
*/
|
|
329
|
+
interface CircuitBreakerRedisLike {
|
|
330
|
+
incr(key: string): Promise<number>;
|
|
331
|
+
pexpire(key: string, milliseconds: number, ...args: unknown[]): Promise<number>;
|
|
332
|
+
del(...keys: string[]): Promise<number>;
|
|
333
|
+
}
|
|
334
|
+
interface RedisCircuitBreakerStateOptions {
|
|
335
|
+
/** Redis client (ioredis or compatible). */
|
|
336
|
+
redis: CircuitBreakerRedisLike;
|
|
337
|
+
/**
|
|
338
|
+
* Key prefix (default: `'arc:circuit:'`). Keys are `{prefix}{circuitName}`,
|
|
339
|
+
* so the circuit's `name` is what every replica must agree on.
|
|
340
|
+
*/
|
|
341
|
+
prefix?: string;
|
|
342
|
+
/**
|
|
343
|
+
* Counting window in ms (default: `60_000`, matching the breaker's default
|
|
344
|
+
* `resetTimeout`). The period over which `failureThreshold` failures mean
|
|
345
|
+
* the downstream is down.
|
|
346
|
+
*/
|
|
347
|
+
windowMs?: number;
|
|
348
|
+
}
|
|
349
|
+
/** Fixed-window cluster failure counter. See the module header. */
|
|
350
|
+
declare class RedisCircuitBreakerState implements CircuitBreakerSharedState {
|
|
351
|
+
#private;
|
|
352
|
+
readonly name = "redis";
|
|
353
|
+
constructor(options: RedisCircuitBreakerStateOptions);
|
|
354
|
+
recordFailure(circuit: string): Promise<number>;
|
|
355
|
+
reset(circuit: string): Promise<void>;
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
257
358
|
//#region src/utils/compensation.d.ts
|
|
258
359
|
/**
|
|
259
360
|
* Compensating Transaction — In-Process Rollback Primitive
|
|
@@ -1152,8 +1253,9 @@ interface RuntimeCapabilityDeclaration {
|
|
|
1152
1253
|
}
|
|
1153
1254
|
/**
|
|
1154
1255
|
* Declare a runtime capability. Callable from ANY registration context —
|
|
1155
|
-
* arc's own plugins, host `plugins()` callbacks, module `plugins` phases
|
|
1156
|
-
* which is the point: the audit
|
|
1256
|
+
* arc's own plugins, host `plugins()` callbacks, module `plugins` phases,
|
|
1257
|
+
* and encapsulated children of any of them — which is the point: the audit
|
|
1258
|
+
* sees what the constructor-time guard cannot.
|
|
1157
1259
|
*/
|
|
1158
1260
|
declare function declareRuntimeCapability(fastify: FastifyInstance, decl: RuntimeCapabilityDeclaration): void;
|
|
1159
1261
|
//#endregion
|
|
@@ -1540,4 +1642,4 @@ declare function hasEvents(instance: FastifyInstance): instance is FastifyInstan
|
|
|
1540
1642
|
events: EventsDecorator;
|
|
1541
1643
|
};
|
|
1542
1644
|
//#endregion
|
|
1543
|
-
export { ArcError, type ArcHeaders, ArcQueryParser, type ArcQueryParserOptions, CircuitBreaker, CircuitBreakerError, type CircuitBreakerOptions, CircuitBreakerRegistry, type CircuitBreakerStats, CircuitState, type CompensationDefinition, type CompensationError, type CompensationHooks, type CompensationResult, type CompensationStep, ConflictError, type ErrorOptions, type EventsDecorator, type FilterDialect, ForbiddenError, type Guard, type GuardConfig, type JsonSchema, type JsonSchemaTarget, MONGO_DIALECT, NEUTRAL_DIALECT, NotFoundError, OrgAccessDeniedError, OrgRequiredError, type PipeUIMessageStreamOptions, RateLimitError, type RuntimeCapabilityDeclaration, type SchemaIo, ServiceUnavailableError, type SseStream, type SseStreamOptions, type StateMachine, type TransitionConfig, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, ValidationError, aggregateListResponse, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, declareRuntimeCapability, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, forwardedStreamHeaders, getDefaultCrudSchemas, getHeader, getListQueryParams, getUserId, getUserRoles, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, normalizeRoles, offsetListResponse, openSseStream, paginationSchema, pipeUIMessageStreamToReply, promoteStreamTokenToHeader, queryParams, requireSingleHeaderValue, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, withCompensation };
|
|
1645
|
+
export { ArcError, type ArcHeaders, ArcQueryParser, type ArcQueryParserOptions, CircuitBreaker, CircuitBreakerError, type CircuitBreakerOptions, type CircuitBreakerRedisLike, CircuitBreakerRegistry, type CircuitBreakerSharedState, type CircuitBreakerStats, CircuitState, type CompensationDefinition, type CompensationError, type CompensationHooks, type CompensationResult, type CompensationStep, ConflictError, type ErrorOptions, type EventsDecorator, type FilterDialect, ForbiddenError, type Guard, type GuardConfig, type JsonSchema, type JsonSchemaTarget, MONGO_DIALECT, NEUTRAL_DIALECT, NotFoundError, OrgAccessDeniedError, OrgRequiredError, type PipeUIMessageStreamOptions, RateLimitError, RedisCircuitBreakerState, type RedisCircuitBreakerStateOptions, type RuntimeCapabilityDeclaration, type SchemaIo, ServiceUnavailableError, type SseStream, type SseStreamOptions, type StateMachine, type TransitionConfig, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, ValidationError, aggregateListResponse, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, declareRuntimeCapability, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, forwardedStreamHeaders, getDefaultCrudSchemas, getHeader, getListQueryParams, getUserId, getUserRoles, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, normalizeRoles, offsetListResponse, openSseStream, paginationSchema, pipeUIMessageStreamToReply, promoteStreamTokenToHeader, queryParams, requireSingleHeaderValue, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, withCompensation };
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createDomainError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-
|
|
3
|
-
import { n as requireSingleHeaderValue, t as getHeader } from "../headers-
|
|
4
|
-
import { n as getUserRoles, r as normalizeRoles, t as getUserId } from "../userHelpers-
|
|
5
|
-
import { t as simpleEqualityMatcher } from "../simpleEqualityMatcher-
|
|
6
|
-
import { A as
|
|
7
|
-
import { a as isZodSchema, i as isJsonSchema, n as convertRouteSchema, o as toJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-
|
|
8
|
-
import { a as promoteStreamTokenToHeader, i as pipeUIMessageStreamToReply, n as forwardedStreamHeaders, r as isReadableStream, t as UI_MESSAGE_STREAM_HEADERS } from "../streaming-
|
|
9
|
-
import { n as declareRuntimeCapability } from "../runtimeCapabilities-
|
|
10
|
-
import { t as openSseStream } from "../sseStream-
|
|
11
|
-
import { t as hasEvents } from "../typeGuards-
|
|
12
|
-
export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, MONGO_DIALECT, NEUTRAL_DIALECT, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, ValidationError, aggregateListResponse, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, declareRuntimeCapability, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, forwardedStreamHeaders, getDefaultCrudSchemas, getHeader, getListQueryParams, getUserId, getUserRoles, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, normalizeRoles, offsetListResponse, openSseStream, paginationSchema, pipeUIMessageStreamToReply, promoteStreamTokenToHeader, queryParams, requireSingleHeaderValue, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, withCompensation };
|
|
1
|
+
/*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
|
|
2
|
+
import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createDomainError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-BH7YU_9O.mjs";
|
|
3
|
+
import { n as requireSingleHeaderValue, t as getHeader } from "../headers-BIXWKrzQ.mjs";
|
|
4
|
+
import { n as getUserRoles, r as normalizeRoles, t as getUserId } from "../userHelpers-ChRvkuSZ.mjs";
|
|
5
|
+
import { t as simpleEqualityMatcher } from "../simpleEqualityMatcher-C9bzq2gO.mjs";
|
|
6
|
+
import { A as NEUTRAL_DIALECT, C as offsetListResponse, D as ArcQueryParser, E as responses, O as createQueryParser, S as listResponse, T as queryParams, _ as errorContractSchema, a as defineCompensation, b as getListQueryParams, c as CircuitBreaker, d as CircuitState, f as createCircuitBreaker, g as deleteResponse, h as bareListResponse, i as defineErrorMapper, j as scheduleBackground, k as MONGO_DIALECT, l as CircuitBreakerError, m as aggregateListResponse, n as handleRaw, o as withCompensation, p as createCircuitBreakerRegistry, r as defineGuard, s as RedisCircuitBreakerState, t as createStateMachine, u as CircuitBreakerRegistry, v as errorDetailSchema, w as paginationSchema, x as keysetListResponse, y as getDefaultCrudSchemas } from "../utils-CxcctrAh.mjs";
|
|
7
|
+
import { a as isZodSchema, i as isJsonSchema, n as convertRouteSchema, o as toJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-BVA7cFRG.mjs";
|
|
8
|
+
import { a as promoteStreamTokenToHeader, i as pipeUIMessageStreamToReply, n as forwardedStreamHeaders, r as isReadableStream, t as UI_MESSAGE_STREAM_HEADERS } from "../streaming-IcRFjWQU.mjs";
|
|
9
|
+
import { n as declareRuntimeCapability } from "../runtimeCapabilities-CkhS6jn6.mjs";
|
|
10
|
+
import { t as openSseStream } from "../sseStream-Ppzo-WUH.mjs";
|
|
11
|
+
import { t as hasEvents } from "../typeGuards-BLjPgVBm.mjs";
|
|
12
|
+
export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, MONGO_DIALECT, NEUTRAL_DIALECT, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, RedisCircuitBreakerState, ServiceUnavailableError, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, ValidationError, aggregateListResponse, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, declareRuntimeCapability, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, forwardedStreamHeaders, getDefaultCrudSchemas, getHeader, getListQueryParams, getUserId, getUserRoles, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, normalizeRoles, offsetListResponse, openSseStream, paginationSchema, pipeUIMessageStreamToReply, promoteStreamTokenToHeader, queryParams, requireSingleHeaderValue, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, withCompensation };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { m as RESERVED_QUERY_PARAMS } from "./constants-
|
|
1
|
+
/*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
|
|
2
|
+
import { m as RESERVED_QUERY_PARAMS } from "./constants-D5Ubn1fe.mjs";
|
|
3
3
|
import { arcLog } from "./logger/index.mjs";
|
|
4
|
-
import { t as ArcError, u as ValidationError } from "./errors-
|
|
4
|
+
import { t as ArcError, u as ValidationError } from "./errors-BH7YU_9O.mjs";
|
|
5
5
|
import { scheduleBackground } from "@classytic/repo-core/cache";
|
|
6
6
|
import { errorContractSchema as errorContractSchema$1, errorDetailSchema as errorDetailSchema$1 } from "@classytic/repo-core/errors";
|
|
7
7
|
//#region src/utils/filter-dialect.ts
|
|
@@ -971,9 +971,12 @@ var CircuitBreaker = class {
|
|
|
971
971
|
onStateChange;
|
|
972
972
|
onError;
|
|
973
973
|
name;
|
|
974
|
+
sharedState;
|
|
974
975
|
fn;
|
|
975
976
|
constructor(fn, options = {}) {
|
|
976
977
|
this.fn = fn;
|
|
978
|
+
if (options.sharedState && !options.name) throw new Error("[arc] CircuitBreaker: `sharedState` requires an explicit `name` — it is the key every replica counts under. Without it each replica would count under its own key.");
|
|
979
|
+
this.sharedState = options.sharedState;
|
|
977
980
|
this.failureThreshold = options.failureThreshold ?? 5;
|
|
978
981
|
this.resetTimeout = options.resetTimeout ?? 6e4;
|
|
979
982
|
this.timeout = options.timeout ?? 1e4;
|
|
@@ -1051,6 +1054,7 @@ var CircuitBreaker = class {
|
|
|
1051
1054
|
if (this.successes >= this.successThreshold) {
|
|
1052
1055
|
this.setState(CircuitState.CLOSED);
|
|
1053
1056
|
this.successes = 0;
|
|
1057
|
+
this.clearSharedWindow();
|
|
1054
1058
|
}
|
|
1055
1059
|
}
|
|
1056
1060
|
}
|
|
@@ -1061,11 +1065,23 @@ var CircuitBreaker = class {
|
|
|
1061
1065
|
this.failures++;
|
|
1062
1066
|
this.successes = 0;
|
|
1063
1067
|
if (this.onError) this.onError(error);
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1068
|
+
const trippedLocally = this.state === CircuitState.HALF_OPEN || this.failures >= this.failureThreshold;
|
|
1069
|
+
if (trippedLocally) this.trip();
|
|
1070
|
+
if (!this.sharedState) return;
|
|
1071
|
+
this.sharedState.recordFailure(this.name).then((clusterFailures) => {
|
|
1072
|
+
if (!trippedLocally && this.state === CircuitState.CLOSED && clusterFailures >= this.failureThreshold) this.trip();
|
|
1073
|
+
}).catch(() => {});
|
|
1074
|
+
}
|
|
1075
|
+
/** Open the circuit and schedule the next probe. */
|
|
1076
|
+
trip() {
|
|
1077
|
+
this.setState(CircuitState.OPEN);
|
|
1078
|
+
this.nextAttempt = Date.now() + this.resetTimeout;
|
|
1079
|
+
this.openedAt = Date.now();
|
|
1080
|
+
}
|
|
1081
|
+
/** Best-effort clear of the cluster failure window. Never throws. */
|
|
1082
|
+
clearSharedWindow() {
|
|
1083
|
+
if (!this.sharedState) return;
|
|
1084
|
+
this.sharedState.reset(this.name).catch(() => {});
|
|
1069
1085
|
}
|
|
1070
1086
|
/**
|
|
1071
1087
|
* Change circuit state
|
|
@@ -1213,6 +1229,31 @@ function createCircuitBreakerRegistry() {
|
|
|
1213
1229
|
return new CircuitBreakerRegistry();
|
|
1214
1230
|
}
|
|
1215
1231
|
//#endregion
|
|
1232
|
+
//#region src/utils/circuitBreakerRedis.ts
|
|
1233
|
+
/** Fixed-window cluster failure counter. See the module header. */
|
|
1234
|
+
var RedisCircuitBreakerState = class {
|
|
1235
|
+
name = "redis";
|
|
1236
|
+
#redis;
|
|
1237
|
+
#prefix;
|
|
1238
|
+
#windowMs;
|
|
1239
|
+
constructor(options) {
|
|
1240
|
+
this.#redis = options.redis;
|
|
1241
|
+
this.#prefix = options.prefix ?? "arc:circuit:";
|
|
1242
|
+
const windowMs = options.windowMs ?? 6e4;
|
|
1243
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) throw new Error(`[arc] RedisCircuitBreakerState: windowMs must be a positive number, got ${String(options.windowMs)}.`);
|
|
1244
|
+
this.#windowMs = windowMs;
|
|
1245
|
+
}
|
|
1246
|
+
async recordFailure(circuit) {
|
|
1247
|
+
const key = this.#prefix + circuit;
|
|
1248
|
+
const count = await this.#redis.incr(key);
|
|
1249
|
+
if (count === 1) await this.#redis.pexpire(key, this.#windowMs);
|
|
1250
|
+
return count;
|
|
1251
|
+
}
|
|
1252
|
+
async reset(circuit) {
|
|
1253
|
+
await this.#redis.del(this.#prefix + circuit);
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
//#endregion
|
|
1216
1257
|
//#region src/utils/compensation.ts
|
|
1217
1258
|
/**
|
|
1218
1259
|
* Observability hooks must never alter the transaction: a throwing metrics
|
|
@@ -1480,4 +1521,4 @@ function createStateMachine(name, transitions = {}, options = {}) {
|
|
|
1480
1521
|
};
|
|
1481
1522
|
}
|
|
1482
1523
|
//#endregion
|
|
1483
|
-
export {
|
|
1524
|
+
export { NEUTRAL_DIALECT as A, offsetListResponse as C, ArcQueryParser as D, responses as E, createQueryParser as O, listResponse as S, queryParams as T, errorContractSchema$1 as _, defineCompensation as a, getListQueryParams as b, CircuitBreaker as c, CircuitState as d, createCircuitBreaker as f, deleteResponse as g, bareListResponse as h, defineErrorMapper as i, scheduleBackground as j, MONGO_DIALECT as k, CircuitBreakerError as l, aggregateListResponse as m, handleRaw as n, withCompensation as o, createCircuitBreakerRegistry as p, defineGuard as r, RedisCircuitBreakerState as s, createStateMachine as t, CircuitBreakerRegistry as u, errorDetailSchema$1 as v, paginationSchema as w, keysetListResponse as x, getDefaultCrudSchemas as y };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { t as __exportAll } from "./rolldown-runtime-
|
|
1
|
+
/*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-eysyzFAF.mjs";
|
|
3
3
|
//#region src/core/aggregation/normalize.ts
|
|
4
4
|
/**
|
|
5
5
|
* Framework cap on URL-driven aggregation limits when the host opts
|
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
|
|
2
|
-
import { n as DomainEvent } from "./EventTransport-
|
|
2
|
+
import { n as DomainEvent } from "./EventTransport-CbUKDzZZ.mjs";
|
|
3
3
|
import { FastifyPluginAsync, FastifyRequest } from "fastify";
|
|
4
|
+
//#region src/plugins/gracefulShutdown.d.ts
|
|
5
|
+
interface GracefulShutdownOptions {
|
|
6
|
+
/** Maximum time to wait for graceful shutdown in ms (default: 30000) */
|
|
7
|
+
timeout?: number;
|
|
8
|
+
/** Custom cleanup function called before exit */
|
|
9
|
+
onShutdown?: () => Promise<void> | void;
|
|
10
|
+
/** Signals to handle (default: ['SIGTERM', 'SIGINT']) */
|
|
11
|
+
signals?: NodeJS.Signals[];
|
|
12
|
+
/** Whether to log shutdown events (default: true) */
|
|
13
|
+
logEvents?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Lame-duck window in ms between flipping readiness and closing the
|
|
16
|
+
* server (default: 0 — close immediately, the pre-2.41 behaviour).
|
|
17
|
+
*
|
|
18
|
+
* A load balancer keeps routing to an instance until its readiness
|
|
19
|
+
* probe has failed for a few intervals; closing the moment SIGTERM
|
|
20
|
+
* arrives is the classic rolling-deploy 502. Set this to exceed the LB's
|
|
21
|
+
* deregistration window (typically 5000–15000) so `/ready` reports
|
|
22
|
+
* `draining` while the instance still answers everything routed to it.
|
|
23
|
+
* Counts against `timeout`.
|
|
24
|
+
*/
|
|
25
|
+
drainDelayMs?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Called when shutdown times out or encounters an error.
|
|
28
|
+
* Defaults to `process.exit(1)` — appropriate for production but dangerous in:
|
|
29
|
+
* - **Tests**: kills the test runner. Pass `() => {}` or `() => { throw … }`.
|
|
30
|
+
* - **Shared runtimes** (e.g., serverless): may kill unrelated handlers.
|
|
31
|
+
*
|
|
32
|
+
* @param reason - `'timeout'` if shutdown exceeded `timeout` ms,
|
|
33
|
+
* `'error'` if `onShutdown` or `fastify.close()` threw.
|
|
34
|
+
*/
|
|
35
|
+
onForceExit?: (reason: "timeout" | "error") => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Readable shutdown state — `fastify.shutdownState`. Set the moment a signal
|
|
39
|
+
* (or `fastify.shutdown()`) arrives, BEFORE the server closes, so readiness
|
|
40
|
+
* can start failing while the instance is still serving. `/live` ignores it:
|
|
41
|
+
* a draining process is alive, that is the whole point of the window.
|
|
42
|
+
*/
|
|
43
|
+
interface ShutdownState {
|
|
44
|
+
/** True from the shutdown signal onward — never resets. */
|
|
45
|
+
readonly draining: boolean;
|
|
46
|
+
/** When draining began. */
|
|
47
|
+
readonly since?: Date;
|
|
48
|
+
}
|
|
49
|
+
declare const gracefulShutdownPlugin: FastifyPluginAsync<GracefulShutdownOptions>;
|
|
50
|
+
declare module "fastify" {
|
|
51
|
+
interface FastifyInstance {
|
|
52
|
+
/** Trigger graceful shutdown manually */
|
|
53
|
+
shutdown: () => Promise<void>;
|
|
54
|
+
/** Draining flag + start time; read by the health plugin's `/ready`. */
|
|
55
|
+
shutdownState: ShutdownState;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
declare const _default$5: FastifyPluginAsync<GracefulShutdownOptions>;
|
|
59
|
+
//#endregion
|
|
4
60
|
//#region src/plugins/caching.d.ts
|
|
5
61
|
interface CachingRule {
|
|
6
62
|
/** Path prefix to match (e.g., '/api/products') */
|
|
@@ -172,4 +228,4 @@ declare module "fastify" {
|
|
|
172
228
|
declare const versioningPlugin: FastifyPluginAsync<VersioningOptions>;
|
|
173
229
|
declare const _default: FastifyPluginAsync<VersioningOptions>;
|
|
174
230
|
//#endregion
|
|
175
|
-
export { CachingRule as _, MetricsCollector as a, metricsPlugin as c, _default$2 as d, realtimePlugin as f, CachingOptions as g, ssePlugin as h, MetricEntry as i, ChangeOperation as l, _default$3 as m, _default as n, MetricsOptions as o, SSEOptions as p, versioningPlugin as r, _default$1 as s, VersioningOptions as t, RealtimeOptions as u, _default$4 as v, cachingPlugin as y };
|
|
231
|
+
export { gracefulShutdownPlugin as C, _default$5 as S, CachingRule as _, MetricsCollector as a, GracefulShutdownOptions as b, metricsPlugin as c, _default$2 as d, realtimePlugin as f, CachingOptions as g, ssePlugin as h, MetricEntry as i, ChangeOperation as l, _default$3 as m, _default as n, MetricsOptions as o, SSEOptions as p, versioningPlugin as r, _default$1 as s, VersioningOptions as t, RealtimeOptions as u, _default$4 as v, ShutdownState as x, cachingPlugin as y };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { t as __exportAll } from "./rolldown-runtime-
|
|
3
|
-
import { d as createDomainError } from "./errors-
|
|
1
|
+
/*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-eysyzFAF.mjs";
|
|
3
|
+
import { d as createDomainError } from "./errors-BH7YU_9O.mjs";
|
|
4
4
|
import fp from "fastify-plugin";
|
|
5
5
|
//#region src/plugins/versioning.ts
|
|
6
6
|
var versioning_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { n as requestContext } from "./requestContext-
|
|
1
|
+
/*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
|
|
2
|
+
import { n as requestContext } from "./requestContext-cDkAUr-P.mjs";
|
|
3
3
|
//#region src/context/workScope.ts
|
|
4
4
|
/** Enter a scope for `fn`, or run it inside the active one. */
|
|
5
5
|
function runWorkScope(seed, fn) {
|
|
@@ -20,8 +20,7 @@ function scopedValue(slot, create) {
|
|
|
20
20
|
const store = requestContext.get();
|
|
21
21
|
if (!store) return void 0;
|
|
22
22
|
const slots = store;
|
|
23
|
-
|
|
24
|
-
if (existing !== void 0) return existing;
|
|
23
|
+
if (Object.hasOwn(slots, slot)) return slots[slot];
|
|
25
24
|
const created = create();
|
|
26
25
|
slots[slot] = created;
|
|
27
26
|
return created;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.41.1",
|
|
4
4
|
"description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -353,8 +353,8 @@
|
|
|
353
353
|
"node": ">=22"
|
|
354
354
|
},
|
|
355
355
|
"peerDependencies": {
|
|
356
|
-
"@classytic/primitives": ">=0.
|
|
357
|
-
"@classytic/repo-core": ">=0.
|
|
356
|
+
"@classytic/primitives": ">=0.28.0",
|
|
357
|
+
"@classytic/repo-core": ">=0.28.0",
|
|
358
358
|
"@classytic/streamline": ">=2.8.0",
|
|
359
359
|
"@fastify/cors": ">=11.0.0",
|
|
360
360
|
"@fastify/helmet": ">=13.0.0",
|
|
@@ -489,8 +489,8 @@
|
|
|
489
489
|
"@classytic/gitkit": ">=0.1.0",
|
|
490
490
|
"@classytic/mongokit": "^3.33.0",
|
|
491
491
|
"@classytic/pgkit": "^0.1.0",
|
|
492
|
-
"@classytic/primitives": "^0.
|
|
493
|
-
"@classytic/repo-core": "^0.
|
|
492
|
+
"@classytic/primitives": "^0.28.0",
|
|
493
|
+
"@classytic/repo-core": "^0.28.0",
|
|
494
494
|
"@classytic/sqlitekit": "^0.9.0",
|
|
495
495
|
"@classytic/streamline": "^2.8.2",
|
|
496
496
|
"@electric-sql/pglite": "^0.5.4",
|
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
|
|