@warlock.js/core 5.19.1 → 5.20.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 (126) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/esm/connectors/cache-connector.d.mts.map +1 -1
  3. package/esm/connectors/cache-connector.mjs +5 -0
  4. package/esm/connectors/cache-connector.mjs.map +1 -1
  5. package/esm/connectors/single-server-warnings.mjs +33 -0
  6. package/esm/connectors/single-server-warnings.mjs.map +1 -0
  7. package/esm/connectors/socket-connector.d.mts.map +1 -1
  8. package/esm/connectors/socket-connector.mjs +4 -0
  9. package/esm/connectors/socket-connector.mjs.map +1 -1
  10. package/esm/connectors/storage.connector.d.mts.map +1 -1
  11. package/esm/connectors/storage.connector.mjs +6 -0
  12. package/esm/connectors/storage.connector.mjs.map +1 -1
  13. package/esm/connectors/types.d.mts +3 -1
  14. package/esm/connectors/types.d.mts.map +1 -1
  15. package/esm/connectors/types.mjs.map +1 -1
  16. package/esm/dev-server/development-server.mjs +4 -0
  17. package/esm/dev-server/development-server.mjs.map +1 -1
  18. package/esm/dev-server/route-types-publisher.mjs +60 -0
  19. package/esm/dev-server/route-types-publisher.mjs.map +1 -0
  20. package/esm/http/build-rate-limit-options.d.mts +37 -0
  21. package/esm/http/build-rate-limit-options.d.mts.map +1 -0
  22. package/esm/http/build-rate-limit-options.mjs +37 -0
  23. package/esm/http/build-rate-limit-options.mjs.map +1 -0
  24. package/esm/http/middleware/idempotency.middleware.d.mts +5 -0
  25. package/esm/http/middleware/idempotency.middleware.d.mts.map +1 -1
  26. package/esm/http/middleware/idempotency.middleware.mjs +35 -3
  27. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  28. package/esm/http/middleware/rate-limit.middleware.d.mts +4 -3
  29. package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
  30. package/esm/http/middleware/rate-limit.middleware.mjs +4 -3
  31. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  32. package/esm/http/plugins.d.mts.map +1 -1
  33. package/esm/http/plugins.mjs +3 -4
  34. package/esm/http/plugins.mjs.map +1 -1
  35. package/esm/http/types.d.mts +2 -14
  36. package/esm/http/types.d.mts.map +1 -1
  37. package/esm/index.d.mts +3 -2
  38. package/esm/index.mjs +3 -2
  39. package/esm/production/assert-generated-imports.mjs +1 -0
  40. package/esm/production/assert-generated-imports.mjs.map +1 -1
  41. package/esm/production/production-builder.mjs +17 -1
  42. package/esm/production/production-builder.mjs.map +1 -1
  43. package/esm/production/route-registration-child.d.mts +1 -0
  44. package/esm/production/route-registration-child.mjs +65 -0
  45. package/esm/production/route-registration-child.mjs.map +1 -0
  46. package/esm/production/route-registration-snapshot.mjs +166 -0
  47. package/esm/production/route-registration-snapshot.mjs.map +1 -0
  48. package/esm/repositories/adapters/cascade/cascade-adapter.d.mts.map +1 -1
  49. package/esm/repositories/adapters/cascade/cascade-adapter.mjs +5 -7
  50. package/esm/repositories/adapters/cascade/cascade-adapter.mjs.map +1 -1
  51. package/esm/socket/index.d.mts +1 -1
  52. package/esm/socket/types.d.mts +16 -1
  53. package/esm/socket/types.d.mts.map +1 -1
  54. package/esm/storage/drivers/cloud-driver.d.mts +9 -0
  55. package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
  56. package/esm/storage/drivers/cloud-driver.mjs +48 -0
  57. package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
  58. package/esm/storage/drivers/do-spaces-driver.d.mts +7 -0
  59. package/esm/storage/drivers/do-spaces-driver.d.mts.map +1 -1
  60. package/esm/storage/drivers/do-spaces-driver.mjs +9 -2
  61. package/esm/storage/drivers/do-spaces-driver.mjs.map +1 -1
  62. package/esm/storage/drivers/local-driver.d.mts +9 -0
  63. package/esm/storage/drivers/local-driver.d.mts.map +1 -1
  64. package/esm/storage/drivers/local-driver.mjs +41 -3
  65. package/esm/storage/drivers/local-driver.mjs.map +1 -1
  66. package/esm/storage/index.d.mts +1 -0
  67. package/esm/storage/index.mjs +2 -1
  68. package/esm/storage/scoped-storage.d.mts +14 -0
  69. package/esm/storage/scoped-storage.d.mts.map +1 -1
  70. package/esm/storage/scoped-storage.mjs +22 -0
  71. package/esm/storage/scoped-storage.mjs.map +1 -1
  72. package/esm/storage/storage.d.mts +11 -0
  73. package/esm/storage/storage.d.mts.map +1 -1
  74. package/esm/storage/storage.mjs +32 -0
  75. package/esm/storage/storage.mjs.map +1 -1
  76. package/esm/storage/types.d.mts +21 -0
  77. package/esm/storage/types.d.mts.map +1 -1
  78. package/esm/storage/utils/storage-capability-error.d.mts +25 -0
  79. package/esm/storage/utils/storage-capability-error.d.mts.map +1 -0
  80. package/esm/storage/utils/storage-capability-error.mjs +26 -0
  81. package/esm/storage/utils/storage-capability-error.mjs.map +1 -0
  82. package/llms-full.txt +71 -42
  83. package/llms.txt +41 -41
  84. package/package.json +21 -15
  85. package/skills/add-connector/SKILL.md +5 -1
  86. package/skills/benchmark-code/SKILL.md +1 -1
  87. package/skills/build-restful/SKILL.md +1 -1
  88. package/skills/build-url/SKILL.md +1 -1
  89. package/skills/configure-app/SKILL.md +7 -1
  90. package/skills/create-controller/SKILL.md +1 -1
  91. package/skills/create-module/SKILL.md +1 -1
  92. package/skills/define-resource/SKILL.md +1 -1
  93. package/skills/encrypt-data/SKILL.md +1 -1
  94. package/skills/hash-password/SKILL.md +1 -1
  95. package/skills/health-checks/SKILL.md +1 -1
  96. package/skills/index.md +30 -0
  97. package/skills/lower-stage3-decorators/SKILL.md +1 -1
  98. package/skills/process-image/SKILL.md +1 -1
  99. package/skills/register-route/SKILL.md +1 -1
  100. package/skills/request-memo/SKILL.md +1 -1
  101. package/skills/request-tracing/SKILL.md +1 -1
  102. package/skills/resolve-path/SKILL.md +1 -1
  103. package/skills/retry-operation/SKILL.md +1 -1
  104. package/skills/run-app/SKILL.md +1 -1
  105. package/skills/send-mail/SKILL.md +1 -1
  106. package/skills/send-response/SKILL.md +1 -1
  107. package/skills/store-file/SKILL.md +11 -1
  108. package/skills/test-http/SKILL.md +1 -1
  109. package/skills/test-service/SKILL.md +1 -1
  110. package/skills/update-packages/SKILL.md +1 -1
  111. package/skills/upload-file/SKILL.md +1 -1
  112. package/skills/use-app-context/SKILL.md +1 -1
  113. package/skills/use-localization/SKILL.md +1 -1
  114. package/skills/use-middleware/SKILL.md +7 -2
  115. package/skills/use-model-transformers/SKILL.md +1 -1
  116. package/skills/use-repository/SKILL.md +1 -1
  117. package/skills/use-request-locals/SKILL.md +1 -1
  118. package/skills/validate-input/SKILL.md +1 -1
  119. package/skills/warlock-conventions/SKILL.md +1 -1
  120. package/skills/warlock-doctor/SKILL.md +1 -1
  121. package/skills/warlock-routes/SKILL.md +1 -1
  122. package/skills/wire-socket/SKILL.md +5 -1
  123. package/skills/write-cli-command/SKILL.md +1 -1
  124. package/skills/write-middleware/SKILL.md +1 -1
  125. package/skills/write-seeder/SKILL.md +1 -1
  126. package/skills/write-use-case/SKILL.md +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  > ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
8
8
 
9
+ ## 5.20.0 - 2026-09-24
10
+
11
+ ### Added
12
+
13
+ - Production build contributions receive a fresh registration-only
14
+ `ConnectorBuildContext.namedApiRoutes` snapshot when Web is configured. It
15
+ contains only named API `name`, `path`, and `method` records, never handlers,
16
+ middleware, schemas, or source paths.
17
+
18
+ - `storage.putIfAbsent(file, location, options?)` and `storage.supportsPutIfAbsent()` — an atomic create-only write. Returns the `StorageFile`, or `null` when something already exists at `location`. Unlike `put`, a string argument is **content**, not a path. Local driver writes a temp file then hard-links it; S3/R2 send `If-None-Match: *`. DigitalOcean Spaces does not expose it. Drivers without it throw `StorageCapabilityError`.
19
+ - `http.rateLimit` now passes `@fastify/rate-limit` options through (`redis` for a store shared across servers, `nameSpace`, `keyGenerator`, `allowList`, ...). `http.rateLimit.enabled: false` turns the global limiter off. The per-route `middleware.rateLimit()` stays in-process.
20
+ - `socket.adapter` — an adapter factory (e.g. `@socket.io/redis-adapter`) applied via `io.adapter()` for broadcasting across servers. Polling clients need sticky sessions.
21
+ - Production single-server warnings, once at boot, when the default cache driver is in-memory, the default storage driver is local, or sockets have no `socket.adapter`. Silence with `cache.silenceSingleServerWarning`, `storage.silenceSingleServerWarning`, `socket.silenceSingleServerWarning`.
22
+ - `middleware.idempotency({ reservationTtl })` (also `http.idempotency.reservationTtl`, default `60` seconds) — how long the in-flight reservation lives.
23
+ - Docs: "Running on multiple servers" guide.
24
+
25
+ ### Changed
26
+
27
+ - Refined connector build guidance and grouped skill discovery.
28
+ - `middleware.idempotency()` reserves the key (create-only) before the handler runs. A concurrent duplicate now gets **409 + `Retry-After`** instead of running twice. A 5xx response frees the key so the client can retry. If the cache is down the middleware fails open.
29
+
30
+ ### Fixed
31
+
32
+ - The repository cache is also cleared after the DB transaction commits (cascade `afterCommit`), fixing stale reads when a concurrent read re-cached the old row between the model event and `COMMIT`.
33
+
9
34
  ## 5.19.1 - 2026-09-23
10
35
 
11
36
  ### Changed
@@ -1 +1 @@
1
- {"version":3,"file":"cache-connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/cache-connector.ts"],"mappings":";;;;;;AAUA;;cAAa,cAAA,SAAuB,aAAA;EAAA,SAClB,IAAA;EAAA,SACA,QAAA,GAAQ,iBAAA,CAAA,KAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,KAAA;EA8BL;;;EAAA,mBAzBN,YAAA;EARe;;;EAarB,KAAA,IAAS,OAAA;EAXE;;;;;;;;EA+BX,QAAA,IAAY,OAAA;AAAA"}
1
+ {"version":3,"file":"cache-connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/cache-connector.ts"],"mappings":";;;;;;AAaA;;cAAa,cAAA,SAAuB,aAAA;EAAA,SAClB,IAAA;EAAA,SACA,QAAA,GAAQ,iBAAA,CAAA,KAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,KAAA;EAuCL;;;EAAA,mBAlCN,YAAA;EARe;;;EAarB,KAAA,IAAS,OAAA;EAXE;;;;;;;;EAwCX,QAAA,IAAY,OAAA;AAAA"}
@@ -1,6 +1,9 @@
1
+ import { environment } from "../utils/environment.mjs";
1
2
  import { ConnectorLifecyclePhase, ConnectorPriority } from "./types.mjs";
2
3
  import { BaseConnector } from "./base-connector.mjs";
4
+ import { memoryCacheWarning } from "./single-server-warnings.mjs";
3
5
  import config from "@mongez/config";
6
+ import { log } from "@warlock.js/logger";
4
7
  import { cache } from "@warlock.js/cache";
5
8
 
6
9
  //#region ../core/src/connectors/cache-connector.ts
@@ -24,6 +27,8 @@ var CacheConnector = class extends BaseConnector {
24
27
  if (!cacheConfig) return;
25
28
  cache.setCacheConfigurations(cacheConfig);
26
29
  await cache.init();
30
+ const warning = memoryCacheWarning(cache.currentDriver?.name ?? cacheConfig.default, cache.currentDriver?.constructor?.name, environment(), config.get("cache.silenceSingleServerWarning") === true);
31
+ if (warning) log.warn("cache", "single-server", warning);
27
32
  this.active = true;
28
33
  }
