@classytic/arc 2.23.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 (132) hide show
  1. package/dist/{BaseController-BQ8KNvWC.mjs → BaseController--M8EMzt0.mjs} +90 -21
  2. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  3. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  4. package/dist/audit/index.d.mts +32 -3
  5. package/dist/audit/index.mjs +7 -4
  6. package/dist/auth/index.d.mts +2 -2
  7. package/dist/auth/index.mjs +263 -259
  8. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  9. package/dist/cache/index.d.mts +41 -3
  10. package/dist/cache/index.mjs +33 -4
  11. package/dist/cli/commands/describe.d.mts +1 -1
  12. package/dist/cli/commands/docs.mjs +1 -1
  13. package/dist/cli/commands/generate.mjs +1 -1
  14. package/dist/cli/commands/init.mjs +1 -1
  15. package/dist/cli/index.mjs +2 -2
  16. package/dist/context/index.mjs +2 -64
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -4
  19. package/dist/{core-DQ6rDTyH.mjs → core-EUNu0u07.mjs} +296 -11
  20. package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-B6GvAuAX.mjs} +3 -2
  21. package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  25. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  26. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  27. package/dist/events/index.d.mts +57 -314
  28. package/dist/events/index.mjs +163 -91
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  31. package/dist/factory/index.d.mts +4 -5
  32. package/dist/factory/index.mjs +28 -8
  33. package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
  34. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  35. package/dist/headers-BvlQSDMB.mjs +45 -0
  36. package/dist/health-CB_m4S32.mjs +377 -0
  37. package/dist/hooks/index.d.mts +1 -1
  38. package/dist/idempotency/index.mjs +27 -3
  39. package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
  40. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  41. package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
  42. package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
  43. package/dist/{index-CC5ruDGh.d.mts → index-f4PTrk3B.d.mts} +37 -5
  44. package/dist/index.d.mts +6 -6
  45. package/dist/index.mjs +5 -5
  46. package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
  47. package/dist/integrations/event-gateway.d.mts +1 -1
  48. package/dist/integrations/event-gateway.mjs +1 -1
  49. package/dist/integrations/index.d.mts +3 -3
  50. package/dist/integrations/jobs/index.d.mts +2 -0
  51. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  52. package/dist/integrations/mcp/index.d.mts +4 -4
  53. package/dist/integrations/mcp/index.mjs +2 -2
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/integrations/webhooks.d.mts +37 -30
  57. package/dist/integrations/webhooks.mjs +51 -62
  58. package/dist/integrations/websocket-redis.d.mts +1 -1
  59. package/dist/integrations/websocket.d.mts +1 -1
  60. package/dist/integrations/websocket.mjs +41 -14
  61. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  62. package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cb7olw0j.mjs} +11 -9
  63. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  64. package/dist/middleware/index.d.mts +17 -1
  65. package/dist/middleware/index.mjs +1 -1
  66. package/dist/migrations/index.d.mts +94 -44
  67. package/dist/migrations/index.mjs +196 -44
  68. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  69. package/dist/{openapi-jSDx-VDv.mjs → openapi-DrLtog_B.mjs} +3 -3
  70. package/dist/permissions/index.d.mts +2 -2
  71. package/dist/permissions/index.mjs +2 -2
  72. package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
  73. package/dist/pipeline/index.d.mts +1 -1
  74. package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BN5G05rd.mjs} +3 -26
  75. package/dist/plugins/index.d.mts +6 -77
  76. package/dist/plugins/index.mjs +11 -95
  77. package/dist/plugins/response-cache.mjs +1 -0
  78. package/dist/plugins/tracing-entry.mjs +1 -1
  79. package/dist/{index-Ba10fdKp.d.mts → preloadResources-0oe-JSSS.d.mts} +50 -50
  80. package/dist/{createApp-CjOeWJ9_.mjs → preloadResources-DfyngIN9.mjs} +233 -70
  81. package/dist/presets/filesUpload.d.mts +3 -3
  82. package/dist/presets/filesUpload.mjs +3 -3
  83. package/dist/presets/index.d.mts +2 -1
  84. package/dist/presets/index.mjs +1 -1
  85. package/dist/presets/multiTenant.d.mts +1 -1
  86. package/dist/presets/multiTenant.mjs +1 -1
  87. package/dist/presets/search.d.mts +2 -2
  88. package/dist/presets/search.mjs +1 -1
  89. package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
  90. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  91. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  92. package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  93. package/dist/{realtime-I8OrfnMR.mjs → realtime-Cuw_deA-.mjs} +2 -2
  94. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  95. package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
  96. package/dist/registry/index.d.mts +2 -2
  97. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  98. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  99. package/dist/schedules-CjOd2MG9.mjs +113 -0
  100. package/dist/schemas/index.d.mts +67 -67
  101. package/dist/schemas/index.mjs +2 -3
  102. package/dist/scim/index.mjs +1 -2
  103. package/dist/scope/index.d.mts +2 -2
  104. package/dist/scope/index.mjs +4 -4
  105. package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
  106. package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
  107. package/dist/testing/index.d.mts +4 -3
  108. package/dist/testing/index.mjs +2 -2
  109. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  110. package/dist/types/index.d.mts +4 -4
  111. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  112. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  113. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  114. package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
  115. package/dist/usage/index.d.mts +3 -52
  116. package/dist/usage/index.mjs +0 -0
  117. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  118. package/dist/utils/index.d.mts +58 -85
  119. package/dist/utils/index.mjs +4 -2
  120. package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
  121. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  122. package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
  123. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  124. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  125. package/package.json +526 -513
  126. package/skills/arc/references/api-reference.md +3 -1
  127. package/skills/arc/references/integrations.md +20 -11
  128. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  129. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  130. package/dist/health-ScyjIVo5.mjs +0 -202
  131. package/dist/requestId-5BhTVzBv.mjs +0 -113
  132. 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;
