@classytic/arc 2.22.0 → 2.25.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.
Files changed (147) hide show
  1. package/dist/{BaseController-C6FlRWxF.mjs → BaseController--M8EMzt0.mjs} +92 -22
  2. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  3. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  4. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  5. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
  6. package/dist/audit/index.d.mts +32 -3
  7. package/dist/audit/index.mjs +7 -4
  8. package/dist/auth/index.d.mts +2 -2
  9. package/dist/auth/index.mjs +264 -260
  10. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  11. package/dist/cache/index.d.mts +41 -3
  12. package/dist/cache/index.mjs +33 -4
  13. package/dist/cli/commands/describe.d.mts +1 -1
  14. package/dist/cli/commands/describe.mjs +1 -1
  15. package/dist/cli/commands/docs.mjs +2 -2
  16. package/dist/cli/commands/generate.mjs +1 -1
  17. package/dist/cli/commands/init.mjs +1 -1
  18. package/dist/cli/commands/introspect.mjs +1 -1
  19. package/dist/cli/index.mjs +2 -2
  20. package/dist/context/index.mjs +2 -64
  21. package/dist/core/index.d.mts +3 -3
  22. package/dist/core/index.mjs +4 -4
  23. package/dist/{core-b6fgFgnd.mjs → core-EUNu0u07.mjs} +298 -12
  24. package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-B6GvAuAX.mjs} +4 -3
  25. package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  26. package/dist/docs/index.d.mts +1 -1
  27. package/dist/docs/index.mjs +2 -2
  28. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  29. package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
  30. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  31. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  32. package/dist/events/index.d.mts +57 -314
  33. package/dist/events/index.mjs +168 -93
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  36. package/dist/factory/index.d.mts +21 -6
  37. package/dist/factory/index.mjs +58 -18
  38. package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
  39. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  40. package/dist/headers-BvlQSDMB.mjs +45 -0
  41. package/dist/health-CB_m4S32.mjs +377 -0
  42. package/dist/hooks/index.d.mts +1 -1
  43. package/dist/idempotency/index.d.mts +7 -0
  44. package/dist/idempotency/index.mjs +32 -20
  45. package/dist/{index-ogIHvOjo.d.mts → index-BMnbzvtO.d.mts} +70 -4
  46. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  47. package/dist/{index-DbtgCOCj.d.mts → index-DPRH0Cme.d.mts} +46 -94
  48. package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
  49. package/dist/{index-D8MKYGDa.d.mts → index-f4PTrk3B.d.mts} +37 -5
  50. package/dist/index.d.mts +6 -6
  51. package/dist/index.mjs +6 -5
  52. package/dist/{init-DleK3p9f.mjs → init-tlRSt667.mjs} +206 -205
  53. package/dist/integrations/event-gateway.d.mts +1 -1
  54. package/dist/integrations/event-gateway.mjs +1 -1
  55. package/dist/integrations/index.d.mts +3 -3
  56. package/dist/integrations/jobs/index.d.mts +2 -0
  57. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  58. package/dist/integrations/mcp/index.d.mts +68 -5
  59. package/dist/integrations/mcp/index.mjs +2 -2
  60. package/dist/integrations/mcp/testing.d.mts +1 -1
  61. package/dist/integrations/mcp/testing.mjs +1 -1
  62. package/dist/integrations/webhooks.d.mts +149 -22
  63. package/dist/integrations/webhooks.mjs +186 -42
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +41 -14
  67. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  68. package/dist/interface-n5hpKg1d.mjs +16 -0
  69. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  70. package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-Cb7olw0j.mjs} +316 -45
  71. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  72. package/dist/middleware/index.d.mts +17 -1
  73. package/dist/middleware/index.mjs +1 -1
  74. package/dist/migrations/index.d.mts +94 -44
  75. package/dist/migrations/index.mjs +196 -44
  76. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  77. package/dist/{openapi-BRpMZD7C.mjs → openapi-DrLtog_B.mjs} +4 -4
  78. package/dist/permissions/index.d.mts +3 -3
  79. package/dist/permissions/index.mjs +4 -3
  80. package/dist/{permissions-BJMuKMl7.mjs → permissions-BrtiBKGs.mjs} +305 -492
  81. package/dist/pipeline/index.d.mts +1 -1
  82. package/dist/{actionPermissions-ze70xXJi.mjs → pipeline-BN5G05rd.mjs} +4 -27
  83. package/dist/plugins/index.d.mts +7 -78
  84. package/dist/plugins/index.mjs +14 -97
  85. package/dist/plugins/response-cache.mjs +1 -0
  86. package/dist/plugins/tracing-entry.mjs +1 -1
  87. package/dist/{index-BEu4wjbL.d.mts → preloadResources-0oe-JSSS.d.mts} +59 -50
  88. package/dist/{createApp-Ci7yG5gv.mjs → preloadResources-DfyngIN9.mjs} +240 -68
  89. package/dist/presets/filesUpload.d.mts +3 -3
  90. package/dist/presets/filesUpload.mjs +3 -3
  91. package/dist/presets/index.d.mts +2 -1
  92. package/dist/presets/index.mjs +1 -1
  93. package/dist/presets/multiTenant.d.mts +1 -1
  94. package/dist/presets/multiTenant.mjs +1 -1
  95. package/dist/presets/search.d.mts +2 -2
  96. package/dist/presets/search.mjs +1 -1
  97. package/dist/{presets-gw0ciIUD.mjs → presets-DwFDVKP0.mjs} +2 -2
  98. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  99. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  100. package/dist/{registerRoute-xHc42fwB.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  101. package/dist/realtime-Cuw_deA-.mjs +375 -0
  102. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  103. package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
  104. package/dist/registry/index.d.mts +2 -2
  105. package/dist/registry/index.mjs +1 -1
  106. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  107. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  108. package/dist/schedules-CjOd2MG9.mjs +113 -0
  109. package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
  110. package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
  111. package/dist/schemas/index.d.mts +67 -67
  112. package/dist/schemas/index.mjs +2 -3
  113. package/dist/scim/index.mjs +1 -2
  114. package/dist/scope/index.d.mts +2 -2
  115. package/dist/scope/index.mjs +4 -4
  116. package/dist/scope-DfaPntpo.mjs +36 -0
  117. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  118. package/dist/{sse-CfUjwjDk.mjs → sse-BQ1w0dfl.mjs} +8 -31
  119. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  120. package/dist/testing/index.d.mts +4 -3
  121. package/dist/testing/index.mjs +2 -2
  122. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  123. package/dist/types/index.d.mts +4 -4
  124. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  125. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  126. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  127. package/dist/{types-Bc42CUyk.d.mts → types-QFGc3Cjv.d.mts} +37 -2
  128. package/dist/usage/index.d.mts +3 -52
  129. package/dist/usage/index.mjs +0 -0
  130. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  131. package/dist/utils/index.d.mts +90 -57
  132. package/dist/utils/index.mjs +7 -3
  133. package/dist/{utils-FhJ88TJE.mjs → utils-taUVD4p3.mjs} +47 -385
  134. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  135. package/dist/{versioning-DLwklPlG.d.mts → versioning-DXgY5BUm.d.mts} +52 -31
  136. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  137. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  138. package/package.json +526 -453
  139. package/skills/arc/references/api-reference.md +3 -1
  140. package/skills/arc/references/integrations.md +20 -11
  141. package/skills/arc/references/production.md +734 -696
  142. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  143. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  144. package/dist/health-ScyjIVo5.mjs +0 -202
  145. package/dist/requestId-5BhTVzBv.mjs +0 -113
  146. package/dist/scope-k15RXfcq.mjs +0 -64
  147. package/dist/types-XTApa8b6.mjs +0 -24
@@ -1,66 +1,128 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
2
  import { arcLog, configureArcLogger, createPinoWriter } from "./logger/index.mjs";
3
- import { n as PUBLIC_SCOPE } from "./types-bHblZ9YN.mjs";
3
+ import { n as PUBLIC_SCOPE } from "./types-CSyCxl2V.mjs";
4
4
  import { t as createTenantKeyGenerator } from "./rateLimitKey-BoWRH3e7.mjs";
5
- import { t as createRequestIdGenerator } from "./requestId-5BhTVzBv.mjs";
5
+ import { i as createRequestIdGenerator, r as mergeHealthChecks } from "./health-CB_m4S32.mjs";
6
6
  import { t as parseJsonBody } from "./jsonBody-BlzAoGCe.mjs";
7
7
  import Fastify from "fastify";
8
8
  import qs from "qs";
9
- //#region src/testing/preloadResources.ts
9
+ //#region src/factory/module/resolve.ts
10
10
  /**
11
- * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
12
- *
13
- * Accepts either:
14
- * - `{ import: 'default' }` form: values are the resource directly
15
- * - default form: values are the full module — picks first export with `toPlugin()`
16
- *
17
- * Throws if any module doesn't yield a valid `ResourceLike`.
11
+ * Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
12
+ * (this is where a dynamic `import()` fires); a promise (or plain module) is
13
+ * awaited. Called once per module at the start of the bootstrap phase.
18
14
  */
19
- function preloadResources(globResult) {
20
- const resources = [];
21
- for (const [path, value] of Object.entries(globResult)) {
22
- const resource = pickResource(value);
23
- if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
24
- resources.push(resource);
25
- }
26
- return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
15
+ async function resolveModule(input) {
16
+ const resolved = typeof input === "function" ? await input() : await input;
17
+ if (!resolved || typeof resolved !== "object" || typeof resolved.name !== "string") throw new Error("[arc] a `modules` entry resolved to something that is not an ArcModule (expected an object with a string `name`). Check dynamic-import thunks return `createXModule(deps)` (or the module object), not the namespace.");
18
+ return resolved;
19
+ }
20
+ /** Resolve a `ModuleContribution` to a concrete array (array passthrough or factory call). */
21
+ async function resolveContribution(contribution, fastify) {
22
+ if (!contribution) return [];
23
+ return typeof contribution === "function" ? await contribution(fastify) : contribution;
27
24
  }
25
+ //#endregion
26
+ //#region src/factory/module/contributions.ts
28
27
  /**
29
- * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
30
- *
31
- * Use this when resources depend on prior bootstrap (e.g., engine init) and
32
- * cannot be evaluated at import time of the preload file.
28
+ * Collect every module's `healthChecks` in the caller's order. Fails at boot
29
+ * on a duplicate check `name` across modules, naming BOTH owning modules.
30
+ * The host's app-level checks are appended AFTER this (modules-first,
31
+ * host-last) by the caller.
33
32
  */
34
- async function preloadResourcesAsync(globResult) {
35
- return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
36
- const resource = pickResource(await loader());
37
- if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
38
- return resource;
39
- }))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
33
+ function collectModuleHealthChecks(modules) {
34
+ return mergeHealthChecks(modules.map((module) => ({
35
+ owner: `module "${module.name}"`,
36
+ checks: module.healthChecks
37
+ })));
40
38
  }