29
34
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"cache-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/cache-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { cache } from \"@warlock.js/cache\";\nimport type { CacheDriver } from \"@warlock.js/cache\";\nimport { BaseConnector } from \"./base-connector\";\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\n\n/**\n * Cache Connector\n * Manages cache engine connection lifecycle\n */\nexport class CacheConnector extends BaseConnector {\n public readonly name = \"cache\";\n public readonly priority = ConnectorPriority.CACHE;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Early;\n\n /**\n * Files that trigger cache restart\n */\n protected readonly watchedFiles = [\"src/config/cache.ts\", \"src/config/cache.tsx\"];\n\n /**\n * Initialize cache connection\n */\n public async start(): Promise<void> {\n const cacheConfig = config.get(\"cache\");\n\n if (!cacheConfig) return;\n\n cache.setCacheConfigurations(cacheConfig);\n\n await cache.init();\n\n this.active = true;\n }\n\n /**\n * Shutdown cache connection\n *\n * Disconnects every loaded cache driver so external clients (e.g. a Redis\n * connection) are released instead of left dangling — mirroring the\n * database and herald connectors, which disconnect all registered\n * connections on shutdown.\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n // Disconnect every driver the manager loaded (not just the current one),\n // so secondary drivers selected via `cache.use(\"...\")` also release their\n // connections.\n const drivers: CacheDriver<unknown, unknown>[] = Object.values(cache.loadedDrivers);\n\n for (const driver of drivers) {\n await driver.disconnect();\n }\n\n this.active = false;\n }\n}\n"],"mappings":";;;;;;;;;;AAUA,IAAa,iBAAb,cAAoC,cAAc;;;cACzB;;;sBAOW,CAAC,uBAAuB,sBAAsB;;;;;CAKhF,MAAa,QAAuB;EAClC,MAAM,cAAc,OAAO,IAAI,OAAO;EAEtC,IAAI,CAAC,aAAa;EAElB,MAAM,uBAAuB,WAAW;EAExC,MAAM,MAAM,KAAK;EAEjB,KAAK,SAAS;CAChB;;;;;;;;;CAUA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAMF,MAAM,UAA2C,OAAO,OAAO,MAAM,aAAa;EAElF,KAAK,MAAM,UAAU,SACnB,MAAM,OAAO,WAAW;EAG1B,KAAK,SAAS;CAChB;AACF"}
1
+ {"version":3,"file":"cache-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/cache-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { cache } from \"@warlock.js/cache\";\nimport type { CacheDriver } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport { environment } from \"../utils/environment\";\nimport { BaseConnector } from \"./base-connector\";\nimport { memoryCacheWarning } from \"./single-server-warnings\";\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\n\n/**\n * Cache Connector\n * Manages cache engine connection lifecycle\n */\nexport class CacheConnector extends BaseConnector {\n public readonly name = \"cache\";\n public readonly priority = ConnectorPriority.CACHE;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Early;\n\n /**\n * Files that trigger cache restart\n */\n protected readonly watchedFiles = [\"src/config/cache.ts\", \"src/config/cache.tsx\"];\n\n /**\n * Initialize cache connection\n */\n public async start(): Promise<void> {\n const cacheConfig = config.get(\"cache\");\n\n if (!cacheConfig) return;\n\n cache.setCacheConfigurations(cacheConfig);\n\n await cache.init();\n\n const warning = memoryCacheWarning(\n cache.currentDriver?.name ?? cacheConfig.default,\n cache.currentDriver?.constructor?.name,\n environment(),\n config.get(\"cache.silenceSingleServerWarning\") === true,\n );\n\n if (warning) log.warn(\"cache\", \"single-server\", warning);\n\n this.active = true;\n }\n\n /**\n * Shutdown cache connection\n *\n * Disconnects every loaded cache driver so external clients (e.g. a Redis\n * connection) are released instead of left dangling — mirroring the\n * database and herald connectors, which disconnect all registered\n * connections on shutdown.\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n // Disconnect every driver the manager loaded (not just the current one),\n // so secondary drivers selected via `cache.use(\"...\")` also release their\n // connections.\n const drivers: CacheDriver<unknown, unknown>[] = Object.values(cache.loadedDrivers);\n\n for (const driver of drivers) {\n await driver.disconnect();\n }\n\n this.active = false;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,IAAa,iBAAb,cAAoC,cAAc;;;cACzB;;;sBAOW,CAAC,uBAAuB,sBAAsB;;;;;CAKhF,MAAa,QAAuB;EAClC,MAAM,cAAc,OAAO,IAAI,OAAO;EAEtC,IAAI,CAAC,aAAa;EAElB,MAAM,uBAAuB,WAAW;EAExC,MAAM,MAAM,KAAK;EAEjB,MAAM,UAAU,mBACd,MAAM,eAAe,QAAQ,YAAY,SACzC,MAAM,eAAe,aAAa,MAClC,YAAY,GACZ,OAAO,IAAI,kCAAkC,MAAM,IACrD;EAEA,IAAI,SAAS,IAAI,KAAK,SAAS,iBAAiB,OAAO;EAEvD,KAAK,SAAS;CAChB;;;;;;;;;CAUA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAMF,MAAM,UAA2C,OAAO,OAAO,MAAM,aAAa;EAElF,KAAK,MAAM,UAAU,SACnB,MAAM,OAAO,WAAW;EAG1B,KAAK,SAAS;CAChB;AACF"}
@@ -0,0 +1,33 @@
1
+ //#region ../core/src/connectors/single-server-warnings.ts
2
+ const IN_PROCESS_CACHE_NAMES = [
3
+ "memory",
4
+ "memoryextended",
5
+ "lru"
6
+ ];
7
+ const IN_PROCESS_CACHE_CLASSES = [
8
+ "memorycachedriver",
9
+ "memoryextendedcachedriver",
10
+ "lrumemorycachedriver"
11
+ ];
12
+ /**
13
+ * Warning for a production app whose default cache driver keeps data in this
14
+ * process only. Returns `undefined` when there is nothing to warn about.
15
+ */
16
+ function memoryCacheWarning(defaultDriverName, driverClassName, env, silenced = false) {
17
+ if (env !== "production" || silenced || !defaultDriverName) return void 0;
18
+ const byName = IN_PROCESS_CACHE_NAMES.includes(defaultDriverName.toLowerCase());
19
+ const byClass = !!driverClassName && IN_PROCESS_CACHE_CLASSES.includes(driverClassName.toLowerCase());
20
+ if (!byName && !byClass) return void 0;
21
+ return `cache: the default driver '${defaultDriverName}' keeps data in this process only. With more than one server, repository cache clears, locks and counters don't reach the other servers. Use the redis or pg cache driver in production, or set cache.silenceSingleServerWarning = true.`;
22
+ }
23
+ /**
24
+ * Warning for a production app whose default storage driver is the local disk.
25
+ */
26
+ function localStorageWarning(defaultDriverName, env, silenced = false) {
27
+ if (env !== "production" || silenced || defaultDriverName !== "local") return void 0;
28
+ return "storage: the default driver 'local' keeps files on this server's disk only. With more than one server, uploads written on one server are missing on the others. Use an S3 or R2 storage driver in production, or set storage.silenceSingleServerWarning = true.";
29
+ }
30
+
31
+ //#endregion
32
+ export { localStorageWarning, memoryCacheWarning };
33
+ //# sourceMappingURL=single-server-warnings.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"single-server-warnings.mjs","names":[],"sources":["../../../../../../../core/src/connectors/single-server-warnings.ts"],"sourcesContent":["import type { Environment } from \"../utils/environment\";\n\nconst IN_PROCESS_CACHE_NAMES = [\"memory\", \"memoryextended\", \"lru\"];\nconst IN_PROCESS_CACHE_CLASSES = [\"memorycachedriver\", \"memoryextendedcachedriver\", \"lrumemorycachedriver\"];\n\n/**\n * Warning for a production app whose default cache driver keeps data in this\n * process only. Returns `undefined` when there is nothing to warn about.\n */\nexport function memoryCacheWarning(\n defaultDriverName: string | undefined,\n driverClassName: string | undefined,\n env: Environment,\n silenced = false,\n): string | undefined {\n if (env !== \"production\" || silenced || !defaultDriverName) return undefined;\n\n const byName = IN_PROCESS_CACHE_NAMES.includes(defaultDriverName.toLowerCase());\n const byClass =\n !!driverClassName && IN_PROCESS_CACHE_CLASSES.includes(driverClassName.toLowerCase());\n\n if (!byName && !byClass) return undefined;\n\n return (\n `cache: the default driver '${defaultDriverName}' keeps data in this process only. ` +\n \"With more than one server, repository cache clears, locks and counters don't reach the other servers. \" +\n \"Use the redis or pg cache driver in production, or set cache.silenceSingleServerWarning = true.\"\n );\n}\n\n/**\n * Warning for a production app whose default storage driver is the local disk.\n */\nexport function localStorageWarning(\n defaultDriverName: string | undefined,\n env: Environment,\n silenced = false,\n): string | undefined {\n if (env !== \"production\" || silenced || defaultDriverName !== \"local\") return undefined;\n\n return (\n \"storage: the default driver 'local' keeps files on this server's disk only. \" +\n \"With more than one server, uploads written on one server are missing on the others. \" +\n \"Use an S3 or R2 storage driver in production, or set storage.silenceSingleServerWarning = true.\"\n );\n}\n"],"mappings":";AAEA,MAAM,yBAAyB;CAAC;CAAU;CAAkB;AAAK;AACjE,MAAM,2BAA2B;CAAC;CAAqB;CAA6B;AAAsB;;;;;AAM1G,SAAgB,mBACd,mBACA,iBACA,KACA,WAAW,OACS;CACpB,IAAI,QAAQ,gBAAgB,YAAY,CAAC,mBAAmB,OAAO;CAEnE,MAAM,SAAS,uBAAuB,SAAS,kBAAkB,YAAY,CAAC;CAC9E,MAAM,UACJ,CAAC,CAAC,mBAAmB,yBAAyB,SAAS,gBAAgB,YAAY,CAAC;CAEtF,IAAI,CAAC,UAAU,CAAC,SAAS,OAAO;CAEhC,OACE,8BAA8B,kBAAkB;AAIpD;;;;AAKA,SAAgB,oBACd,mBACA,KACA,WAAW,OACS;CACpB,IAAI,QAAQ,gBAAgB,YAAY,sBAAsB,SAAS,OAAO;CAE9E,OACE;AAIJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"socket-connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/socket-connector.ts"],"mappings":";;;;;;;AA8BA;;cAAa,eAAA,SAAwB,aAAA;EAAA,SACnB,IAAA,EAAM,aAAA;EAAA,SACN,QAAA,GAAQ,iBAAA,CAAA,MAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,IAAA;EASX;;;;;EAAA,mBAFA,YAAA;EAAA,UAET,MAAA,GAAS,MAAA;EAZgB;;;;;;;;EAAA,UAsBzB,aAAA;EAZS;;;EAiBN,IAAA,IAAI,OAAA;EAAJ;;;EAuDA,KAAA,IAAS,OAAA;EAqBT;;;AAAmB;;;;;;EAAnB,QAAA,IAAY,OAAA;AAAA"}
1
+ {"version":3,"file":"socket-connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/socket-connector.ts"],"mappings":";;;;;;;AAkCA;;cAAa,eAAA,SAAwB,aAAA;EAAA,SACnB,IAAA,EAAM,aAAA;EAAA,SACN,QAAA,GAAQ,iBAAA,CAAA,MAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,IAAA;EASX;;;;;EAAA,mBAFA,YAAA;EAAA,UAET,MAAA,GAAS,MAAA;EAZgB;;;;;;;;EAAA,UAsBzB,aAAA;EAZS;;;EAiBN,IAAA,IAAI,OAAA;EAAJ;;;EA6DA,KAAA,IAAS,OAAA;EAqBT;;;AAAmB;;;;;;EAAnB,QAAA,IAAY,OAAA;AAAA"}
@@ -1,3 +1,4 @@
1
+ import { environment } from "../utils/environment.mjs";
1
2
  import { container } from "../container/index.mjs";
2
3
  import { ConnectorLifecyclePhase, ConnectorPriority } from "./types.mjs";
3
4
  import { BaseConnector } from "./base-connector.mjs";
@@ -23,6 +24,7 @@ Or manually:
23
24
  pnpm add socket.io
24
25
  yarn add socket.io