27
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;
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
+ }
63
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;
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,14 +532,14 @@ 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-0u4P4-OI.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
  }
464
540
  if (config.arcPlugins?.realtime) if (config.arcPlugins?.events === false) fastify.log.warn("Realtime plugin requires events plugin (arcPlugins.events). Realtime disabled.");
465
541
  else {
466
- const { default: realtimePlugin } = await import("./realtime-I8OrfnMR.mjs").then((n) => n.r);
542
+ const { default: realtimePlugin } = await import("./realtime-Cuw_deA-.mjs").then((n) => n.r);
467
543
  const opts = config.arcPlugins.realtime === true ? {} : config.arcPlugins.realtime;
468
544
  await fastify.register(realtimePlugin, opts);
469
545
  trackPlugin("arc-realtime", opts);
@@ -475,7 +551,7 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
475
551
  trackPlugin("arc-metrics", opts);
476
552
  }
477
553
  if (config.arcPlugins?.versioning) {
478
- 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);
479
555
  await fastify.register(versioningPlugin, config.arcPlugins.versioning);
480
556
  trackPlugin("arc-versioning", config.arcPlugins.versioning);
481
557
  }
@@ -544,7 +620,7 @@ async function registerAuth(fastify, config, trackPlugin) {
544
620
  */
545
621
  async function registerElevation(fastify, config, trackPlugin) {
546
622
  if (!config.elevation) return;
547
- const { elevationPlugin } = await import("./elevation-COpYKt2h.mjs").then((n) => n.r);
623
+ const { elevationPlugin } = await import("./elevation-CIz1GjGm.mjs").then((n) => n.r);
548
624
  await fastify.register(elevationPlugin, config.elevation);
549
625
  trackPlugin("arc-elevation", config.elevation);
550
626
  fastify.log.debug("Elevation plugin enabled");
@@ -780,9 +856,34 @@ async function registerResources(fastify, config, preResolvedModules) {
780
856
  throw new Error(`[arc] module "${m.name}" afterResources() threw: ${msg}`, { cause: err });
781
857
  }
782
858
  }
783
- 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 {
784
881
  await config.afterResources(fastify);
785
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;
786
887
  }
787
888
  if (config.onReady) {
788
889
  const onReady = config.onReady;
@@ -792,7 +893,9 @@ async function registerResources(fastify, config, preResolvedModules) {
792
893
  }
793
894
  const closers = modules.filter((m) => m.onClose).reverse();
794
895
  const appOnClose = config.onClose;
795
- 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");
796
899
  for (const m of closers) await m.onClose?.(fastify);
797
900
  if (appOnClose) await appOnClose(fastify);
798
901
  });
@@ -1018,7 +1121,21 @@ async function registerUtilityPlugins(fastify, config) {
1018
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.");
1019
1122
  if (config.underPressure !== false) {
1020
1123
  const underPressure = await loadPlugin("underPressure");
1021
- 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);
1022
1139
  fastify.log.debug("Health monitoring (under-pressure) enabled");
1023
1140
  }