41
- function pickResource(value) {
42
- if (!value || typeof value !== "object") return void 0;
43
- if (typeof value.toPlugin === "function") return value;
44
- const mod = value;
45
- const candidates = [
46
- mod.default,
47
- mod.resource,
48
- ...Object.values(mod)
49
- ];
50
- for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
39
+ /**
40
+ * Collect every module's `workflows`. Resolves factory contributions (so a
41
+ * factory can close over a container the caller has already decorated on
42
+ * `fastify`) and flattens to a single opaque array. Arc core deliberately
43
+ * does NOT validate or interpret the values — the streamline integration
44
+ * owns name/shape checks and registration. Called by the integration at init
45
+ * time, not by arc's boot pipeline (workflows depend on the integration's
46
+ * container).
47
+ */
48
+ async function collectModuleWorkflows(fastify, modules) {
49
+ const out = [];
50
+ for (const m of modules) {
51
+ const wfs = await resolveContribution(m.workflows, fastify);
52
+ out.push(...wfs);
53
+ }
54
+ return out;
55
+ }
56
+ /**
57
+ * Collect every module's `scheduledJobs`. Resolves factory contributions
58
+ * (after bootstraps) and fails at boot on a duplicate job `name` across
59
+ * modules. The caller passes the returned definitions to the canonical
60
+ * schedules plugin; factories must therefore be resolved only once.
61
+ */
62
+ async function collectModuleScheduledJobs(fastify, modules) {
63
+ const owner = /* @__PURE__ */ new Map();
64
+ const out = [];
65
+ for (const m of modules) {
66
+ const jobs = await resolveContribution(m.scheduledJobs, fastify);
67
+ for (const job of jobs) {
68
+ const prior = owner.get(job.name);
69
+ if (prior !== void 0) throw new Error(`[arc] duplicate scheduled-job name "${job.name}" — declared by module "${prior}" and module "${m.name}". Scheduled-job names must be unique across the module graph.`);
70
+ owner.set(job.name, m.name);
71
+ out.push(job);
72
+ }
73
+ }
74
+ return out;
51
75
  }
52
76
  //#endregion
53
- //#region src/factory/module.ts
77
+ //#region src/factory/module/lifecycle.ts
54
78
  /**
55
- * Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
56
- * (this is where a dynamic `import()` fires); a promise (or plain module) is
57
- * awaited. Called once per module at the start of the bootstrap phase.
79
+ * Subscribe every module's `eventHandlers` in dependency order (pass the
80
+ * `orderModules`-sorted list). Returns the ordered list of unsubscribe
81
+ * functions so the caller can tear them down at shutdown BEFORE module
82
+ * `onClose` (while module deps are still alive). Fails at boot on:
83
+ * - a duplicate NAMED handler across modules (attributing both owners);
84
+ * - a module declaring handlers while the event subsystem is unavailable.
85
+ * Resolves factory contributions (after bootstraps) so a handler can close over
86
+ * booted engines rather than a global getter.
58
87
  */
59
- async function resolveModule(input) {
60
- const resolved = typeof input === "function" ? await input() : await input;
61
- if (!resolved || typeof resolved !== "object" || typeof resolved.name !== "string") throw new Error("[arc] a `modules` entry resolved to something that is not an ArcModule (expected an object with a string `name`). Check dynamic-import thunks return `createXModule(deps)` (or the module object), not the namespace.");
62
- return resolved;
88
+ async function subscribeModuleEventHandlers(fastify, modules) {
89
+ const unsubscribes = [];
90
+ const owner = /* @__PURE__ */ new Map();
91
+ try {
92
+ for (const m of modules) {
93
+ const handlers = await resolveContribution(m.eventHandlers, fastify);
94
+ if (handlers.length === 0) continue;
95
+ const bus = fastify.events;
96
+ if (!bus) throw new Error(`[arc] module "${m.name}" declares eventHandlers but the event subsystem is unavailable (fastify.events). Enable arcPlugins.events.`);
97
+ for (const def of handlers) {
98
+ if (def.name !== void 0) {
99
+ const prior = owner.get(def.name);
100
+ if (prior !== void 0) throw new Error(`[arc] duplicate event-handler name "${def.name}" — declared by module "${prior}" and module "${m.name}". Named event handlers must be unique across the module graph; prefix names with the owning module.`);
101
+ owner.set(def.name, m.name);
102
+ }
103
+ const patterns = Array.isArray(def.event) ? def.event : [def.event];
104
+ for (const pattern of patterns) unsubscribes.push(await bus.subscribe(pattern, def.handler));
105
+ }
106
+ }
107
+ return unsubscribes;
108
+ } catch (err) {
109
+ const rollbackErrors = await unsubscribeModuleEventHandlers(unsubscribes);
110
+ for (const rollbackError of rollbackErrors) fastify.log.error({ err: rollbackError }, "[arc] module event-handler activation rollback failed");
111
+ throw err;
112
+ }
113
+ }
114
+ /** Best-effort reverse-order teardown; every unsubscribe is attempted. */
115
+ async function unsubscribeModuleEventHandlers(unsubscribes) {
116
+ const errors = [];
117
+ for (let index = unsubscribes.length - 1; index >= 0; index--) try {
118
+ await unsubscribes[index]?.();
119
+ } catch (err) {
120
+ errors.push(err);
121
+ }
122
+ return errors;
63
123
  }
124
+ //#endregion
125
+ //#region src/factory/module/order.ts
64
126
  /**
65
127
  * Order modules for composition by their `dependsOn` edges — a STABLE
66
128
  * topological sort. Called once at the start of the bootstrap phase; every
@@ -151,6 +213,8 @@ function describeModuleCycle(remaining, byName) {
151
213
  }
152
214
  return `[arc] module dependency cycle: ${cycle ? cycle.join(" → ") : [...remaining].join(", ")}. Modules cannot dependsOn each other circularly — break it with a shared module both point at, or wire the softer direction through an event/port instead of a hard dependsOn.`;
153
215
  }
216
+ //#endregion
217
+ //#region src/factory/module/index.ts
154
218
  /**
155
219
  * Identity helper for authoring a typed module — mirrors `defineResource`.
156
220
  * Gives inference + a single obvious construction site; does no work.
@@ -200,7 +264,7 @@ const productionPreset = {
200
264
  censor: "[REDACTED]"
201
265
  }
202
266
  },
203
- trustProxy: true,
267
+ trustProxy: false,
204
268
  helmet: { contentSecurityPolicy: { directives: {
205
269
  defaultSrc: ["'self'"],
206
270
  styleSrc: ["'self'", "'unsafe-inline'"],
@@ -237,6 +301,7 @@ const productionPreset = {
237
301
  underPressure: {
238
302
  exposeStatusRoute: true,
239
303
  maxEventLoopDelay: 3e3,
304
+ maxEventLoopUtilization: .98,
240
305
  maxHeapUsedBytes: 1024 * 1024 * 1024,
241
306
  maxRssBytes: 1024 * 1024 * 1024
242
307
  }
@@ -402,7 +467,7 @@ async function registerArcCore(fastify, config, trackPlugin) {
402
467
  await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
403
468
  trackPlugin("arc-core");
404
469
  if (config.arcPlugins?.events !== false) {
405
- const { default: eventPlugin } = await import("./eventPlugin-DJG2D7sy.mjs").then((n) => n.n);
470
+ const { default: eventPlugin } = await import("./eventPlugin-DrQ_FesK.mjs").then((n) => n.n);
406
471
  const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
407
472
  await fastify.register(eventPlugin, {
408
473
  ...eventOpts,
@@ -423,16 +488,27 @@ async function registerArcCore(fastify, config, trackPlugin) {
423
488
  *
424
489
  * @param modules - Plugin modules loaded by registerArcCore (avoids re-importing)
425
490
  */
426
- async function registerArcPlugins(fastify, config, trackPlugin, modules) {
491
+ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleHealthChecks = []) {
427
492
  const { requestIdPlugin, healthPlugin, gracefulShutdownPlugin } = modules;
428
493
  if (config.arcPlugins?.requestId !== false) {
429
494
  await fastify.register(requestIdPlugin);
430
495
  trackPlugin("arc-request-id");
431
496
  }
432
497
  if (config.arcPlugins?.health !== false) {
433
- const healthOpts = typeof config.arcPlugins?.health === "object" && config.arcPlugins.health !== null ? config.arcPlugins.health : {};
498
+ const appHealth = typeof config.arcPlugins?.health === "object" && config.arcPlugins.health !== null ? config.arcPlugins.health : {};
499
+ const checks = mergeHealthChecks([{
500
+ owner: "the module graph",
501
+ checks: moduleHealthChecks
502
+ }, {
503
+ owner: "arcPlugins.health",
504
+ checks: appHealth.checks
505
+ }]);
506
+ const healthOpts = {
507
+ ...appHealth,
508
+ checks
509
+ };
434
510
  await fastify.register(healthPlugin, healthOpts);
435
- trackPlugin("arc-health", healthOpts);
511
+ trackPlugin("arc-health", { checkCount: healthOpts.checks?.length ?? 0 });
436
512
  }
437
513
  if (config.arcPlugins?.gracefulShutdown !== false) {
438
514
  await fastify.register(gracefulShutdownPlugin);
@@ -445,9 +521,9 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
445
521
  trackPlugin("arc-caching", opts);
446
522
  }
447
523
  if (config.arcPlugins?.queryCache) {
448
- const { queryCachePlugin } = await import("./queryCachePlugin-DzqLN904.mjs").then((n) => n.n);
524
+ const { queryCachePlugin } = await import("./queryCachePlugin-DSGuSVLW.mjs").then((n) => n.n);
449
525
  const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
450
- const store = config.stores?.queryCache ?? new (await (import("./memory-CLXA2VeJ.mjs").then((n) => n.n))).MemoryCacheStore();
526
+ const store = config.stores?.queryCache ?? new (await (import("./memory-CmnzXE5Z.mjs").then((n) => n.n))).MemoryCacheStore();
451
527
  await fastify.register(queryCachePlugin, {
452
528
  store,
453
529
  ...opts
@@ -456,11 +532,18 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
456
532
  }
457
533
  if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
458
534
  else {
459
- const { default: ssePlugin } = await import("./sse-CfUjwjDk.mjs").then((n) => n.r);
535
+ const { default: ssePlugin } = await import("./sse-BQ1w0dfl.mjs").then((n) => n.r);
460
536
  const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
461
537
  await fastify.register(ssePlugin, opts);
462
538
  trackPlugin("arc-sse", opts);
463
539
  }
540
+ if (config.arcPlugins?.realtime) if (config.arcPlugins?.events === false) fastify.log.warn("Realtime plugin requires events plugin (arcPlugins.events). Realtime disabled.");
541
+ else {
542
+ const { default: realtimePlugin } = await import("./realtime-Cuw_deA-.mjs").then((n) => n.r);
543
+ const opts = config.arcPlugins.realtime === true ? {} : config.arcPlugins.realtime;
544
+ await fastify.register(realtimePlugin, opts);
545
+ trackPlugin("arc-realtime", opts);
546
+ }
464
547
  if (config.arcPlugins?.metrics) {
465
548
  const { default: metricsPlugin } = await import("./metrics-fdxI5mn_.mjs").then((n) => n.r);
466
549
  const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
@@ -468,7 +551,7 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
468
551
  trackPlugin("arc-metrics", opts);
469
552
  }
470
553
  if (config.arcPlugins?.versioning) {
471
- const { default: versioningPlugin } = await import("./versioning-Dk-71iIz.mjs").then((n) => n.r);
554
+ const { default: versioningPlugin } = await import("./versioning-gKWyryT3.mjs").then((n) => n.r);
472
555
  await fastify.register(versioningPlugin, config.arcPlugins.versioning);
473
556
  trackPlugin("arc-versioning", config.arcPlugins.versioning);
474
557
  }
@@ -537,7 +620,7 @@ async function registerAuth(fastify, config, trackPlugin) {
537
620
  */
538
621
  async function registerElevation(fastify, config, trackPlugin) {
539
622
  if (!config.elevation) return;
540
- const { elevationPlugin } = await import("./elevation-COpYKt2h.mjs").then((n) => n.r);
623
+ const { elevationPlugin } = await import("./elevation-CIz1GjGm.mjs").then((n) => n.r);
541
624
  await fastify.register(elevationPlugin, config.elevation);
542
625
  trackPlugin("arc-elevation", config.elevation);
543
626
  fastify.log.debug("Elevation plugin enabled");
@@ -552,7 +635,7 @@ async function registerElevation(fastify, config, trackPlugin) {
552
635
  */
553
636
  async function registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers) {
554
637
  if (config.errorHandler === false) return;
555
- const { errorHandlerPlugin } = await import("./errorHandler-BTzXSwIg.mjs").then((n) => n.r);
638
+ const { errorHandlerPlugin } = await import("./errorHandler-Bq2OeEQL.mjs").then((n) => n.r);
556
639
  let errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
557
640
  if (moduleErrorMappers && moduleErrorMappers.length > 0) errorOpts = {
558
641
  ...errorOpts,
@@ -773,9 +856,34 @@ async function registerResources(fastify, config, preResolvedModules) {
773
856
  throw new Error(`[arc] module "${m.name}" afterResources() threw: ${msg}`, { cause: err });
774
857
  }
775
858
  }
776
- if (config.afterResources) {
859
+ const scheduledJobs = await collectModuleScheduledJobs(fastify, modules);
860
+ if (scheduledJobs.length > 0) {
861
+ if (config.arcPlugins?.schedules === false) throw new Error("[arc] modules declare scheduledJobs but arcPlugins.schedules is false. Enable the scheduler or remove the declarations.");
862
+ if (fastify.hasDecorator("getScheduleStats")) throw new Error("[arc] modules declare scheduledJobs but schedulesPlugin was already registered manually. Configure their runner through arcPlugins.schedules so Arc can compose one schedule table.");
863
+ const frozenJobs = Object.freeze(scheduledJobs.map((job) => Object.freeze({ ...job })));
864
+ const scheduleOptions = typeof config.arcPlugins?.schedules === "object" ? config.arcPlugins.schedules : {};
865
+ const { default: schedulesPlugin } = await import("./schedules-CjOd2MG9.mjs").then((n) => n.r);
866
+ await fastify.register(schedulesPlugin, {
867
+ ...scheduleOptions,
868
+ schedules: frozenJobs
869
+ });
870
+ if (fastify.arc) {
871
+ fastify.arc.scheduledJobs = frozenJobs;
872
+ fastify.arc.plugins.set("arc-schedules", {
873
+ name: "arc-schedules",
874
+ options: { scheduleCount: frozenJobs.length },
875
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
876
+ });
877
+ }
878
+ }
879
+ const eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules);
880
+ if (config.afterResources) try {
777
881
  await config.afterResources(fastify);
778
882
  fastify.log.debug("afterResources hook executed");
883
+ } catch (err) {
884
+ const rollbackErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
885
+ for (const rollbackError of rollbackErrors) fastify.log.error({ err: rollbackError }, "[arc] module event-handler rollback after afterResources failure failed");
886
+ throw err;
779
887
  }
780
888
  if (config.onReady) {
781
889
  const onReady = config.onReady;
@@ -785,7 +893,9 @@ async function registerResources(fastify, config, preResolvedModules) {
785
893
  }
786
894
  const closers = modules.filter((m) => m.onClose).reverse();
787
895
  const appOnClose = config.onClose;
788
- if (closers.length || appOnClose) fastify.addHook("onClose", async () => {
896
+ if (closers.length || appOnClose || eventUnsubscribes.length) fastify.addHook("onClose", async () => {
897
+ const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
898
+ for (const err of unsubscribeErrors) fastify.log.error({ err }, "[arc] module event-handler shutdown unsubscribe failed");
789
899
  for (const m of closers) await m.onClose?.(fastify);
790
900
  if (appOnClose) await appOnClose(fastify);
791
901
  });
@@ -1011,7 +1121,21 @@ async function registerUtilityPlugins(fastify, config) {
1011
1121
  if (config.preset === "production") fastify.log.warn("Response compression is not enabled (Fastify 5 stream issues). Use a reverse proxy (Nginx, Caddy, Cloudflare) for gzip/brotli in production.");
1012
1122
  if (config.underPressure !== false) {
1013
1123
  const underPressure = await loadPlugin("underPressure");
1014
- await fastify.register(underPressure, config.underPressure ?? { exposeStatusRoute: true });
1124
+ const hostConfig = config.underPressure;
1125
+ const effective = hostConfig ?? {
1126
+ exposeStatusRoute: true,
1127
+ maxEventLoopDelay: 3e3,
1128
+ maxEventLoopUtilization: .98
1129
+ };
1130
+ if (config.preset === "production" && hostConfig && ![
1131
+ "maxEventLoopDelay",
1132
+ "maxEventLoopUtilization",
1133
+ "maxHeapUsedBytes",
1134
+ "maxRssBytes",
1135
+ "pressureHandler",
1136
+ "healthCheck"
1137
+ ].some((key) => key in hostConfig)) fastify.log.warn("[arc] underPressure is configured without any pressure threshold (maxEventLoopDelay / maxEventLoopUtilization / maxHeapUsedBytes / maxRssBytes) — it will monitor but never shed load. Set at least an event-loop threshold, and size heap/RSS limits from your container memory limit.");
1138
+ await fastify.register(underPressure, effective);
1015
1139
  fastify.log.debug("Health monitoring (under-pressure) enabled");
1016
1140
  }
1017
1141
  if (config.sensible !== false) {
@@ -1184,6 +1308,8 @@ function validateDistributedRuntime(options) {
1184
1308
  hint: "a Redis-backed query-cache adapter"
1185
1309
  });
1186
1310
  }
1311
+ const schedules = options.arcPlugins?.schedules;
1312
+ if (schedules && typeof schedules === "object" && schedules.enabled !== false && !schedules.lock) deferredWarnings.push("runtime: 'distributed' — schedules configured without a `lock` adapter. EVERY replica will fire every schedule tick. Pass an ecosystem lock (e.g. createMongoLockAdapter) under arcPlugins.schedules.lock for single-firer leases.");
1187
1313
  if (missing.length > 0) {
1188
1314
  const lines = missing.map((m) => ` • ${m.key.padEnd(20)} → ${m.hint}`).join("\n");
1189
1315
  throw new Error(`[Arc] runtime: 'distributed' requires durable (Redis/MongoDB) adapters, not memory.
@@ -1220,8 +1346,10 @@ async function createApp(options) {
1220
1346
  ...options.preset ? getPreset(options.preset) : {},
1221
1347
  ...options
1222
1348
  };
1349
+ const inheritedTrustProxyDefault = options.preset === "production" && options.trustProxy === void 0;
1223
1350
  const resolvedModules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
1224
1351
  const moduleErrorMappers = resolvedModules.flatMap((m) => m.errorMappers ?? []);
1352
+ const moduleHealthChecks = collectModuleHealthChecks(resolvedModules);
1225
1353
  const resolvedLogger = resolveLoggerConfig(config.logger);
1226
1354
  const fastify = Fastify({
1227
1355
  logger: resolvedLogger,
@@ -1255,12 +1383,7 @@ async function createApp(options) {
1255
1383
  writer: createPinoWriter(fastify.log)
1256
1384
  });
1257
1385
  for (const warning of deferredWarnings) fastify.log.warn(warning);
1258
- if (config.typeProvider === "typebox") try {
1259
- const { TypeBoxValidatorCompiler } = await import("@fastify/type-provider-typebox");
1260
- fastify.setValidatorCompiler(TypeBoxValidatorCompiler);
1261
- } catch {
1262
- fastify.log.warn("typeProvider: \"typebox\" requested but @fastify/type-provider-typebox is not installed.");
1263
- }
1386
+ if (inheritedTrustProxyDefault) fastify.log.warn("[arc] preset: 'production' now defaults to trustProxy: false (fail-closed; was true before 2.24). If this app runs behind a load balancer or reverse proxy, request.ip is currently the PROXY's address — rate-limit keys collapse onto one bucket and audit logs record the wrong client. Set trustProxy explicitly: a hop count (trustProxy: 1), a CIDR allow-list ('10.0.0.0/8'), a resolver function, or false if clients truly connect directly.");
1264
1387
  fastify.removeContentTypeParser("application/json");
1265
1388
  fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
1266
1389
  try {
@@ -1280,7 +1403,12 @@ async function createApp(options) {
1280
1403
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
1281
1404
  });
1282
1405
  };
1283
- await registerArcPlugins(fastify, config, trackPlugin, await registerArcCore(fastify, config, trackPlugin));
1406
+ const arcModules = await registerArcCore(fastify, config, trackPlugin);
1407
+ if (fastify.arc) {
1408
+ fastify.arc.healthChecks = Object.freeze([...moduleHealthChecks]);
1409
+ fastify.arc.moduleDefinitions = Object.freeze([...resolvedModules]);
1410
+ }
1411
+ await registerArcPlugins(fastify, config, trackPlugin, arcModules, moduleHealthChecks);
1284
1412
  decorateRequestScope(fastify);
1285
1413
  await registerAuth(fastify, config, trackPlugin);
1286
1414
  await registerElevation(fastify, config, trackPlugin);
@@ -1338,4 +1466,48 @@ const ArcFactory = {
1338
1466
  }
1339
1467
  };
1340
1468
  //#endregion
1341
- export { edgePreset as a, testingPreset as c, getModuleExports as d, orderModules as f, preloadResourcesAsync as h, developmentPreset as i, workerPreset as l, preloadResources as m, createApp as n, getPreset as o, resolveModule as p, createApp_exports as r, productionPreset as s, ArcFactory as t, defineModule as u };
1469
+ //#region src/factory/preloadResources.ts
1470
+ /**
1471
+ * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
1472
+ *
1473
+ * Accepts either:
1474
+ * - `{ import: 'default' }` form: values are the resource directly
1475
+ * - default form: values are the full module — picks first export with `toPlugin()`
1476
+ *
1477
+ * Throws if any module doesn't yield a valid `ResourceLike`.
1478
+ */
1479
+ function preloadResources(globResult) {
1480
+ const resources = [];
1481
+ for (const [path, value] of Object.entries(globResult)) {
1482
+ const resource = pickResource(value);
1483
+ if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
1484
+ resources.push(resource);
1485
+ }
1486
+ return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
1487
+ }
1488
+ /**
1489
+ * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
1490
+ *
1491
+ * Use this when resources depend on prior bootstrap (e.g., engine init) and
1492
+ * cannot be evaluated at import time of the preload file.
1493
+ */
1494
+ async function preloadResourcesAsync(globResult) {
1495
+ return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
1496
+ const resource = pickResource(await loader());
1497
+ if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
1498
+ return resource;
1499
+ }))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
1500
+ }
1501
+ function pickResource(value) {
1502
+ if (!value || typeof value !== "object") return void 0;
1503
+ if (typeof value.toPlugin === "function") return value;
1504
+ const mod = value;
1505
+ const candidates = [
1506
+ mod.default,
1507
+ mod.resource,
1508
+ ...Object.values(mod)
1509
+ ];
1510
+ for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
1511
+ }
1512
+ //#endregion
1513
+ export { collectModuleScheduledJobs as _, createApp_exports as a, resolveModule as b, getPreset as c, workerPreset as d, defineModule as f, collectModuleHealthChecks as g, subscribeModuleEventHandlers as h, createApp as i, productionPreset as l, orderModules as m, preloadResourcesAsync as n, developmentPreset as o, getModuleExports as p, ArcFactory as r, edgePreset as s, preloadResources as t, testingPreset as u, collectModuleWorkflows as v, resolveContribution as y };
@@ -1,6 +1,6 @@
1
- import { Y as PresetResult } from "../index-DbtgCOCj.mjs";
2
- import { i as RequestScope } from "../types-DH0GupI7.mjs";
3
- import { t as PermissionCheck } from "../types-Csxga7d3.mjs";
1
+ import { Y as PresetResult } from "../index-DPRH0Cme.mjs";
2
+ import { i as RequestScope } from "../types-DjC-KUCz.mjs";
3
+ import { t as PermissionCheck } from "../types-Dmw-6Qeo.mjs";
4
4
  import { a as StorageReadResult, i as StorageReadRange, n as StorageContext, o as StorageUploadInput, r as StorageFile, t as Storage } from "../storage-DLUZ04kK.mjs";
5
5
  //#region src/presets/filesUpload.d.ts
6
6
  interface FilesUploadPresetRoutes {
@@ -1,7 +1,7 @@
1
1
  import { i as NotFoundError, u as ValidationError } from "../errors-DkO8Nk6-.mjs";
2
- import { c as getOrgId, g as getUserId } from "../types-bHblZ9YN.mjs";
3
- import { b as allowPublic, w as requireAuth } from "../permissions-BJMuKMl7.mjs";
4
- import { t as multipartBody } from "../multipartBody-CHEQSZ6G.mjs";
2
+ import { c as getOrgId, g as getUserId } from "../types-CSyCxl2V.mjs";
3
+ import { A as requireAuth, E as allowPublic } from "../permissions-BrtiBKGs.mjs";
4
+ import { t as multipartBody } from "../multipartBody-D0eBvkeq.mjs";
5
5
  //#region src/presets/filesUpload.ts
6
6
  const DEFAULT_FIELD_NAME = "file";
7
7
  const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
@@ -1,4 +1,5 @@
1
- import { Rt as IControllerResponse, U as ResourceConfig, Y as PresetResult, d as PaginationResult, qt as AnyRecord, zt as IRequestContext } from "../index-DbtgCOCj.mjs";
1
+ import { Rt as IControllerResponse, U as ResourceConfig, Y as PresetResult, d as PaginationResult, zt as IRequestContext } from "../index-DPRH0Cme.mjs";
2
+ import { c as AnyRecord } from "../types-Dmw-6Qeo.mjs";
2
3
  import { FilesUploadPresetOptions, FilesUploadPresetPermissions, FilesUploadPresetRoutes, filesUploadPreset } from "./filesUpload.mjs";
3
4
  import { MultiTenantOptions, TenantFieldSpec, multiTenantPreset } from "./multiTenant.mjs";
4
5
  import { SearchHandler, SearchPresetOptions, SearchRouteConfig, searchPreset } from "./search.mjs";
@@ -1,5 +1,5 @@
1
1
  import { multiTenantPreset } from "./multiTenant.mjs";
2
- import { a as registerPreset, c as auditedPreset, d as ownedByUserPreset, i as getPreset, l as softDeletePreset, n as flexibleMultiTenantPreset, o as treePreset, r as getAvailablePresets, s as bulkPreset, t as applyPresets, u as slugLookupPreset } from "../presets-gw0ciIUD.mjs";
2
+ import { a as registerPreset, c as auditedPreset, d as ownedByUserPreset, i as getPreset, l as softDeletePreset, n as flexibleMultiTenantPreset, o as treePreset, r as getAvailablePresets, s as bulkPreset, t as applyPresets, u as slugLookupPreset } from "../presets-DwFDVKP0.mjs";
3
3
  import { filesUploadPreset } from "./filesUpload.mjs";
4
4
  import { searchPreset } from "./search.mjs";
5
5
  export { applyPresets, auditedPreset, bulkPreset, filesUploadPreset, flexibleMultiTenantPreset, getAvailablePresets, getPreset, multiTenantPreset, ownedByUserPreset, registerPreset, searchPreset, slugLookupPreset, softDeletePreset, treePreset };
@@ -1,4 +1,4 @@
1
- import { X as CrudRouteKey, Y as PresetResult } from "../index-DbtgCOCj.mjs";
1
+ import { X as CrudRouteKey, Y as PresetResult } from "../index-DPRH0Cme.mjs";
2
2
  //#region src/presets/multiTenant.d.ts
3
3
  /**
4
4
  * One tenant dimension for multi-field filtering. Discriminated by source:
@@ -1,5 +1,5 @@
1
1
  import "../constants-DNre0wRg.mjs";
2
- import { b as isElevated, c as getOrgId, d as getScopeContext, m as getTeamId, u as getRequestScope, v as hasOrgAccess } from "../types-bHblZ9YN.mjs";
2
+ import { b as isElevated, c as getOrgId, d as getScopeContext, m as getTeamId, u as getRequestScope, v as hasOrgAccess } from "../types-CSyCxl2V.mjs";
3
3
  //#region src/presets/multiTenant.ts
4
4
  /**
5
5
  * Resolve a single TenantFieldSpec against the current scope.
@@ -1,5 +1,5 @@
1
- import { At as RouteDefinition, Pt as ControllerHandler, Y as PresetResult, jt as RouteMcpConfig } from "../index-DbtgCOCj.mjs";
2
- import { t as PermissionCheck } from "../types-Csxga7d3.mjs";
1
+ import { At as RouteDefinition, Pt as ControllerHandler, Y as PresetResult, jt as RouteMcpConfig } from "../index-DPRH0Cme.mjs";
2
+ import { t as PermissionCheck } from "../types-Dmw-6Qeo.mjs";
3
3
  //#region src/presets/search.d.ts
4
4
  /**
5
5
  * Search Preset — backend-agnostic search / vector / embed routes
@@ -1,4 +1,4 @@
1
- import { b as allowPublic, w as requireAuth } from "../permissions-BJMuKMl7.mjs";
1
+ import { A as requireAuth, E as allowPublic } from "../permissions-BrtiBKGs.mjs";
2
2
  //#region src/presets/search.ts
3
3
  const BUILTINS = [
4
4
  {
@@ -1,5 +1,5 @@
1
- import { b as isElevated, n as PUBLIC_SCOPE } from "./types-bHblZ9YN.mjs";
2
- import { E as requireRoles, b as allowPublic, w as requireAuth } from "./permissions-BJMuKMl7.mjs";
1
+ import { b as isElevated, n as PUBLIC_SCOPE } from "./types-CSyCxl2V.mjs";
2
+ import { A as requireAuth, E as allowPublic, M as requireRoles } from "./permissions-BrtiBKGs.mjs";
3
3
  import { multiTenantPreset } from "./presets/multiTenant.mjs";
4
4
  //#region src/presets/ownedByUser.ts
5
5
  /**
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
- import { t as MemoryCacheStore } from "./memory-CLXA2VeJ.mjs";
3
2
  import { t as hasEvents } from "./typeGuards-C9KXUly6.mjs";
4
3
  import { i as versionKey, r as tagVersionKey } from "./keys-Cd316u_y.mjs";
4
+ import { t as MemoryCacheStore } from "./memory-CmnzXE5Z.mjs";
5
5
  import fp from "fastify-plugin";
6
6
  //#region src/cache/QueryCache.ts
7
7
  var QueryCache = class {
@@ -33,7 +33,9 @@ var QueryCache = class {
33
33
  };
34
34
  }
35
35
  async set(key, data, config) {
36
- const staleTimeSec = config.staleTime ?? 0;
36
+ const baseStaleTimeSec = config.staleTime ?? 0;
37
+ const jitter = config.jitter ?? 0;
38
+ const staleTimeSec = jitter > 0 ? Math.max(0, baseStaleTimeSec * (1 + (Math.random() * 2 - 1) * jitter)) : baseStaleTimeSec;
37
39
  const totalTtlSec = staleTimeSec + (config.gcTime ?? 60);
38
40
  const now = Date.now();
39
41
  const envelope = {
@@ -54,9 +56,7 @@ var QueryCache = class {
54
56
  }
55
57
  /** Bump resource version — orphans all cached queries for this resource */
56
58
  async bumpResourceVersion(resource) {
57
- const key = versionKey(resource);
58
- const newVersion = Date.now();
59
- await this.store.set(key, newVersion, 1440 * 60);
59
+ await this.bumpVersion(versionKey(resource));
60
60
  }
61
61
  /** Get current version for a tag */
62
62
  async getTagVersion(tag) {
@@ -64,11 +64,42 @@ var QueryCache = class {
64
64
  }
65
65
  /** Bump tag version — orphans all cached queries tagged with this tag */
66
66
  async bumpTagVersion(tag) {
67
- const key = tagVersionKey(tag);
68
- const newVersion = Date.now();
69
- await this.store.set(key, newVersion, 1440 * 60);
67
+ await this.bumpVersion(tagVersionKey(tag));
68
+ }
69
+ /**
70
+ * Version-bump semantics:
71
+ *
72
+ * - `store.increment` present (Redis `INCR`) — atomic and strictly
73
+ * monotonic across replicas; concurrent bumps can never collide or
74
+ * go backwards.
75
+ * - fallback — read-modify-write with `max(now, current + 1)`: two
76
+ * bumps in the same millisecond and clock regressions still advance
77
+ * the version. Concurrent bumps from DIFFERENT replicas can lose one
78
+ * write, which is why distributed hosts should use an incrementing
79
+ * store (`runtime: 'distributed'` already requires a shared one).
80
+ *
81
+ * Version keys must outlive every derived cache entry — that invariant
82
+ * is what makes version invalidation sound (a version key expiring back
83
+ * to 0 would make entries written under the pre-reset version
84
+ * addressable again). The 1-year TTL applies with the repo-core
85
+ * TTL-ON-CREATE semantics, so a version key expires one year after its
86
+ * FIRST bump — safe, because a revived collision would need an entry to
87
+ * outlive that reset and entry TTLs are minutes-scale (staleTime +
88
+ * gcTime). The read-modify-write fallback uses plain `set`, which
89
+ * refreshes the TTL per bump — strictly longer-lived, equally safe.
90
+ */
91
+ async bumpVersion(key) {
92
+ if (typeof this.store.increment === "function") {
93
+ await this.store.increment(key, 1, VERSION_TTL_SECONDS);
94
+ return;
95
+ }
96
+ const current = await this.store.get(key);
97
+ const next = Math.max(Date.now(), (current ?? 0) + 1);
98
+ await this.store.set(key, next, VERSION_TTL_SECONDS);
70
99
  }
71
100
  };
101
+ /** See `bumpVersion` — effectively non-expiring, refreshed on every bump. */
102
+ const VERSION_TTL_SECONDS = 365 * 24 * 60 * 60;
72
103
  //#endregion
73
104
  //#region src/cache/queryCachePlugin.ts
74
105
  var queryCachePlugin_exports = /* @__PURE__ */ __exportAll({ queryCachePlugin: () => queryCachePlugin });
@@ -1,5 +1,5 @@
1
- import { r as CacheStore } from "./interface-C1_WLTMK.mjs";
2
- import { i as QueryCache } from "./QueryCache-Z1B38koh.mjs";
1
+ import { r as CacheStore } from "./interface-BGZCzOGL.mjs";
2
+ import { i as QueryCache } from "./QueryCache-DOMT_iq7.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  //#region src/cache/queryCachePlugin.d.ts
5
5
  interface QueryCachePluginOptions {