25
26
  `.trim();
27
+ const SINGLE_SERVER_WARNING = "socket: no adapter configured — broadcasts only reach clients connected to this server. Use @socket.io/redis-adapter (socket.adapter) behind a load balancer, with sticky sessions for the polling transport.";
26
28
  /**
27
29
  * Socket Connector
28
30
  * Manages Socket server (Socket.IO) lifecycle
@@ -64,6 +66,8 @@ var SocketConnector = class extends BaseConnector {
64
66
  destroyUpgrade: false,
65
67
  ...socketConfig.options
66
68
  });
69
+ if (socketConfig.adapter) this.socket.adapter(await socketConfig.adapter(this.socket));
70
+ else if (environment() === "production" && !socketConfig.silenceSingleServerWarning) log.warn("socket", "adapter", SINGLE_SERVER_WARNING);
67
71
  container.set("socket", this.socket);
68
72
  }
69
73
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"socket-connector.mjs","names":["createHttpsServer","createHttpServer"],"sources":["../../../../../../../core/src/connectors/socket-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { log } from \"@warlock.js/logger\";\nimport { createServer as createHttpServer } from \"http\";\nimport { createServer as createHttpsServer } from \"https\";\nimport type { Server } from \"socket.io\";\nimport { container } from \"../container\";\nimport { BaseConnector } from \"./base-connector\";\nimport { ConnectorLifecyclePhase, type ConnectorName, ConnectorPriority } from \"./types\";\n\n/**\n * Shown when a project enables sockets (a `socket` config is present) but the\n * optional `socket.io` peer is not installed.\n */\nconst SOCKET_INSTALL_INSTRUCTIONS = `\nRealtime socket server requires the socket.io package.\nInstall it with:\n\n warlock add socket\n\nOr manually:\n\n npm install socket.io\n pnpm add socket.io\n yarn add socket.io\n`.trim();\n\n/**\n * Socket Connector\n * Manages Socket server (Socket.IO) lifecycle\n */\nexport class SocketConnector extends BaseConnector {\n public readonly name: ConnectorName = \"socket\";\n public readonly priority = ConnectorPriority.SOCKET;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\n\n /**\n * Files that trigger Socket server restart\n * Note: routes.ts changes will be handled by HMR with wildcard routing\n * Connectors receive config file paths directly (not .env) thanks to layer-executor\n */\n protected readonly watchedFiles = [\"src/config/socket.ts\"];\n\n protected socket?: Server;\n\n /**\n * Whether this connector created the underlying node HTTP(S) server.\n *\n * When `http` is present, socket binds onto the HTTP connector's shared\n * `fastify.server` and must NOT close it on shutdown — the HTTP connector\n * owns the graceful drain. Socket only closes the node server when it\n * created one itself (http absent). See {@link shutdown}.\n */\n protected ownsRawServer = false;\n\n /**\n * Boot the connector\n */\n public async boot() {\n const socketConfig = config.get(\"socket\");\n\n if (!socketConfig) return;\n\n // socket.io is an optional peer — load it lazily so projects that don't use\n // realtime sockets never need it installed (mirrors the mail/storage drivers).\n let SocketServer: typeof import(\"socket.io\").Server;\n try {\n ({ Server: SocketServer } = await import(\"socket.io\"));\n } catch {\n throw new Error(SOCKET_INSTALL_INSTRUCTIONS);\n }\n\n log.info(\"socket\", \"connection\", \"Starting Socket.IO server\");\n\n // now we have two cases\n // 1. http is used, then use it (shared — the HTTP connector owns it)\n // 2. http is not used, then create a new server (we own it)\n let server;\n const fastify = container.tryGet(\"http.server\");\n if (fastify) {\n server = fastify.server;\n this.ownsRawServer = false;\n } else {\n server = socketConfig.ssl ? createHttpsServer() : createHttpServer();\n server.listen(socketConfig.port);\n this.ownsRawServer = true;\n }\n\n container.set(\"socket.rawServer\", server);\n\n this.socket = new SocketServer(server, {\n // The shared raw server may carry OTHER `upgrade` consumers — Vite's HMR\n // websocket does exactly that when `WebConnector` is registered\n // (`web/src/server/web-connector.ts`, `server.hmr.server = fastify.server`).\n // engine.io's default `destroyUpgrade: true` schedules a destroy for every\n // upgrade whose path is not `/socket.io`\n // (`engine.io/build/server.js:676-695`: `setTimeout(… socket.end(), 1000)`),\n // and the only thing saving a foreign websocket today is that it wrote its\n // handshake bytes synchronously and so trips the `bytesWritten <= 0` guard\n // before the 1s timer fires. That is an undocumented race, not a contract.\n // Turning the destroy off removes the coupling outright and makes the\n // shared server safe for any app-owned websocket route.\n // A project can still override it via `socket.options`.\n destroyUpgrade: false,\n ...socketConfig.options,\n });\n\n container.set(\"socket\", this.socket);\n }\n\n /**\n * Initialize Socket server\n */\n public async start(): Promise<void> {\n const socketConfig = config.get(\"socket\");\n\n // `this.socket` is only set by boot() once a socket config is present —\n // use it (not a never-assigned field) to detect a successful boot.\n if (!socketConfig || !this.socket) return;\n\n log.success(\"socket\", \"connection\", \"Established Socket.IO server\");\n\n this.active = true;\n }\n\n /**\n * Shutdown Socket server\n *\n * Always closes the Socket.IO layer (and awaits the drain). The underlying\n * node HTTP(S) server is only closed here when this connector created it\n * ({@link ownsRawServer}); when `http` owns the shared server, we leave it\n * for the HTTP connector to drain and close, so the two connectors don't\n * race to `close()` the same socket.\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n const socket = container.tryGet(\"socket\");\n if (socket) {\n // socket.io's close() takes a callback — await it so shutdown doesn't\n // report done while sockets are still draining.\n await new Promise<void>((resolve) => {\n socket.close(() => resolve());\n });\n }\n\n const rawServer = this.ownsRawServer ? container.tryGet(\"socket.rawServer\") : undefined;\n if (rawServer) {\n await new Promise<void>((resolve, reject) => {\n rawServer.close((error?: Error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n\n this.active = false;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAM,8BAA8B;;;;;;;;;;;EAWlC,KAAK;;;;;AAMP,IAAa,kBAAb,cAAqC,cAAc;;;cACX;;;sBASJ,CAAC,sBAAsB;uBAY/B;;;;;CAK1B,MAAa,OAAO;EAClB,MAAM,eAAe,OAAO,IAAI,QAAQ;EAExC,IAAI,CAAC,cAAc;EAInB,IAAI;EACJ,IAAI;GACF,CAAC,CAAE,QAAQ,gBAAiB,MAAM,OAAO;EAC3C,QAAQ;GACN,MAAM,IAAI,MAAM,2BAA2B;EAC7C;EAEA,IAAI,KAAK,UAAU,cAAc,2BAA2B;EAK5D,IAAI;EACJ,MAAM,UAAU,UAAU,OAAO,aAAa;EAC9C,IAAI,SAAS;GACX,SAAS,QAAQ;GACjB,KAAK,gBAAgB;EACvB,OAAO;GACL,SAAS,aAAa,MAAMA,eAAkB,IAAIC,aAAiB;GACnE,OAAO,OAAO,aAAa,IAAI;GAC/B,KAAK,gBAAgB;EACvB;EAEA,UAAU,IAAI,oBAAoB,MAAM;EAExC,KAAK,SAAS,IAAI,aAAa,QAAQ;GAarC,gBAAgB;GAChB,GAAG,aAAa;EAClB,CAAC;EAED,UAAU,IAAI,UAAU,KAAK,MAAM;CACrC;;;;CAKA,MAAa,QAAuB;EAKlC,IAAI,CAJiB,OAAO,IAAI,QAIhB,KAAK,CAAC,KAAK,QAAQ;EAEnC,IAAI,QAAQ,UAAU,cAAc,8BAA8B;EAElE,KAAK,SAAS;CAChB;;;;;;;;;;CAWA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAGF,MAAM,SAAS,UAAU,OAAO,QAAQ;EACxC,IAAI,QAGF,MAAM,IAAI,SAAe,YAAY;GACnC,OAAO,YAAY,QAAQ,CAAC;EAC9B,CAAC;EAGH,MAAM,YAAY,KAAK,gBAAgB,UAAU,OAAO,kBAAkB,IAAI;EAC9E,IAAI,WACF,MAAM,IAAI,SAAe,SAAS,WAAW;GAC3C,UAAU,OAAO,UAAkB;IACjC,IAAI,OACF,OAAO,KAAK;SAEZ,QAAQ;GAEZ,CAAC;EACH,CAAC;EAGH,KAAK,SAAS;CAChB;AACF"}
1
+ {"version":3,"file":"socket-connector.mjs","names":["createHttpsServer","createHttpServer"],"sources":["../../../../../../../core/src/connectors/socket-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { log } from \"@warlock.js/logger\";\nimport { createServer as createHttpServer } from \"http\";\nimport { createServer as createHttpsServer } from \"https\";\nimport type { Server } from \"socket.io\";\nimport { container } from \"../container\";\nimport { environment } from \"../utils/environment\";\nimport { BaseConnector } from \"./base-connector\";\nimport { ConnectorLifecyclePhase, type ConnectorName, ConnectorPriority } from \"./types\";\n\n/**\n * Shown when a project enables sockets (a `socket` config is present) but the\n * optional `socket.io` peer is not installed.\n */\nconst SOCKET_INSTALL_INSTRUCTIONS = `\nRealtime socket server requires the socket.io package.\nInstall it with:\n\n warlock add socket\n\nOr manually:\n\n npm install socket.io\n pnpm add socket.io\n yarn add socket.io\n`.trim();\n\nconst SINGLE_SERVER_WARNING =\n \"socket: no adapter configured — broadcasts only reach clients connected to this server. Use @socket.io/redis-adapter (socket.adapter) behind a load balancer, with sticky sessions for the polling transport.\";\n\n/**\n * Socket Connector\n * Manages Socket server (Socket.IO) lifecycle\n */\nexport class SocketConnector extends BaseConnector {\n public readonly name: ConnectorName = \"socket\";\n public readonly priority = ConnectorPriority.SOCKET;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\n\n /**\n * Files that trigger Socket server restart\n * Note: routes.ts changes will be handled by HMR with wildcard routing\n * Connectors receive config file paths directly (not .env) thanks to layer-executor\n */\n protected readonly watchedFiles = [\"src/config/socket.ts\"];\n\n protected socket?: Server;\n\n /**\n * Whether this connector created the underlying node HTTP(S) server.\n *\n * When `http` is present, socket binds onto the HTTP connector's shared\n * `fastify.server` and must NOT close it on shutdown — the HTTP connector\n * owns the graceful drain. Socket only closes the node server when it\n * created one itself (http absent). See {@link shutdown}.\n */\n protected ownsRawServer = false;\n\n /**\n * Boot the connector\n */\n public async boot() {\n const socketConfig = config.get(\"socket\");\n\n if (!socketConfig) return;\n\n // socket.io is an optional peer — load it lazily so projects that don't use\n // realtime sockets never need it installed (mirrors the mail/storage drivers).\n let SocketServer: typeof import(\"socket.io\").Server;\n try {\n ({ Server: SocketServer } = await import(\"socket.io\"));\n } catch {\n throw new Error(SOCKET_INSTALL_INSTRUCTIONS);\n }\n\n log.info(\"socket\", \"connection\", \"Starting Socket.IO server\");\n\n // now we have two cases\n // 1. http is used, then use it (shared — the HTTP connector owns it)\n // 2. http is not used, then create a new server (we own it)\n let server;\n const fastify = container.tryGet(\"http.server\");\n if (fastify) {\n server = fastify.server;\n this.ownsRawServer = false;\n } else {\n server = socketConfig.ssl ? createHttpsServer() : createHttpServer();\n server.listen(socketConfig.port);\n this.ownsRawServer = true;\n }\n\n container.set(\"socket.rawServer\", server);\n\n this.socket = new SocketServer(server, {\n // The shared raw server may carry OTHER `upgrade` consumers — Vite's HMR\n // websocket does exactly that when `WebConnector` is registered\n // (`web/src/server/web-connector.ts`, `server.hmr.server = fastify.server`).\n // engine.io's default `destroyUpgrade: true` schedules a destroy for every\n // upgrade whose path is not `/socket.io`\n // (`engine.io/build/server.js:676-695`: `setTimeout(… socket.end(), 1000)`),\n // and the only thing saving a foreign websocket today is that it wrote its\n // handshake bytes synchronously and so trips the `bytesWritten <= 0` guard\n // before the 1s timer fires. That is an undocumented race, not a contract.\n // Turning the destroy off removes the coupling outright and makes the\n // shared server safe for any app-owned websocket route.\n // A project can still override it via `socket.options`.\n destroyUpgrade: false,\n ...socketConfig.options,\n });\n\n if (socketConfig.adapter) {\n this.socket.adapter((await socketConfig.adapter(this.socket)) as never);\n } else if (environment() === \"production\" && !socketConfig.silenceSingleServerWarning) {\n log.warn(\"socket\", \"adapter\", SINGLE_SERVER_WARNING);\n }\n\n container.set(\"socket\", this.socket);\n }\n\n /**\n * Initialize Socket server\n */\n public async start(): Promise<void> {\n const socketConfig = config.get(\"socket\");\n\n // `this.socket` is only set by boot() once a socket config is present —\n // use it (not a never-assigned field) to detect a successful boot.\n if (!socketConfig || !this.socket) return;\n\n log.success(\"socket\", \"connection\", \"Established Socket.IO server\");\n\n this.active = true;\n }\n\n /**\n * Shutdown Socket server\n *\n * Always closes the Socket.IO layer (and awaits the drain). The underlying\n * node HTTP(S) server is only closed here when this connector created it\n * ({@link ownsRawServer}); when `http` owns the shared server, we leave it\n * for the HTTP connector to drain and close, so the two connectors don't\n * race to `close()` the same socket.\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n const socket = container.tryGet(\"socket\");\n if (socket) {\n // socket.io's close() takes a callback — await it so shutdown doesn't\n // report done while sockets are still draining.\n await new Promise<void>((resolve) => {\n socket.close(() => resolve());\n });\n }\n\n const rawServer = this.ownsRawServer ? container.tryGet(\"socket.rawServer\") : undefined;\n if (rawServer) {\n await new Promise<void>((resolve, reject) => {\n rawServer.close((error?: Error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n\n this.active = false;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAM,8BAA8B;;;;;;;;;;;EAWlC,KAAK;AAEP,MAAM,wBACJ;;;;;AAMF,IAAa,kBAAb,cAAqC,cAAc;;;cACX;;;sBASJ,CAAC,sBAAsB;uBAY/B;;;;;CAK1B,MAAa,OAAO;EAClB,MAAM,eAAe,OAAO,IAAI,QAAQ;EAExC,IAAI,CAAC,cAAc;EAInB,IAAI;EACJ,IAAI;GACF,CAAC,CAAE,QAAQ,gBAAiB,MAAM,OAAO;EAC3C,QAAQ;GACN,MAAM,IAAI,MAAM,2BAA2B;EAC7C;EAEA,IAAI,KAAK,UAAU,cAAc,2BAA2B;EAK5D,IAAI;EACJ,MAAM,UAAU,UAAU,OAAO,aAAa;EAC9C,IAAI,SAAS;GACX,SAAS,QAAQ;GACjB,KAAK,gBAAgB;EACvB,OAAO;GACL,SAAS,aAAa,MAAMA,eAAkB,IAAIC,aAAiB;GACnE,OAAO,OAAO,aAAa,IAAI;GAC/B,KAAK,gBAAgB;EACvB;EAEA,UAAU,IAAI,oBAAoB,MAAM;EAExC,KAAK,SAAS,IAAI,aAAa,QAAQ;GAarC,gBAAgB;GAChB,GAAG,aAAa;EAClB,CAAC;EAED,IAAI,aAAa,SACf,KAAK,OAAO,QAAS,MAAM,aAAa,QAAQ,KAAK,MAAM,CAAW;OACjE,IAAI,YAAY,MAAM,gBAAgB,CAAC,aAAa,4BACzD,IAAI,KAAK,UAAU,WAAW,qBAAqB;EAGrD,UAAU,IAAI,UAAU,KAAK,MAAM;CACrC;;;;CAKA,MAAa,QAAuB;EAKlC,IAAI,CAJiB,OAAO,IAAI,QAIhB,KAAK,CAAC,KAAK,QAAQ;EAEnC,IAAI,QAAQ,UAAU,cAAc,8BAA8B;EAElE,KAAK,SAAS;CAChB;;;;;;;;;;CAWA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAGF,MAAM,SAAS,UAAU,OAAO,QAAQ;EACxC,IAAI,QAGF,MAAM,IAAI,SAAe,YAAY;GACnC,OAAO,YAAY,QAAQ,CAAC;EAC9B,CAAC;EAGH,MAAM,YAAY,KAAK,gBAAgB,UAAU,OAAO,kBAAkB,IAAI;EAC9E,IAAI,WACF,MAAM,IAAI,SAAe,SAAS,WAAW;GAC3C,UAAU,OAAO,UAAkB;IACjC,IAAI,OACF,OAAO,KAAK;SAEZ,QAAQ;GAEZ,CAAC;EACH,CAAC;EAGH,KAAK,SAAS;CAChB;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"storage.connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/storage.connector.ts"],"mappings":";;;;;;AASA;;cAAa,gBAAA,SAAyB,aAAA;EAAA,SACpB,IAAA;EAAA,SACA,QAAA,GAAQ,iBAAA,CAAA,OAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,KAAA;EAiCL;;;EAAA,mBA5BN,YAAA;EARiB;;;;;;;;;;;;;;AAoCJ;;EAVnB,KAAA,IAAS,OAAA;;;;EAUT,QAAA,IAAY,OAAA;AAAA"}
1
+ {"version":3,"file":"storage.connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/storage.connector.ts"],"mappings":";;;;;;AAaA;;cAAa,gBAAA,SAAyB,aAAA;EAAA,SACpB,IAAA;EAAA,SACA,QAAA,GAAQ,iBAAA,CAAA,OAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,KAAA;EAyCL;;;EAAA,mBApCN,YAAA;EARiB;;;;;;;;;;;;;;AA4CJ;;EAlBnB,KAAA,IAAS,OAAA;;;;EAkBT,QAAA,IAAY,OAAA;AAAA"}
@@ -1,8 +1,12 @@
1
+ import { environment } from "../utils/environment.mjs";
1
2
  import { loadS3 } from "../storage/drivers/cloud-driver.mjs";
2
3
  import { storage } from "../storage/storage.mjs";
3
4
  import "../storage/index.mjs";
4
5
  import { ConnectorLifecyclePhase, ConnectorPriority } from "./types.mjs";
5
6
  import { BaseConnector } from "./base-connector.mjs";
7
+ import { localStorageWarning } from "./single-server-warnings.mjs";
8
+ import config from "@mongez/config";
9
+ import { log } from "@warlock.js/logger";
6
10
 
7
11
  //#region ../core/src/connectors/storage.connector.ts
8
12
  /**
@@ -36,6 +40,8 @@ var StorageConnector = class extends BaseConnector {
36
40
  async start() {
37
41
  await loadS3();
38
42
  await storage.init();
43
+ const warning = localStorageWarning(config.get("storage.default", "local"), environment(), config.get("storage.silenceSingleServerWarning") === true);
44
+ if (warning) log.warn("storage", "single-server", warning);
39
45
  this.active = true;
40
46
  }
41
47
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"storage.connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/storage.connector.ts"],"sourcesContent":["import { storage } from \"../storage\";\nimport { loadS3 } from \"../storage/drivers/cloud-driver\";\nimport { BaseConnector } from \"./base-connector\";\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\n\n/**\n * Cache Connector\n * Manages cache engine connection lifecycle\n */\nexport class StorageConnector extends BaseConnector {\n public readonly name = \"storage\";\n public readonly priority = ConnectorPriority.STORAGE;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Early;\n\n /**\n * Files that trigger cache restart\n */\n protected readonly watchedFiles = [\"src/config/storage.ts\", \"src/config/storage.tsx\"];\n\n /**\n * Initialize storage.\n *\n * Unlike its siblings (database, cache, herald), this connector does NOT\n * early-return when `config.get(\"storage\")` is absent. Storage is\n * intentionally always-on: `storage.init()` registers a built-in `local`\n * driver rooted at `uploadsPath()` before reading configured drivers, so file\n * storage works out of the box even when a project ships no\n * `src/config/storage.ts`. A config-presence guard here would break that.\n *\n * That fallback is real as of 4.11.0. Until then this comment described it\n * and nothing implemented it, so every app without `src/config/storage.ts`\n * died at boot with `Storage driver \"local\" is not configured` — a decision\n * to skip the guard, taken on the authority of behaviour nobody had written.\n * See `Storage.registerBuiltInLocalDriver`.\n */\n public async start(): Promise<void> {\n await loadS3();\n await storage.init();\n\n this.active = true;\n }\n\n /**\n * Shutdown cache connection\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n storage.reset();\n\n this.active = false;\n }\n}\n"],"mappings":";;;;;;;;;;;AASA,IAAa,mBAAb,cAAsC,cAAc;;;cAC3B;;;sBAOW,CAAC,yBAAyB,wBAAwB;;;;;;;;;;;;;;;;;;CAkBpF,MAAa,QAAuB;EAClC,MAAM,OAAO;EACb,MAAM,QAAQ,KAAK;EAEnB,KAAK,SAAS;CAChB;;;;CAKA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAGF,QAAQ,MAAM;EAEd,KAAK,SAAS;CAChB;AACF"}
1
+ {"version":3,"file":"storage.connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/storage.connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { log } from \"@warlock.js/logger\";\nimport { storage } from \"../storage\";\nimport { environment } from \"../utils/environment\";\nimport { localStorageWarning } from \"./single-server-warnings\";\nimport { loadS3 } from \"../storage/drivers/cloud-driver\";\nimport { BaseConnector } from \"./base-connector\";\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\n\n/**\n * Cache Connector\n * Manages cache engine connection lifecycle\n */\nexport class StorageConnector extends BaseConnector {\n public readonly name = \"storage\";\n public readonly priority = ConnectorPriority.STORAGE;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Early;\n\n /**\n * Files that trigger cache restart\n */\n protected readonly watchedFiles = [\"src/config/storage.ts\", \"src/config/storage.tsx\"];\n\n /**\n * Initialize storage.\n *\n * Unlike its siblings (database, cache, herald), this connector does NOT\n * early-return when `config.get(\"storage\")` is absent. Storage is\n * intentionally always-on: `storage.init()` registers a built-in `local`\n * driver rooted at `uploadsPath()` before reading configured drivers, so file\n * storage works out of the box even when a project ships no\n * `src/config/storage.ts`. A config-presence guard here would break that.\n *\n * That fallback is real as of 4.11.0. Until then this comment described it\n * and nothing implemented it, so every app without `src/config/storage.ts`\n * died at boot with `Storage driver \"local\" is not configured` — a decision\n * to skip the guard, taken on the authority of behaviour nobody had written.\n * See `Storage.registerBuiltInLocalDriver`.\n */\n public async start(): Promise<void> {\n await loadS3();\n await storage.init();\n\n const warning = localStorageWarning(\n config.get(\"storage.default\", \"local\"),\n environment(),\n config.get(\"storage.silenceSingleServerWarning\") === true,\n );\n\n if (warning) log.warn(\"storage\", \"single-server\", warning);\n\n this.active = true;\n }\n\n /**\n * Shutdown cache connection\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n storage.reset();\n\n this.active = false;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAaA,IAAa,mBAAb,cAAsC,cAAc;;;cAC3B;;;sBAOW,CAAC,yBAAyB,wBAAwB;;;;;;;;;;;;;;;;;;CAkBpF,MAAa,QAAuB;EAClC,MAAM,OAAO;EACb,MAAM,QAAQ,KAAK;EAEnB,MAAM,UAAU,oBACd,OAAO,IAAI,mBAAmB,OAAO,GACrC,YAAY,GACZ,OAAO,IAAI,oCAAoC,MAAM,IACvD;EAEA,IAAI,SAAS,IAAI,KAAK,WAAW,iBAAiB,OAAO;EAEzD,KAAK,SAAS;CAChB;;;;CAKA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAGF,QAAQ,MAAM;EAEd,KAAK,SAAS;CAChB;AACF"}
@@ -1,3 +1,4 @@
1
+ import { NamedApiRoute } from "../router/types.mjs";
1
2
  import { ResolvedBuildConfig } from "../production/resolve-build-config.mjs";