1024
1141
  if (config.sensible !== false) {
@@ -1191,6 +1308,8 @@ function validateDistributedRuntime(options) {
1191
1308
  hint: "a Redis-backed query-cache adapter"
1192
1309
  });
1193
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.");
1194
1313
  if (missing.length > 0) {
1195
1314
  const lines = missing.map((m) => ` • ${m.key.padEnd(20)} → ${m.hint}`).join("\n");
1196
1315
  throw new Error(`[Arc] runtime: 'distributed' requires durable (Redis/MongoDB) adapters, not memory.
@@ -1227,9 +1346,10 @@ async function createApp(options) {
1227
1346
  ...options.preset ? getPreset(options.preset) : {},
1228
1347
  ...options
1229
1348
  };
1230
- const inheritedPermissiveTrustProxy = options.preset === "production" && options.trustProxy === void 0;
1349
+ const inheritedTrustProxyDefault = options.preset === "production" && options.trustProxy === void 0;
1231
1350
  const resolvedModules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
1232
1351
  const moduleErrorMappers = resolvedModules.flatMap((m) => m.errorMappers ?? []);
1352
+ const moduleHealthChecks = collectModuleHealthChecks(resolvedModules);
1233
1353
  const resolvedLogger = resolveLoggerConfig(config.logger);
1234
1354
  const fastify = Fastify({
1235
1355
  logger: resolvedLogger,
@@ -1263,13 +1383,7 @@ async function createApp(options) {
1263
1383
  writer: createPinoWriter(fastify.log)
1264
1384
  });
1265
1385
  for (const warning of deferredWarnings) fastify.log.warn(warning);
1266
- if (inheritedPermissiveTrustProxy) fastify.log.warn("[arc] preset: 'production' defaults to trustProxy: true (trust EVERY proxy). If this app is reachable without your load balancer, clients can spoof X-Forwarded-For/Proto (rate-limit bypass, wrong audit IPs, forged protocol). Silence this by setting trustProxy explicitly: a hop count (trustProxy: 1), a CIDR allow-list ('10.0.0.0/8'), a resolver function, or true if you accept the trade-off. v3 flips this preset default to false — set it now to be upgrade-proof.");
1267
- if (config.typeProvider === "typebox") try {
1268
- const { TypeBoxValidatorCompiler } = await import("@fastify/type-provider-typebox");
1269
- fastify.setValidatorCompiler(TypeBoxValidatorCompiler);
1270
- } catch {
1271
- fastify.log.warn("typeProvider: \"typebox\" requested but @fastify/type-provider-typebox is not installed.");
1272
- }
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.");
1273
1387
  fastify.removeContentTypeParser("application/json");
1274
1388
  fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
1275
1389
  try {
@@ -1289,7 +1403,12 @@ async function createApp(options) {
1289
1403
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
1290
1404
  });
1291
1405
  };
1292
- 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);
1293
1412
  decorateRequestScope(fastify);
1294
1413
  await registerAuth(fastify, config, trackPlugin);
1295
1414
  await registerElevation(fastify, config, trackPlugin);
@@ -1347,4 +1466,48 @@ const ArcFactory = {
1347
1466
  }
1348
1467
  };
1349
1468
  //#endregion
1350
- 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-BBsvQt7U.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 { C as allowPublic, D as requireAuth } from "../permissions-tbxFHxjZ.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 { Jt as AnyRecord, Rt as IControllerResponse, U as ResourceConfig, Y as PresetResult, d as PaginationResult, zt as IRequestContext } from "../index-BBsvQt7U.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-BOe4Ugkl.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-BBsvQt7U.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-BBsvQt7U.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 { C as allowPublic, D as requireAuth } from "../permissions-tbxFHxjZ.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,6 +1,6 @@
1
- import { b as isElevated, n as PUBLIC_SCOPE } from "./types-bHblZ9YN.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";
2
3
  import { multiTenantPreset } from "./presets/multiTenant.mjs";
3
- import { C as allowPublic, D as requireAuth, k as requireRoles } from "./permissions-tbxFHxjZ.mjs";
4
4
  //#region src/presets/ownedByUser.ts
5
5
  /**
6
6
  * Create ownership check middleware.
@@ -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 {