2
3
  import { BuildOptions } from "esbuild";
3
4
 
@@ -69,7 +70,8 @@ interface Connector {
69
70
  type ConnectorBuildContext = {
70
71
  /** Absolute path to `.warlock/production` — deleted after the build */productionDir: string; /** Absolute path to the application root (where `package.json` lives) */
71
72
  appRoot: string; /** The build config after framework defaults and user overrides */
72
- options: ResolvedBuildConfig;
73
+ options: ResolvedBuildConfig; /** Fresh registration-only snapshot, present only when the Web connector is configured. */
74
+ readonly namedApiRoutes?: readonly NamedApiRoute[];
73
75
  };
74
76
  /**
75
77
  * The esbuild options a contributor may patch.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/types.ts"],"mappings":";;;;;;AAOA;;UAAiB,SAAA;EAIA;;;EAAA,SAAN,IAAA,EAAM,aAAA;EAmCJ;;;;EAAA,SA7BF,QAAA;EANA;;;;EAAA,SAYA,cAAA,EAAgB,uBAAA;EAKzB;;;EAAA,QAAA;EAYS;;;;EANT,IAAA,WAAe,OAAA;EAyBf;;;;EAnBA,KAAA,IAAS,OAAA;EA8BkC;AAO7C;;;EA/BE,OAAA,IAAW,OAAA;EAiCX;;;;EA3BA,QAAA,IAAY,OAAA;EA+BgB;AAa9B;;;;EArCE,aAAA,CAAc,YAAA;EA6CJ;;;;;;;;AAUqB;EAVrB,SAlCD,KAAA,GAAQ,0BAAA;AAAA;;;;;KAOP,qBAAA;EAqEK,uEAnEf,aAAA,UAmE8C;EAjE9C,OAAA,UAwDA;EAtDA,OAAA,EAAS,mBAAmB;AAAA;;;;;;;;;AA+DkB;AAGhD;KArDY,qBAAA,GAAwB,IAAI,CACtC,YAAA;;;AAoDuB;KA7Cb,4BAAA;EAoEuB;;;AAE7B;AAMN;;;EApEE,YAAA,aAqEA;EAnEA,OAAA,GAAU,qBAAqB;AAAA;;;;;;;;;;AA6E7B;;;;;KA5DQ,0BAAA;;;;;;EAMV,QAAA,EACE,OAAA,EAAS,qBAAA,GACR,OAAA,CAAQ,4BAAA,WAAuC,4BAAA;;;;;;EAOlD,IAAA,EAAM,OAAA,EAAS,qBAAA,GAAwB,OAAA;AAAA;AAAA,KAG7B,aAAA;;;;;;;;;;aAuBA,uBAAA;EACV,KAAA;EACA,IAAI;AAAA;;;;aAMM,iBAAA;EACV,MAAA;EACA,MAAA;EACA,QAAA;EACA,YAAA;EACA,KAAA;EACA,IAAA;EACA,OAAA;EACA,MAAA;EACA,aAAA;EACA,MAAA;EACA,EAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/types.ts"],"mappings":";;;;;;;AAQA;;UAAiB,SAAA;EAIA;;;EAAA,SAAN,IAAA,EAAM,aAAA;EAmCJ;;;;EAAA,SA7BF,QAAA;EANA;;;;EAAA,SAYA,cAAA,EAAgB,uBAAA;EAKzB;;;EAAA,QAAA;EAYS;;;;EANT,IAAA,WAAe,OAAA;EAyBf;;;;EAnBA,KAAA,IAAS,OAAA;EA8BkC;AAO7C;;;EA/BE,OAAA,IAAW,OAAA;EAiCX;;;;EA3BA,QAAA,IAAY,OAAA;EAiCuB;;AAAa;AAalD;;EAvCE,aAAA,CAAc,YAAA;EAuCoB;AACtB;AAOd;;;;;;;EARoC,SA5BzB,KAAA,GAAQ,0BAAA;AAAA;AA+DnB;;;;AAAA,KAxDY,qBAAA;EAgEP,uEA9DH,aAAA,UAqEe;EAnEf,OAAA,UAmE8C;EAjE9C,OAAA,EAAS,mBAAA,EAwDT;EAAA,SAtDS,cAAA,YAA0B,aAAa;AAAA;;;;;;;;;AA+DF;AAGhD;KArDY,qBAAA,GAAwB,IAAI,CACtC,YAAA;;;AAoDuB;KA7Cb,4BAAA;EAoEuB;;;AAE7B;AAMN;;;EApEE,YAAA,aAqEA;EAnEA,OAAA,GAAU,qBAAqB;AAAA;;;;;;;;;;AA6E7B;;;;;KA5DQ,0BAAA;;;;;;EAMV,QAAA,EACE,OAAA,EAAS,qBAAA,GACR,OAAA,CAAQ,4BAAA,WAAuC,4BAAA;;;;;;EAOlD,IAAA,EAAM,OAAA,EAAS,qBAAA,GAAwB,OAAA;AAAA;AAAA,KAG7B,aAAA;;;;;;;;;;aAuBA,uBAAA;EACV,KAAA;EACA,IAAI;AAAA;;;;aAMM,iBAAA;EACV,MAAA;EACA,MAAA;EACA,QAAA;EACA,YAAA;EACA,KAAA;EACA,IAAA;EACA,OAAA;EACA,MAAA;EACA,aAAA;EACA,MAAA;EACA,EAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../core/src/connectors/types.ts"],"sourcesContent":["import type { BuildOptions } from \"esbuild\";\nimport type { ResolvedBuildConfig } from \"../production/resolve-build-config\";\n\n/**\n * Connector Interface\n * All service connectors (Database, HTTP, Cache, etc.) must implement this interface\n */\nexport interface Connector {\n /**\n * Unique name of the connector\n */\n readonly name: ConnectorName;\n\n /**\n * Priority for initialization order\n * Lower numbers initialize first (e.g., Database: 1, HTTP: 3)\n */\n readonly priority: number;\n\n /**\n * Phase the connector boots in, relative to user code.\n * See `ConnectorLifecyclePhase` for semantics.\n */\n readonly lifecyclePhase: ConnectorLifecyclePhase;\n\n /**\n * Whether the connector is currently active/connected\n */\n isActive(): boolean;\n\n /**\n * Boot the connector\n * Called during server startup before starting the connector\n */\n boot(): void | Promise<void>;\n\n /**\n * Initialize the connector\n * Called during server startup\n */\n start(): Promise<void>;\n\n /**\n * Restart the connector\n * Called when watched files change\n */\n restart(): Promise<void>;\n\n /**\n * Gracefully shutdown the connector\n * Called during server shutdown\n */\n shutdown(): Promise<void>;\n\n /**\n * Determine if this connector needs restart based on changed files\n * @param changedFiles Array of relative file paths that changed\n * @returns true if connector should restart\n */\n shouldRestart(changedFiles: string[]): boolean;\n\n /**\n * What this connector contributes to `warlock build`.\n *\n * Read STATICALLY from `warlock.config.ts > connectors` — `warlock build`\n * never calls `boot()`/`start()`, so nothing here may assume a running\n * connector. The same `connectors` key drives both halves of a connector's\n * life — build-time contribution and runtime boot; see\n * {@link ConnectorBuildContribution}.\n */\n readonly build?: ConnectorBuildContribution;\n}\n\n/**\n * What a build contribution is handed. Everything it needs to write files\n * and shape the bundle, and nothing that would let it reach the runtime.\n */\nexport type ConnectorBuildContext = {\n /** Absolute path to `.warlock/production` — deleted after the build */\n productionDir: string;\n /** Absolute path to the application root (where `package.json` lives) */\n appRoot: string;\n /** The build config after framework defaults and user overrides */\n options: ResolvedBuildConfig;\n};\n\n/**\n * The esbuild options a contributor may patch.\n *\n * Deliberately a narrow `Pick`, not `Partial<BuildOptions>`: `entryPoints`,\n * `tsconfig` and `tsconfigRaw` stay forbidden here for the same reason they\n * are forbidden in the user's `build` block — they re-compile every file in\n * the bundle, workspace packages included (see `ForbiddenBuildOptionError`).\n * A contributor reaching them would reintroduce the failure the user-level\n * guard exists to prevent.\n */\nexport type ConnectorEsbuildPatch = Pick<\n BuildOptions,\n \"jsx\" | \"jsxImportSource\" | \"define\" | \"external\" | \"loader\"\n>;\n\n/**\n * What `generate` may hand back to the builder.\n */\nexport type ConnectorBuildGenerateResult = {\n /**\n * Lines appended verbatim to section 4 of the generated entry, AFTER the\n * `./routes` import — page registration must see the routes app code\n * collected, mirroring dev's ordering.\n *\n * @example ['await import(\"./pages\");']\n */\n entryImports?: string[];\n /** Merged into the esbuild call: defaults < this patch < user `build` */\n esbuild?: ConnectorEsbuildPatch;\n};\n\n/**\n * A connector's build-time half: two hooks, both awaited, drained in\n * `connectors` array order. A hook that throws fails the build.\n *\n * **This type is closed on purpose**: exactly these two optional function properties and ZERO data-bearing\n * fields. There is deliberately no property where a Vite/babel plugin\n * instance, an alias map or a compiled pipeline could live, so\n * `build: { plugins: [...] }` is a type error rather than something a\n * reviewer has to catch. Anything heavy must be constructed INSIDE a hook,\n * after that hook dynamically imports it — that is what keeps a connector's\n * static import graph out of the runtime closure. The drain mirrors this at\n * read time by rejecting unknown enumerable keys (`assertClosedContribution`):\n * the type narrows, the runtime asserts.\n */\nexport type ConnectorBuildContribution = {\n /**\n * Write files into `ctx.productionDir` and/or patch the bundle.\n *\n * Runs after the generated-imports check and before esbuild bundles.\n */\n generate?(\n context: ConnectorBuildContext,\n ): Promise<ConnectorBuildGenerateResult | void> | ConnectorBuildGenerateResult | void;\n\n /**\n * Produce artifacts esbuild does not — e.g. a client bundle.\n *\n * Runs after esbuild and before `.warlock/production` is removed.\n */\n emit?(context: ConnectorBuildContext): Promise<void> | void;\n};\n\nexport type ConnectorName =\n | \"logger\"\n | \"mailer\"\n | \"http\"\n | \"database\"\n | \"cache\"\n | \"storage\"\n | \"herald\"\n | \"socket\"\n | \"notifications\"\n | \"access\"\n | \"ai\"\n | (string & {});\n\n/**\n * When a connector boots relative to user code (main, routes, events).\n *\n * - `Early`: before user code is imported. For services user code\n * needs at import time (database, cache, logger, storage, mailer).\n * - `Late`: after user code is imported. For services that bind to\n * things user code registered (http scans the router; socket\n * depends on http's instance).\n */\nexport enum ConnectorLifecyclePhase {\n Early = \"early\",\n Late = \"late\",\n}\n\n/**\n * Connector priority constants\n */\nexport enum ConnectorPriority {\n LOGGER = 0,\n MAILER = 1,\n DATABASE = 2,\n COMMUNICATOR = 3,\n CACHE = 4,\n HTTP = 5,\n STORAGE = 6,\n SOCKET = 7,\n NOTIFICATIONS = 8,\n ACCESS = 9,\n AI = 10,\n}\n"],"mappings":";;;;;;;;;;AA4KA,IAAY,0BAAL;CACL;CACA;;AACF;;;;AAKA,IAAY,oBAAL;CACL;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;AACF"}
1
+ {"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../core/src/connectors/types.ts"],"sourcesContent":["import type { BuildOptions } from \"esbuild\";\nimport type { NamedApiRoute } from \"../router/types\";\nimport type { ResolvedBuildConfig } from \"../production/resolve-build-config\";\n\n/**\n * Connector Interface\n * All service connectors (Database, HTTP, Cache, etc.) must implement this interface\n */\nexport interface Connector {\n /**\n * Unique name of the connector\n */\n readonly name: ConnectorName;\n\n /**\n * Priority for initialization order\n * Lower numbers initialize first (e.g., Database: 1, HTTP: 3)\n */\n readonly priority: number;\n\n /**\n * Phase the connector boots in, relative to user code.\n * See `ConnectorLifecyclePhase` for semantics.\n */\n readonly lifecyclePhase: ConnectorLifecyclePhase;\n\n /**\n * Whether the connector is currently active/connected\n */\n isActive(): boolean;\n\n /**\n * Boot the connector\n * Called during server startup before starting the connector\n */\n boot(): void | Promise<void>;\n\n /**\n * Initialize the connector\n * Called during server startup\n */\n start(): Promise<void>;\n\n /**\n * Restart the connector\n * Called when watched files change\n */\n restart(): Promise<void>;\n\n /**\n * Gracefully shutdown the connector\n * Called during server shutdown\n */\n shutdown(): Promise<void>;\n\n /**\n * Determine if this connector needs restart based on changed files\n * @param changedFiles Array of relative file paths that changed\n * @returns true if connector should restart\n */\n shouldRestart(changedFiles: string[]): boolean;\n\n /**\n * What this connector contributes to `warlock build`.\n *\n * Read STATICALLY from `warlock.config.ts > connectors` — `warlock build`\n * never calls `boot()`/`start()`, so nothing here may assume a running\n * connector. The same `connectors` key drives both halves of a connector's\n * life — build-time contribution and runtime boot; see\n * {@link ConnectorBuildContribution}.\n */\n readonly build?: ConnectorBuildContribution;\n}\n\n/**\n * What a build contribution is handed. Everything it needs to write files\n * and shape the bundle, and nothing that would let it reach the runtime.\n */\nexport type ConnectorBuildContext = {\n /** Absolute path to `.warlock/production` — deleted after the build */\n productionDir: string;\n /** Absolute path to the application root (where `package.json` lives) */\n appRoot: string;\n /** The build config after framework defaults and user overrides */\n options: ResolvedBuildConfig;\n /** Fresh registration-only snapshot, present only when the Web connector is configured. */\n readonly namedApiRoutes?: readonly NamedApiRoute[];\n};\n\n/**\n * The esbuild options a contributor may patch.\n *\n * Deliberately a narrow `Pick`, not `Partial<BuildOptions>`: `entryPoints`,\n * `tsconfig` and `tsconfigRaw` stay forbidden here for the same reason they\n * are forbidden in the user's `build` block — they re-compile every file in\n * the bundle, workspace packages included (see `ForbiddenBuildOptionError`).\n * A contributor reaching them would reintroduce the failure the user-level\n * guard exists to prevent.\n */\nexport type ConnectorEsbuildPatch = Pick<\n BuildOptions,\n \"jsx\" | \"jsxImportSource\" | \"define\" | \"external\" | \"loader\"\n>;\n\n/**\n * What `generate` may hand back to the builder.\n */\nexport type ConnectorBuildGenerateResult = {\n /**\n * Lines appended verbatim to section 4 of the generated entry, AFTER the\n * `./routes` import — page registration must see the routes app code\n * collected, mirroring dev's ordering.\n *\n * @example ['await import(\"./pages\");']\n */\n entryImports?: string[];\n /** Merged into the esbuild call: defaults < this patch < user `build` */\n esbuild?: ConnectorEsbuildPatch;\n};\n\n/**\n * A connector's build-time half: two hooks, both awaited, drained in\n * `connectors` array order. A hook that throws fails the build.\n *\n * **This type is closed on purpose**: exactly these two optional function properties and ZERO data-bearing\n * fields. There is deliberately no property where a Vite/babel plugin\n * instance, an alias map or a compiled pipeline could live, so\n * `build: { plugins: [...] }` is a type error rather than something a\n * reviewer has to catch. Anything heavy must be constructed INSIDE a hook,\n * after that hook dynamically imports it — that is what keeps a connector's\n * static import graph out of the runtime closure. The drain mirrors this at\n * read time by rejecting unknown enumerable keys (`assertClosedContribution`):\n * the type narrows, the runtime asserts.\n */\nexport type ConnectorBuildContribution = {\n /**\n * Write files into `ctx.productionDir` and/or patch the bundle.\n *\n * Runs after the generated-imports check and before esbuild bundles.\n */\n generate?(\n context: ConnectorBuildContext,\n ): Promise<ConnectorBuildGenerateResult | void> | ConnectorBuildGenerateResult | void;\n\n /**\n * Produce artifacts esbuild does not — e.g. a client bundle.\n *\n * Runs after esbuild and before `.warlock/production` is removed.\n */\n emit?(context: ConnectorBuildContext): Promise<void> | void;\n};\n\nexport type ConnectorName =\n | \"logger\"\n | \"mailer\"\n | \"http\"\n | \"database\"\n | \"cache\"\n | \"storage\"\n | \"herald\"\n | \"socket\"\n | \"notifications\"\n | \"access\"\n | \"ai\"\n | (string & {});\n\n/**\n * When a connector boots relative to user code (main, routes, events).\n *\n * - `Early`: before user code is imported. For services user code\n * needs at import time (database, cache, logger, storage, mailer).\n * - `Late`: after user code is imported. For services that bind to\n * things user code registered (http scans the router; socket\n * depends on http's instance).\n */\nexport enum ConnectorLifecyclePhase {\n Early = \"early\",\n Late = \"late\",\n}\n\n/**\n * Connector priority constants\n */\nexport enum ConnectorPriority {\n LOGGER = 0,\n MAILER = 1,\n DATABASE = 2,\n COMMUNICATOR = 3,\n CACHE = 4,\n HTTP = 5,\n STORAGE = 6,\n SOCKET = 7,\n NOTIFICATIONS = 8,\n ACCESS = 9,\n AI = 10,\n}\n"],"mappings":";;;;;;;;;;AA+KA,IAAY,0BAAL;CACL;CACA;;AACF;;;;AAKA,IAAY,oBAAL;CACL;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;AACF"}
@@ -1,4 +1,5 @@
1
1
  import { Application } from "../application/application.mjs";
2
+ import { router } from "../router/router.mjs";
2
3
  import "../application/index.mjs";
3
4
  import { ConnectorLifecyclePhase } from "../connectors/types.mjs";
4
5
  import { connectorsManager } from "../connectors/connectors-manager.mjs";
@@ -7,6 +8,7 @@ import "../warlock-config/index.mjs";
7
8
  import { devLogInfo, devLogSection, devLogWarn, devServeLog } from "./dev-logger.mjs";
8
9
  import { MANIFEST_PATH } from "./flags.mjs";
9
10
  import { filesOrchestrator } from "./files-orchestrator.mjs";
11
+ import { publishCurrentRouteTypes } from "./route-types-publisher.mjs";
10
12
  import { restartDevServer } from "./restart-dev-server.mjs";
11
13
  import { devServerShortcuts } from "./shortcuts.mjs";
12
14
  import { BootPreconditionError } from "./boot-precondition-error.mjs";
@@ -49,6 +51,7 @@ var DevelopmentServer = class {
49
51
  throw new BootPreconditionError(error.message, { cause: error });
50
52
  }
51
53
  await connectorsManager.startPhase("late");
54
+ await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());
52
55
  this.layerExecutor = new LayerExecutor(filesOrchestrator.getDependencyGraph(), filesOrchestrator.specialFilesCollector, filesOrchestrator.moduleLoader, (absolutePath) => filesOrchestrator.bumpVersion(absolutePath), () => filesOrchestrator.flushVersionBumps());
53
56
  this.running = true;
54
57
  const duration = performance.now() - startedAt;
@@ -88,6 +91,7 @@ var DevelopmentServer = class {
88
91
  const codeFiles = [...batch.added, ...batch.changed].filter((p) => !isEnvPath(p) && p !== "warlock.config.ts");
89
92
  try {
90
93
  await this.layerExecutor.executeBatchReload(codeFiles, filesOrchestrator.getFiles(), batch.deleted, batch.changed, batch.timings);
94
+ await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());
91
95
  typeGenerator.executeTypingsGenerator([
92
96
  ...batch.added,
93
97
  ...batch.changed,
@@ -1 +1 @@
1
- {"version":3,"file":"development-server.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/development-server.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport events from \"@mongez/events\";\r\nimport { fileExistsAsync, unlinkAsync } from \"@warlock.js/fs\";\r\nimport { Application } from \"../application\";\r\nimport { connectorsManager } from \"../connectors/connectors-manager\";\r\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\r\nimport { warlockConfigManager } from \"../warlock-config\";\r\nimport { BootPreconditionError } from \"./boot-precondition-error\";\r\nimport { devLogInfo, devLogSection, devLogWarn, devServeLog } from \"./dev-logger\";\r\nimport { filesOrchestrator } from \"./files-orchestrator\";\r\nimport { MANIFEST_PATH } from \"./flags\";\r\nimport type { IncomingReloadTimings } from \"./layer-executor\";\r\nimport { LayerExecutor } from \"./layer-executor\";\r\nimport { printReadyBlock } from \"./ready-block\";\r\nimport { restartDevServer } from \"./restart-dev-server\";\r\nimport { devServerShortcuts } from \"./shortcuts\";\r\nimport type { StartDevServerOptions } from \"./start-development-server\";\r\nimport { typeGenerator } from \"./type-generator\";\r\n\r\ntype Batch = {\r\n added: string[];\r\n changed: string[];\r\n deleted: string[];\r\n timings?: IncomingReloadTimings;\r\n};\r\n\r\n/**\r\n * Top-level coordinator for `warlock dev`. Wires the file orchestrator, the\r\n * connectors, and the layer executor together, and listens for the watcher's\r\n * batched events to drive HMR.\r\n */\r\nexport class DevelopmentServer {\r\n private layerExecutor?: LayerExecutor;\r\n private running = false;\r\n private readonly options: StartDevServerOptions;\r\n\r\n public constructor(options: StartDevServerOptions = {}) {\r\n this.options = options;\r\n devLogSection(\"Starting Development Server...\");\r\n }\r\n\r\n public async start(): Promise<void> {\r\n try {\r\n const startedAt = performance.now();\r\n\r\n // --fresh deletes the manifest so reconciliation re-parses every file\r\n // from disk. Transpile caching is owned by the loader hook (in-memory).\r\n if (this.options.fresh && (await fileExistsAsync(MANIFEST_PATH))) {\r\n await unlinkAsync(MANIFEST_PATH);\r\n devServeLog(colors.cyanBright(\"Cleared manifest (--fresh)\"));\r\n }\r\n\r\n await filesOrchestrator.init();\r\n await filesOrchestrator.initializeAll();\r\n await filesOrchestrator.watchFiles();\r\n\r\n filesOrchestrator.specialFilesCollector.collect(filesOrchestrator.getFiles());\r\n\r\n this.setupEventListeners();\r\n\r\n // Decorator-driven registries (models, etc.) must be populated before\r\n // routes/services can resolve symbols by name.\r\n await this.autoDiscoverFiles();\r\n\r\n await filesOrchestrator.moduleLoader.loadAll();\r\n\r\n // A rejecting validator (Application.onValidateBoot) must abort boot\r\n // before late-phase connectors bind a port — this is what makes the\r\n // hook actually prevent serving instead of merely being defined.\r\n //\r\n // Wrapped and re-thrown as a BootPreconditionError: a rejected\r\n // validator is, by definition, a precondition whose cause cannot\r\n // change because the worker tried again — the supervisor must not\r\n // restart into the same rejection. See boot-precondition-error.ts.\r\n try {\r\n await Application.runStartupValidators();\r\n } catch (error) {\r\n throw new BootPreconditionError((error as Error).message, { cause: error });\r\n }\r\n\r\n // Late-phase connectors (http, socket) bind after app code has\r\n // registered routes/listeners.\r\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\r\n\r\n this.layerExecutor = new LayerExecutor(\r\n filesOrchestrator.getDependencyGraph(),\r\n filesOrchestrator.specialFilesCollector,\r\n filesOrchestrator.moduleLoader,\r\n (absolutePath) => filesOrchestrator.bumpVersion(absolutePath),\r\n () => filesOrchestrator.flushVersionBumps(),\r\n );\r\n\r\n this.running = true;\r\n\r\n const duration = performance.now() - startedAt;\r\n\r\n // App modules are loaded and both connector phases are active — signal a\r\n // complete boot so `Application.onceBooted(...)` listeners fire.\r\n Application.markBooted({\r\n environment: Application.environment,\r\n runtimeStrategy: Application.runtimeStrategy,\r\n bootDurationMs: duration,\r\n });\r\n\r\n // ONE block, AFTER the boot it summarises, replacing the scattered\r\n // \"N route(s) registered\" / \"Server ready at …\" / \"ready in …\" lines that\r\n // used to arrive in completion order. Warnings logged during boot sit\r\n // above it and stay there — see `ready-block.ts`.\r\n printReadyBlock(duration);\r\n\r\n // Precedence: explicit CLI option > devServer.* config > default.\r\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\r\n const generateTypings =\r\n this.options.generateTypings ?? devServerConfig?.generateTypings ?? true;\r\n const healthCheckers = this.options.healthCheckers ?? devServerConfig?.healthCheckers ?? true;\r\n\r\n if (generateTypings) await typeGenerator.executeGenerateAllCommand();\r\n\r\n if (healthCheckers) {\r\n filesOrchestrator.startCheckingHealth(healthCheckers === true ? undefined : healthCheckers);\r\n }\r\n } catch (error) {\r\n devServeLog(colors.redBright(`Failed to start Development Server: ${error}`));\r\n await this.shutdown();\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Eagerly import files whose decorators populate global registries so any\r\n * symbol-by-name resolution later in boot finds them.\r\n */\r\n private async autoDiscoverFiles(): Promise<void> {\r\n const discoveryTypes = [\"model\"] as const;\r\n for (const file of filesOrchestrator.files.values()) {\r\n if (file.type && (discoveryTypes as readonly string[]).includes(file.type)) {\r\n await filesOrchestrator.moduleLoader.loadModule(file, file.type);\r\n }\r\n }\r\n }\r\n\r\n private setupEventListeners(): void {\r\n events.on(\"dev-server:batch-complete\", (batch: Batch) => this.handleBatchComplete(batch));\r\n }\r\n\r\n private async handleBatchComplete(batch: Batch): Promise<void> {\r\n if (!this.running || !this.layerExecutor) return;\r\n\r\n // warlock.config.ts holds settings read at boot (CLI commands, build\r\n // options, watch patterns, scheduled jobs) and .env feeds every config\r\n // that reads it. Neither can be hot-reloaded without leaving running\r\n // services configured with stale values, so the only honest response is\r\n // a full restart.\r\n const restartTrigger = this.findRestartTrigger(batch);\r\n\r\n if (restartTrigger) {\r\n // On success this never comes back — the process exits and the\r\n // supervisor replaces it. Reaching the next line means the restart was\r\n // declined by config or wasn't possible, so we fall through and still\r\n // hot-reload whatever ordinary code shared the batch.\r\n await this.restartForConfigChange(restartTrigger);\r\n }\r\n\r\n // No-op saves (an editor that fsyncs without a content write) are already\r\n // filtered upstream by the file hash in FileEventHandler, so batch.changed\r\n // only contains genuinely-changed paths by the time it reaches here.\r\n\r\n const total = batch.added.length + batch.changed.length + batch.deleted.length;\r\n if (total === 0) return;\r\n\r\n // Boot-time files still ride along in `changed` when a restart was\r\n // declined (`restartOnConfigChange: false`) or wasn't possible — they are\r\n // never hot-reloaded, so keep them out of the reload set either way.\r\n const codeFiles = [...batch.added, ...batch.changed].filter(\r\n (p) => !isEnvPath(p) && p !== \"warlock.config.ts\",\r\n );\r\n\r\n try {\r\n await this.layerExecutor.executeBatchReload(\r\n codeFiles,\r\n filesOrchestrator.getFiles(),\r\n batch.deleted,\r\n batch.changed,\r\n batch.timings,\r\n );\r\n\r\n typeGenerator.executeTypingsGenerator([...batch.added, ...batch.changed, ...batch.deleted]);\r\n\r\n filesOrchestrator.checkHealth(batch);\r\n } catch (error) {\r\n devServeLog(colors.redBright(`Failed to execute batch reload: ${error}`));\r\n }\r\n }\r\n\r\n /**\r\n * The boot-time file in this batch that can only be applied by restarting,\r\n * or `undefined` when the batch is ordinary application code.\r\n */\r\n private findRestartTrigger(batch: Batch): string | undefined {\r\n if (batch.changed.includes(\"warlock.config.ts\")) {\r\n return \"warlock.config.ts\";\r\n }\r\n\r\n return [...batch.added, ...batch.changed].find(isEnvPath);\r\n }\r\n\r\n /**\r\n * Restart so a changed boot-time file actually takes effect.\r\n *\r\n * Opt out with `devServer.restartOnConfigChange: false` and the old\r\n * behaviour returns: a warning telling you to restart yourself. If the\r\n * restart can't be performed at all (no supervisor, a shutdown that threw),\r\n * `restartDevServer` reports it and we fall back to the same warning rather\r\n * than leaving the server running against config it isn't using.\r\n */\r\n private async restartForConfigChange(file: string): Promise<void> {\r\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\r\n\r\n if (devServerConfig?.restartOnConfigChange === false) {\r\n devLogWarn(`${file} changed — restart the dev server to apply.`);\r\n return;\r\n }\r\n\r\n devLogInfo(`${file} changed — restarting to apply.`);\r\n\r\n const restarted = await restartDevServer(this);\r\n\r\n if (!restarted) {\r\n devLogWarn(`${file} changed — restart the dev server to apply.`);\r\n }\r\n }\r\n\r\n public async shutdown(): Promise<void> {\r\n // Always hand the terminal back, even on a repeat/no-op shutdown — a\r\n // process that exits while stdin is still in raw mode leaves the user's\r\n // shell without line editing or Ctrl+C.\r\n devServerShortcuts.release();\r\n\r\n if (!this.running) return;\r\n devServeLog(colors.redBright(\"Shutting down Development Server...\"));\r\n this.running = false;\r\n await connectorsManager.shutdown();\r\n devServeLog(colors.greenBright(\"Development Server stopped\"));\r\n }\r\n\r\n public isRunning(): boolean {\r\n return this.running;\r\n }\r\n}\r\n\r\nfunction isEnvPath(path: string): boolean {\r\n const basename = path.split(\"/\").pop() ?? path;\r\n return basename === \".env\" || basename.startsWith(\".env.\");\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"development-server.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/development-server.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport events from \"@mongez/events\";\r\nimport { fileExistsAsync, unlinkAsync } from \"@warlock.js/fs\";\r\nimport { Application } from \"../application\";\r\nimport { connectorsManager } from \"../connectors/connectors-manager\";\r\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\r\nimport { router } from \"../router/router\";\r\nimport { warlockConfigManager } from \"../warlock-config\";\r\nimport { BootPreconditionError } from \"./boot-precondition-error\";\r\nimport { devLogInfo, devLogSection, devLogWarn, devServeLog } from \"./dev-logger\";\r\nimport { filesOrchestrator } from \"./files-orchestrator\";\r\nimport { MANIFEST_PATH } from \"./flags\";\r\nimport type { IncomingReloadTimings } from \"./layer-executor\";\r\nimport { LayerExecutor } from \"./layer-executor\";\r\nimport { printReadyBlock } from \"./ready-block\";\r\nimport { restartDevServer } from \"./restart-dev-server\";\r\nimport { publishCurrentRouteTypes } from \"./route-types-publisher\";\r\nimport { devServerShortcuts } from \"./shortcuts\";\r\nimport type { StartDevServerOptions } from \"./start-development-server\";\r\nimport { typeGenerator } from \"./type-generator\";\r\n\r\ntype Batch = {\r\n added: string[];\r\n changed: string[];\r\n deleted: string[];\r\n timings?: IncomingReloadTimings;\r\n};\r\n\r\n/**\r\n * Top-level coordinator for `warlock dev`. Wires the file orchestrator, the\r\n * connectors, and the layer executor together, and listens for the watcher's\r\n * batched events to drive HMR.\r\n */\r\nexport class DevelopmentServer {\r\n private layerExecutor?: LayerExecutor;\r\n private running = false;\r\n private readonly options: StartDevServerOptions;\r\n\r\n public constructor(options: StartDevServerOptions = {}) {\r\n this.options = options;\r\n devLogSection(\"Starting Development Server...\");\r\n }\r\n\r\n public async start(): Promise<void> {\r\n try {\r\n const startedAt = performance.now();\r\n\r\n // --fresh deletes the manifest so reconciliation re-parses every file\r\n // from disk. Transpile caching is owned by the loader hook (in-memory).\r\n if (this.options.fresh && (await fileExistsAsync(MANIFEST_PATH))) {\r\n await unlinkAsync(MANIFEST_PATH);\r\n devServeLog(colors.cyanBright(\"Cleared manifest (--fresh)\"));\r\n }\r\n\r\n await filesOrchestrator.init();\r\n await filesOrchestrator.initializeAll();\r\n await filesOrchestrator.watchFiles();\r\n\r\n filesOrchestrator.specialFilesCollector.collect(filesOrchestrator.getFiles());\r\n\r\n this.setupEventListeners();\r\n\r\n // Decorator-driven registries (models, etc.) must be populated before\r\n // routes/services can resolve symbols by name.\r\n await this.autoDiscoverFiles();\r\n\r\n await filesOrchestrator.moduleLoader.loadAll();\r\n\r\n // A rejecting validator (Application.onValidateBoot) must abort boot\r\n // before late-phase connectors bind a port — this is what makes the\r\n // hook actually prevent serving instead of merely being defined.\r\n //\r\n // Wrapped and re-thrown as a BootPreconditionError: a rejected\r\n // validator is, by definition, a precondition whose cause cannot\r\n // change because the worker tried again — the supervisor must not\r\n // restart into the same rejection. See boot-precondition-error.ts.\r\n try {\r\n await Application.runStartupValidators();\r\n } catch (error) {\r\n throw new BootPreconditionError((error as Error).message, { cause: error });\r\n }\r\n\r\n // Late-phase connectors (http, socket) bind after app code has\r\n // registered routes/listeners.\r\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\r\n\r\n // Web installs its page table from its late connector. Generate only\r\n // after that installation so `href()` and the declaration table agree.\r\n await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());\r\n\r\n this.layerExecutor = new LayerExecutor(\r\n filesOrchestrator.getDependencyGraph(),\r\n filesOrchestrator.specialFilesCollector,\r\n filesOrchestrator.moduleLoader,\r\n (absolutePath) => filesOrchestrator.bumpVersion(absolutePath),\r\n () => filesOrchestrator.flushVersionBumps(),\r\n );\r\n\r\n this.running = true;\r\n\r\n const duration = performance.now() - startedAt;\r\n\r\n // App modules are loaded and both connector phases are active — signal a\r\n // complete boot so `Application.onceBooted(...)` listeners fire.\r\n Application.markBooted({\r\n environment: Application.environment,\r\n runtimeStrategy: Application.runtimeStrategy,\r\n bootDurationMs: duration,\r\n });\r\n\r\n // ONE block, AFTER the boot it summarises, replacing the scattered\r\n // \"N route(s) registered\" / \"Server ready at …\" / \"ready in …\" lines that\r\n // used to arrive in completion order. Warnings logged during boot sit\r\n // above it and stay there — see `ready-block.ts`.\r\n printReadyBlock(duration);\r\n\r\n // Precedence: explicit CLI option > devServer.* config > default.\r\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\r\n const generateTypings =\r\n this.options.generateTypings ?? devServerConfig?.generateTypings ?? true;\r\n const healthCheckers = this.options.healthCheckers ?? devServerConfig?.healthCheckers ?? true;\r\n\r\n if (generateTypings) await typeGenerator.executeGenerateAllCommand();\r\n\r\n if (healthCheckers) {\r\n filesOrchestrator.startCheckingHealth(healthCheckers === true ? undefined : healthCheckers);\r\n }\r\n } catch (error) {\r\n devServeLog(colors.redBright(`Failed to start Development Server: ${error}`));\r\n await this.shutdown();\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Eagerly import files whose decorators populate global registries so any\r\n * symbol-by-name resolution later in boot finds them.\r\n */\r\n private async autoDiscoverFiles(): Promise<void> {\r\n const discoveryTypes = [\"model\"] as const;\r\n for (const file of filesOrchestrator.files.values()) {\r\n if (file.type && (discoveryTypes as readonly string[]).includes(file.type)) {\r\n await filesOrchestrator.moduleLoader.loadModule(file, file.type);\r\n }\r\n }\r\n }\r\n\r\n private setupEventListeners(): void {\r\n events.on(\"dev-server:batch-complete\", (batch: Batch) => this.handleBatchComplete(batch));\r\n }\r\n\r\n private async handleBatchComplete(batch: Batch): Promise<void> {\r\n if (!this.running || !this.layerExecutor) return;\r\n\r\n // warlock.config.ts holds settings read at boot (CLI commands, build\r\n // options, watch patterns, scheduled jobs) and .env feeds every config\r\n // that reads it. Neither can be hot-reloaded without leaving running\r\n // services configured with stale values, so the only honest response is\r\n // a full restart.\r\n const restartTrigger = this.findRestartTrigger(batch);\r\n\r\n if (restartTrigger) {\r\n // On success this never comes back — the process exits and the\r\n // supervisor replaces it. Reaching the next line means the restart was\r\n // declined by config or wasn't possible, so we fall through and still\r\n // hot-reload whatever ordinary code shared the batch.\r\n await this.restartForConfigChange(restartTrigger);\r\n }\r\n\r\n // No-op saves (an editor that fsyncs without a content write) are already\r\n // filtered upstream by the file hash in FileEventHandler, so batch.changed\r\n // only contains genuinely-changed paths by the time it reaches here.\r\n\r\n const total = batch.added.length + batch.changed.length + batch.deleted.length;\r\n if (total === 0) return;\r\n\r\n // Boot-time files still ride along in `changed` when a restart was\r\n // declined (`restartOnConfigChange: false`) or wasn't possible — they are\r\n // never hot-reloaded, so keep them out of the reload set either way.\r\n const codeFiles = [...batch.added, ...batch.changed].filter(\r\n (p) => !isEnvPath(p) && p !== \"warlock.config.ts\",\r\n );\r\n\r\n try {\r\n await this.layerExecutor.executeBatchReload(\r\n codeFiles,\r\n filesOrchestrator.getFiles(),\r\n batch.deleted,\r\n batch.changed,\r\n batch.timings,\r\n );\r\n\r\n // `executeBatchReload` includes route re-imports and affected connector\r\n // restarts. Publishing earlier could freeze a partial page/API view.\r\n // This also refreshes deletion-only batches after their cleanup/restart.\r\n await publishCurrentRouteTypes(process.cwd(), router.getNamedApiRoutes());\r\n\r\n typeGenerator.executeTypingsGenerator([...batch.added, ...batch.changed, ...batch.deleted]);\r\n\r\n filesOrchestrator.checkHealth(batch);\r\n } catch (error) {\r\n devServeLog(colors.redBright(`Failed to execute batch reload: ${error}`));\r\n }\r\n }\r\n\r\n /**\r\n * The boot-time file in this batch that can only be applied by restarting,\r\n * or `undefined` when the batch is ordinary application code.\r\n */\r\n private findRestartTrigger(batch: Batch): string | undefined {\r\n if (batch.changed.includes(\"warlock.config.ts\")) {\r\n return \"warlock.config.ts\";\r\n }\r\n\r\n return [...batch.added, ...batch.changed].find(isEnvPath);\r\n }\r\n\r\n /**\r\n * Restart so a changed boot-time file actually takes effect.\r\n *\r\n * Opt out with `devServer.restartOnConfigChange: false` and the old\r\n * behaviour returns: a warning telling you to restart yourself. If the\r\n * restart can't be performed at all (no supervisor, a shutdown that threw),\r\n * `restartDevServer` reports it and we fall back to the same warning rather\r\n * than leaving the server running against config it isn't using.\r\n */\r\n private async restartForConfigChange(file: string): Promise<void> {\r\n const devServerConfig = await warlockConfigManager.get(\"devServer\");\r\n\r\n if (devServerConfig?.restartOnConfigChange === false) {\r\n devLogWarn(`${file} changed — restart the dev server to apply.`);\r\n return;\r\n }\r\n\r\n devLogInfo(`${file} changed — restarting to apply.`);\r\n\r\n const restarted = await restartDevServer(this);\r\n\r\n if (!restarted) {\r\n devLogWarn(`${file} changed — restart the dev server to apply.`);\r\n }\r\n }\r\n\r\n public async shutdown(): Promise<void> {\r\n // Always hand the terminal back, even on a repeat/no-op shutdown — a\r\n // process that exits while stdin is still in raw mode leaves the user's\r\n // shell without line editing or Ctrl+C.\r\n devServerShortcuts.release();\r\n\r\n if (!this.running) return;\r\n devServeLog(colors.redBright(\"Shutting down Development Server...\"));\r\n this.running = false;\r\n await connectorsManager.shutdown();\r\n devServeLog(colors.greenBright(\"Development Server stopped\"));\r\n }\r\n\r\n public isRunning(): boolean {\r\n return this.running;\r\n }\r\n}\r\n\r\nfunction isEnvPath(path: string): boolean {\r\n const basename = path.split(\"/\").pop() ?? path;\r\n return basename === \".env\" || basename.startsWith(\".env.\");\r\n}\r\n"],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { resolve } from "node:path";
2
+ import { readFile, unlink } from "node:fs/promises";
3
+ import { pathToFileURL } from "node:url";
4
+ import { moduleResolve } from "import-meta-resolve";
5
+
6
+ //#region ../core/src/dev-server/route-types-publisher.ts
7
+ const WEB_PACKAGE = "@warlock.js/web";
8
+ const WEB_BUILD_PACKAGE = "@warlock.js/web/build";
9
+ const GENERATED_ROUTE_TYPES = ".warlock/typings/web-routes.d.ts";
10
+ const GENERATED_ROUTE_TYPES_HEADER = "// AUTO-GENERATED by @warlock.js/web -- do not edit.";
11
+ const defaultPorts = {
12
+ resolveModule: moduleResolve,
13
+ importModule: async (url) => await import(url.href)
14
+ };
15
+ function isAbsentOptionalWebPackage(thrown) {
16
+ return typeof thrown === "object" && thrown !== null && "code" in thrown && thrown.code === "ERR_MODULE_NOT_FOUND" && thrown instanceof Error && thrown.message.includes(`Cannot find package '${WEB_PACKAGE}'`);
17
+ }
18
+ /**
19
+ * Publish dev route declarations through the consuming app's optional Web
20
+ * build entry. A Core-only app performs no work; an installed but incompatible
21
+ * Web package fails loudly instead of leaving stale generated declarations.
22
+ */
23
+ async function publishCurrentRouteTypes(appRoot, apis, ports = defaultPorts) {
24
+ const projectUrl = pathToFileURL(resolve(appRoot, "package.json"));
25
+ try {
26
+ ports.resolveModule(WEB_PACKAGE, projectUrl);
27
+ } catch (error) {
28
+ if (isAbsentOptionalWebPackage(error)) {
29
+ await clearOwnedRouteTypes(appRoot);
30
+ return false;
31
+ }
32
+ throw error;
33
+ }
34
+ const buildModuleUrl = ports.resolveModule(WEB_BUILD_PACKAGE, projectUrl);
35
+ const buildModule = await ports.importModule(buildModuleUrl);
36
+ if (typeof buildModule.writeCurrentRouteTypes !== "function") throw new Error(`${WEB_BUILD_PACKAGE} does not export writeCurrentRouteTypes. Upgrade @warlock.js/web to a compatible version.`);
37
+ await buildModule.writeCurrentRouteTypes({
38
+ appRoot,
39
+ apis
40
+ });
41
+ return true;
42
+ }
43
+ /** Remove only this integration's header-proven generated declaration file. */
44
+ async function clearOwnedRouteTypes(appRoot) {
45
+ const target = resolve(appRoot, GENERATED_ROUTE_TYPES);
46
+ let content;
47
+ try {
48
+ content = await readFile(target, "utf8");
49
+ } catch (error) {
50
+ if (error.code === "ENOENT") return false;
51
+ throw error;
52
+ }
53
+ if (!content.startsWith(GENERATED_ROUTE_TYPES_HEADER)) return false;
54
+ await unlink(target);
55
+ return true;
56
+ }
57
+
58
+ //#endregion
59
+ export { clearOwnedRouteTypes, publishCurrentRouteTypes };
60
+ //# sourceMappingURL=route-types-publisher.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-types-publisher.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/route-types-publisher.ts"],"sourcesContent":["import { moduleResolve } from \"import-meta-resolve\";\nimport { readFile, unlink } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { NamedApiRoute } from \"../router/types\";\n\nconst WEB_PACKAGE = \"@warlock.js/web\";\nconst WEB_BUILD_PACKAGE = \"@warlock.js/web/build\";\nconst GENERATED_ROUTE_TYPES = \".warlock/typings/web-routes.d.ts\";\nconst GENERATED_ROUTE_TYPES_HEADER = \"// AUTO-GENERATED by @warlock.js/web -- do not edit.\";\n\ntype WebBuildModule = {\n writeCurrentRouteTypes?: (input: {\n appRoot: string;\n apis: readonly NamedApiRoute[];\n }) => Promise<unknown>;\n};\n\nexport type RouteTypesPublisherPorts = Readonly<{\n resolveModule: (specifier: string, parent: URL) => URL;\n importModule: (url: URL) => Promise<WebBuildModule>;\n}>;\n\nconst defaultPorts: RouteTypesPublisherPorts = {\n resolveModule: moduleResolve,\n importModule: async (url) => (await import(url.href)) as WebBuildModule,\n};\n\nfunction isAbsentOptionalWebPackage(thrown: unknown): boolean {\n return (\n typeof thrown === \"object\" &&\n thrown !== null &&\n \"code\" in thrown &&\n (thrown as { code?: unknown }).code === \"ERR_MODULE_NOT_FOUND\" &&\n thrown instanceof Error &&\n thrown.message.includes(`Cannot find package '${WEB_PACKAGE}'`)\n );\n}\n\n/**\n * Publish dev route declarations through the consuming app's optional Web\n * build entry. A Core-only app performs no work; an installed but incompatible\n * Web package fails loudly instead of leaving stale generated declarations.\n */\nexport async function publishCurrentRouteTypes(\n appRoot: string,\n apis: readonly NamedApiRoute[],\n ports: RouteTypesPublisherPorts = defaultPorts,\n): Promise<boolean> {\n const projectUrl = pathToFileURL(resolve(appRoot, \"package.json\"));\n\n try {\n ports.resolveModule(WEB_PACKAGE, projectUrl);\n } catch (error) {\n if (isAbsentOptionalWebPackage(error)) {\n await clearOwnedRouteTypes(appRoot);\n return false;\n }\n throw error;\n }\n\n const buildModuleUrl = ports.resolveModule(WEB_BUILD_PACKAGE, projectUrl);\n const buildModule = await ports.importModule(buildModuleUrl);\n\n if (typeof buildModule.writeCurrentRouteTypes !== \"function\") {\n throw new Error(\n `${WEB_BUILD_PACKAGE} does not export writeCurrentRouteTypes. Upgrade @warlock.js/web to a compatible version.`,\n );\n }\n\n await buildModule.writeCurrentRouteTypes({ appRoot, apis });\n return true;\n}\n\n/** Remove only this integration's header-proven generated declaration file. */\nexport async function clearOwnedRouteTypes(appRoot: string): Promise<boolean> {\n const target = resolve(appRoot, GENERATED_ROUTE_TYPES);\n\n let content: string;\n try {\n content = await readFile(target, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n throw error;\n }\n\n if (!content.startsWith(GENERATED_ROUTE_TYPES_HEADER)) return false;\n await unlink(target);\n return true;\n}\n"],"mappings":";;;;;;AAMA,MAAM,cAAc;AACpB,MAAM,oBAAoB;AAC1B,MAAM,wBAAwB;AAC9B,MAAM,+BAA+B;AAcrC,MAAM,eAAyC;CAC7C,eAAe;CACf,cAAc,OAAO,QAAS,MAAM,OAAO,IAAI;AACjD;AAEA,SAAS,2BAA2B,QAA0B;CAC5D,OACE,OAAO,WAAW,YAClB,WAAW,QACX,UAAU,UACT,OAA8B,SAAS,0BACxC,kBAAkB,SAClB,OAAO,QAAQ,SAAS,wBAAwB,YAAY,EAAE;AAElE;;;;;;AAOA,eAAsB,yBACpB,SACA,MACA,QAAkC,cAChB;CAClB,MAAM,aAAa,cAAc,QAAQ,SAAS,cAAc,CAAC;CAEjE,IAAI;EACF,MAAM,cAAc,aAAa,UAAU;CAC7C,SAAS,OAAO;EACd,IAAI,2BAA2B,KAAK,GAAG;GACrC,MAAM,qBAAqB,OAAO;GAClC,OAAO;EACT;EACA,MAAM;CACR;CAEA,MAAM,iBAAiB,MAAM,cAAc,mBAAmB,UAAU;CACxE,MAAM,cAAc,MAAM,MAAM,aAAa,cAAc;CAE3D,IAAI,OAAO,YAAY,2BAA2B,YAChD,MAAM,IAAI,MACR,GAAG,kBAAkB,0FACvB;CAGF,MAAM,YAAY,uBAAuB;EAAE;EAAS;CAAK,CAAC;CAC1D,OAAO;AACT;;AAGA,eAAsB,qBAAqB,SAAmC;CAC5E,MAAM,SAAS,QAAQ,SAAS,qBAAqB;CAErD,IAAI;CACJ,IAAI;EACF,UAAU,MAAM,SAAS,QAAQ,MAAM;CACzC,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAAU,OAAO;EAC/D,MAAM;CACR;CAEA,IAAI,CAAC,QAAQ,WAAW,4BAA4B,GAAG,OAAO;CAC9D,MAAM,OAAO,MAAM;CACnB,OAAO;AACT"}
@@ -0,0 +1,37 @@
1
+ import { RateLimitPluginOptions } from "@fastify/rate-limit";
2
+
3
+ //#region ../core/src/http/build-rate-limit-options.d.ts
4
+ /**
5
+ * `@fastify/rate-limit` options that `http.rateLimit` passes straight through.
6
+ */
7
+ declare const passthroughKeys: readonly ["redis", "nameSpace", "keyGenerator", "skipOnError", "allowList", "continueExceeding", "enableDraftSpec", "global", "hook", "cache", "errorResponseBuilder", "addHeaders", "addHeadersOnExceeding"];
8
+ /**
9
+ * Shape of the `http.rateLimit` config.
10
+ */
11
+ type HttpRateLimitConfig = Pick<RateLimitPluginOptions, (typeof passthroughKeys)[number]> & {
12
+ /**
13
+ * Set `false` to skip registering the global rate limiter.
14
+ *
15
+ * @default true
16
+ */
17
+ enabled?: boolean;
18
+ /**
19
+ * Max requests per time window.
20
+ *
21
+ * @default 60
22
+ */
23
+ max?: number;
24
+ /**
25
+ * Time window in milliseconds. Alias of `timeWindow`.
26
+ *
27
+ * @default 60 * 1000
28
+ */
29
+ duration?: number;
30
+ /**
31
+ * Time window; takes precedence over `duration` when both are set.
32
+ */
33
+ timeWindow?: RateLimitPluginOptions["timeWindow"];
34
+ };
35
+ //#endregion
36
+ export { HttpRateLimitConfig };
37
+ //# sourceMappingURL=build-rate-limit-options.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-rate-limit-options.d.mts","names":[],"sources":["../../../../../../../core/src/http/build-rate-limit-options.ts"],"mappings":";;;;;AAAkE;cAK5D,eAAA;;;AAcI;KAKE,mBAAA,GAAsB,IAAA,CAAK,sBAAA,UAAgC,eAAA;EAAxC;;;;;EAM7B,OAAA;EAgBmC;;;;;EAVnC,GAAA;EAAA;;;;;EAMA,QAAA;;;;EAIA,UAAA,GAAa,sBAAA;AAAA"}
@@ -0,0 +1,37 @@
1
+ //#region ../core/src/http/build-rate-limit-options.ts
2
+ /**
3
+ * `@fastify/rate-limit` options that `http.rateLimit` passes straight through.
4
+ */
5
+ const passthroughKeys = [
6
+ "redis",
7
+ "nameSpace",
8
+ "keyGenerator",
9
+ "skipOnError",
10
+ "allowList",
11
+ "continueExceeding",
12
+ "enableDraftSpec",
13
+ "global",
14
+ "hook",
15
+ "cache",
16
+ "errorResponseBuilder",
17
+ "addHeaders",
18
+ "addHeadersOnExceeding"
19
+ ];
20
+ /**
21
+ * Build the `@fastify/rate-limit` plugin options from `http.rateLimit`.
22
+ * Returns `null` when the limiter is disabled.
23
+ */
24
+ function buildRateLimitOptions(config) {
25
+ const source = config ?? {};
26
+ if (source.enabled === false) return null;
27
+ const options = {
28
+ max: source.max ?? 60,
29
+ timeWindow: source.timeWindow ?? source.duration ?? 60 * 1e3
30
+ };
31
+ for (const key of passthroughKeys) if (source[key] !== void 0) options[key] = source[key];
32
+ return options;
33
+ }
34
+
35
+ //#endregion
36
+ export { buildRateLimitOptions };
37
+ //# sourceMappingURL=build-rate-limit-options.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-rate-limit-options.mjs","names":[],"sources":["../../../../../../../core/src/http/build-rate-limit-options.ts"],"sourcesContent":["import type { RateLimitPluginOptions } from \"@fastify/rate-limit\";\n\n/**\n * `@fastify/rate-limit` options that `http.rateLimit` passes straight through.\n */\nconst passthroughKeys = [\n \"redis\",\n \"nameSpace\",\n \"keyGenerator\",\n \"skipOnError\",\n \"allowList\",\n \"continueExceeding\",\n \"enableDraftSpec\",\n \"global\",\n \"hook\",\n \"cache\",\n \"errorResponseBuilder\",\n \"addHeaders\",\n \"addHeadersOnExceeding\",\n] as const;\n\n/**\n * Shape of the `http.rateLimit` config.\n */\nexport type HttpRateLimitConfig = Pick<RateLimitPluginOptions, (typeof passthroughKeys)[number]> & {\n /**\n * Set `false` to skip registering the global rate limiter.\n *\n * @default true\n */\n enabled?: boolean;\n /**\n * Max requests per time window.\n *\n * @default 60\n */\n max?: number;\n /**\n * Time window in milliseconds. Alias of `timeWindow`.\n *\n * @default 60 * 1000\n */\n duration?: number;\n /**\n * Time window; takes precedence over `duration` when both are set.\n */\n timeWindow?: RateLimitPluginOptions[\"timeWindow\"];\n};\n\n/**\n * Build the `@fastify/rate-limit` plugin options from `http.rateLimit`.\n * Returns `null` when the limiter is disabled.\n */\nexport function buildRateLimitOptions(\n config: HttpRateLimitConfig | undefined,\n): RateLimitPluginOptions | null {\n const source = config ?? {};\n\n if (source.enabled === false) {\n return null;\n }\n\n const options: Record<string, unknown> = {\n max: source.max ?? 60,\n timeWindow: source.timeWindow ?? source.duration ?? 60 * 1000,\n };\n\n for (const key of passthroughKeys) {\n if (source[key] !== undefined) {\n options[key] = source[key];\n }\n }\n\n return options as RateLimitPluginOptions;\n}\n"],"mappings":";;;;AAKA,MAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAkCA,SAAgB,sBACd,QAC+B;CAC/B,MAAM,SAAS,UAAU,CAAC;CAE1B,IAAI,OAAO,YAAY,OACrB,OAAO;CAGT,MAAM,UAAmC;EACvC,KAAK,OAAO,OAAO;EACnB,YAAY,OAAO,cAAc,OAAO,YAAY,KAAK;CAC3D;CAEA,KAAK,MAAM,OAAO,iBAChB,IAAI,OAAO,SAAS,QAClB,QAAQ,OAAO,OAAO;CAI1B,OAAO;AACT"}
@@ -8,6 +8,11 @@ type IdempotencyOptions = {
8
8
  * Cache TTL in seconds. Falls back to `http.idempotency.ttl`, then `86400` (24h).
9
9
  */
10
10
  ttl?: number;
11
+ /**
12
+ * TTL in seconds of the in-flight reservation held while the handler runs.
13
+ * Falls back to `http.idempotency.reservationTtl`, then `60`.
14
+ */
15
+ reservationTtl?: number;
11
16
  /**
12
17
  * Header name carrying the client's key. Falls back to
13
18
  * `http.idempotency.headerName`, then `"Idempotency-Key"`.
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/idempotency.middleware.ts"],"mappings":";;;;;KAYY,kBAAA;EAAkB;;;EAI5B,GAAA;EAKA;;;;EAAA,UAAA;EAqDc;;;;;EA/Cd,OAAA;EA+CuE;;AAAU;;EA1CjF,MAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0Cc,qBAAA,CAAsB,OAAA,GAAS,kBAAA,GAA0B,UAAU"}
1
+ {"version":3,"file":"idempotency.middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/idempotency.middleware.ts"],"mappings":";;;;;KAYY,kBAAA;EAAkB;;;EAI5B,GAAA;EAKA;;;;EAAA,cAAA;EAgBM;AA+CR;;;EA1DE,UAAA;EA0D6C;;;;AAAoC;EApDjF,OAAA;;;;;EAKA,MAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+Cc,qBAAA,CAAsB,OAAA,GAAS,kBAAA,GAA0B,UAAU"}