stratal 0.0.24 → 0.0.26

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 (137) hide show
  1. package/dist/bin/quarry.mjs +60 -4
  2. package/dist/bin/quarry.mjs.map +1 -1
  3. package/dist/cache/index.d.mts +1 -1
  4. package/dist/cache/index.mjs +2 -2
  5. package/dist/{command-CPhFHjG3.d.mts → command-DoBD2Cwl.d.mts} +2 -2
  6. package/dist/{command-CPhFHjG3.d.mts.map → command-DoBD2Cwl.d.mts.map} +1 -1
  7. package/dist/config/index.d.mts +1 -1
  8. package/dist/config/index.mjs +2 -2
  9. package/dist/{controller.decorator-C5UVeJS3.mjs → controller.decorator-YSTPQntu.mjs} +3 -3
  10. package/dist/{controller.decorator-C5UVeJS3.mjs.map → controller.decorator-YSTPQntu.mjs.map} +1 -1
  11. package/dist/cron/index.d.mts +1 -1
  12. package/dist/cron/index.mjs +1 -1
  13. package/dist/{cron.module-Bgzq5hiT.mjs → cron.module-C81HTzR7.mjs} +3 -3
  14. package/dist/{cron.module-Bgzq5hiT.mjs.map → cron.module-C81HTzR7.mjs.map} +1 -1
  15. package/dist/di/index.d.mts +2 -2
  16. package/dist/di/index.mjs +2 -2
  17. package/dist/{di-DseMn-z9.mjs → di-D7qmrAir.mjs} +60 -3
  18. package/dist/di-D7qmrAir.mjs.map +1 -0
  19. package/dist/email/index.d.mts +2 -2
  20. package/dist/email/index.mjs +3 -3
  21. package/dist/errors/index.d.mts +2 -2
  22. package/dist/errors/index.mjs +3 -3
  23. package/dist/{errors-mXYxG0XB.mjs → errors-C01O2T-n.mjs} +19 -4
  24. package/dist/errors-C01O2T-n.mjs.map +1 -0
  25. package/dist/events/index.d.mts +8 -0
  26. package/dist/events/index.d.mts.map +1 -1
  27. package/dist/events/index.mjs +1 -1
  28. package/dist/{events-BXJGZjpG.mjs → events-BhEQuT1X.mjs} +5 -2
  29. package/dist/events-BhEQuT1X.mjs.map +1 -0
  30. package/dist/{exception-context-kEoMFwze.mjs → exception-context-D-kvney-.mjs} +2 -2
  31. package/dist/{exception-context-kEoMFwze.mjs.map → exception-context-D-kvney-.mjs.map} +1 -1
  32. package/dist/{gateway-context-TMu_AlJt.mjs → gateway-context-m7kEzRa2.mjs} +4 -4
  33. package/dist/{gateway-context-TMu_AlJt.mjs.map → gateway-context-m7kEzRa2.mjs.map} +1 -1
  34. package/dist/guards/index.d.mts +1 -1
  35. package/dist/{hono-app-CvV3hOfT.mjs → hono-app-COAgmutc.mjs} +18 -11
  36. package/dist/hono-app-COAgmutc.mjs.map +1 -0
  37. package/dist/{http-method.decorator-ByWZb9DO.mjs → http-method.decorator-BljM8BDj.mjs} +2 -2
  38. package/dist/{http-method.decorator-ByWZb9DO.mjs.map → http-method.decorator-BljM8BDj.mjs.map} +1 -1
  39. package/dist/i18n/index.d.mts +10 -3
  40. package/dist/i18n/index.d.mts.map +1 -1
  41. package/dist/i18n/index.mjs +3 -3
  42. package/dist/{i18n.module-DRQAZoSZ.mjs → i18n.module-B2DvWUPa.mjs} +24 -9
  43. package/dist/i18n.module-B2DvWUPa.mjs.map +1 -0
  44. package/dist/{index-B5JBRcWD.d.mts → index-CNuFQSNj.d.mts} +6 -4
  45. package/dist/{index-B5JBRcWD.d.mts.map → index-CNuFQSNj.d.mts.map} +1 -1
  46. package/dist/{index-B_JoEl3V.d.mts → index-uybm0bhQ.d.mts} +111 -6
  47. package/dist/index-uybm0bhQ.d.mts.map +1 -0
  48. package/dist/index.d.mts +3 -3
  49. package/dist/index.mjs +2 -2
  50. package/dist/{lazy-module-loader-Ib383jH_.d.mts → lazy-module-loader-M6YKudNL.d.mts} +2 -2
  51. package/dist/{lazy-module-loader-Ib383jH_.d.mts.map → lazy-module-loader-M6YKudNL.d.mts.map} +1 -1
  52. package/dist/{locale-path.service-D-dHiIPc.mjs → locale-path.service-CH0CaxwH.mjs} +3 -3
  53. package/dist/{locale-path.service-D-dHiIPc.mjs.map → locale-path.service-CH0CaxwH.mjs.map} +1 -1
  54. package/dist/{locale-url.service-C2EWmGdq.mjs → locale-url.service-6bgia24_.mjs} +2 -2
  55. package/dist/{locale-url.service-C2EWmGdq.mjs.map → locale-url.service-6bgia24_.mjs.map} +1 -1
  56. package/dist/logger/index.mjs +1 -1
  57. package/dist/module/index.d.mts +2 -2
  58. package/dist/module/index.mjs +2 -2
  59. package/dist/{module-registry-Dm-pqHd3.mjs → module-registry-NxX5O0Qk.mjs} +4 -4
  60. package/dist/{module-registry-Dm-pqHd3.mjs.map → module-registry-NxX5O0Qk.mjs.map} +1 -1
  61. package/dist/openapi/index.d.mts +2 -2
  62. package/dist/openapi/index.mjs +1 -1
  63. package/dist/{openapi-CstuTM8S.mjs → openapi-CMwuCp31.mjs} +3 -3
  64. package/dist/{openapi-CstuTM8S.mjs.map → openapi-CMwuCp31.mjs.map} +1 -1
  65. package/dist/{openapi.service-YhTiJ1bO.d.mts → openapi.service-2rvJBCEg.d.mts} +2 -2
  66. package/dist/{openapi.service-YhTiJ1bO.d.mts.map → openapi.service-2rvJBCEg.d.mts.map} +1 -1
  67. package/dist/quarry/index.d.mts +3 -3
  68. package/dist/quarry/index.mjs +1 -1
  69. package/dist/quarry/runner.d.mts +6 -6
  70. package/dist/quarry/runner.mjs +5 -5
  71. package/dist/{quarry-registry-CXg0RFXq.d.mts → quarry-registry-DRnV-DDa.d.mts} +3 -3
  72. package/dist/{quarry-registry-CXg0RFXq.d.mts.map → quarry-registry-DRnV-DDa.d.mts.map} +1 -1
  73. package/dist/{quarry.module-BuRPGMDm.mjs → quarry.module-CcGxU2dJ.mjs} +3 -3
  74. package/dist/{quarry.module-BuRPGMDm.mjs.map → quarry.module-CcGxU2dJ.mjs.map} +1 -1
  75. package/dist/queue/index.d.mts +1 -1
  76. package/dist/queue/index.mjs +2 -2
  77. package/dist/{queue.module-nddvxzCB.mjs → queue.module-CEs4_kEM.mjs} +15 -8
  78. package/dist/{queue.module-nddvxzCB.mjs.map → queue.module-CEs4_kEM.mjs.map} +1 -1
  79. package/dist/{r2-storage.provider-DCxQt9dD.mjs → r2-storage.provider-BoZmR6Ut.mjs} +2 -2
  80. package/dist/{r2-storage.provider-DCxQt9dD.mjs.map → r2-storage.provider-BoZmR6Ut.mjs.map} +1 -1
  81. package/dist/{rate-limit.decorator-BPAie_p3.mjs → rate-limit.decorator-Djs4oYDB.mjs} +2 -2
  82. package/dist/{rate-limit.decorator-BPAie_p3.mjs.map → rate-limit.decorator-Djs4oYDB.mjs.map} +1 -1
  83. package/dist/rate-limiter/index.d.mts +54 -47
  84. package/dist/rate-limiter/index.d.mts.map +1 -1
  85. package/dist/rate-limiter/index.mjs +16 -7
  86. package/dist/rate-limiter/index.mjs.map +1 -1
  87. package/dist/{route-registration.service-D6vSwiKP.mjs → route-registration.service-CDPQKpm4.mjs} +9 -9
  88. package/dist/{route-registration.service-D6vSwiKP.mjs.map → route-registration.service-CDPQKpm4.mjs.map} +1 -1
  89. package/dist/{route-registry-CYqLp2Nj.mjs → route-registry-BvLJisvK.mjs} +3 -3
  90. package/dist/{route-registry-CYqLp2Nj.mjs.map → route-registry-BvLJisvK.mjs.map} +1 -1
  91. package/dist/router/index.d.mts +2 -2
  92. package/dist/router/index.mjs +16 -16
  93. package/dist/{router-CWGBD-Bg.mjs → router-DwyqEXgf.mjs} +13 -13
  94. package/dist/{router-CWGBD-Bg.mjs.map → router-DwyqEXgf.mjs.map} +1 -1
  95. package/dist/{router-resolver-D4YlPNlm.mjs → router-resolver-sUV_jTrU.mjs} +2 -2
  96. package/dist/{router-resolver-D4YlPNlm.mjs.map → router-resolver-sUV_jTrU.mjs.map} +1 -1
  97. package/dist/seeder/index.d.mts +2 -2
  98. package/dist/seeder/index.mjs +3 -3
  99. package/dist/{seeder-7ubkms-Y.mjs → seeder-BPGY5rUb.mjs} +4 -4
  100. package/dist/{seeder-7ubkms-Y.mjs.map → seeder-BPGY5rUb.mjs.map} +1 -1
  101. package/dist/{seeder-registry-CyUmKsJq.mjs → seeder-registry-DEvCycsT.mjs} +3 -3
  102. package/dist/{seeder-registry-CyUmKsJq.mjs.map → seeder-registry-DEvCycsT.mjs.map} +1 -1
  103. package/dist/{seeder.module-CYYwk3Qk.mjs → seeder.module-CIwQbdN4.mjs} +2 -2
  104. package/dist/{seeder.module-CYYwk3Qk.mjs.map → seeder.module-CIwQbdN4.mjs.map} +1 -1
  105. package/dist/storage/index.d.mts +1 -1
  106. package/dist/storage/index.mjs +2 -2
  107. package/dist/storage/providers/index.mjs +1 -1
  108. package/dist/{storage-MDZypIE9.mjs → storage-C30X81CS.mjs} +7 -7
  109. package/dist/{storage-MDZypIE9.mjs.map → storage-C30X81CS.mjs.map} +1 -1
  110. package/dist/{storage.error-Dnib4VHc.mjs → storage.error-BStXPmO4.mjs} +2 -2
  111. package/dist/{storage.error-Dnib4VHc.mjs.map → storage.error-BStXPmO4.mjs.map} +1 -1
  112. package/dist/{stratal-DL9M38_s.mjs → stratal-BL6FKUM_.mjs} +85 -35
  113. package/dist/stratal-BL6FKUM_.mjs.map +1 -0
  114. package/dist/{stratal-DwDJPY9N.d.mts → stratal-D5j_I14G.d.mts} +13 -3
  115. package/dist/stratal-D5j_I14G.d.mts.map +1 -0
  116. package/dist/trailing-slash-2SctvePW.mjs +80 -0
  117. package/dist/trailing-slash-2SctvePW.mjs.map +1 -0
  118. package/dist/{uri-h7Q8Jug9.mjs → uri-iwofWJ_T.mjs} +12 -10
  119. package/dist/uri-iwofWJ_T.mjs.map +1 -0
  120. package/dist/{versioning.service-C6aHky8-.mjs → versioning.service-CCa2oYMJ.mjs} +3 -3
  121. package/dist/{versioning.service-C6aHky8-.mjs.map → versioning.service-CCa2oYMJ.mjs.map} +1 -1
  122. package/dist/websocket/index.d.mts +1 -1
  123. package/dist/websocket/index.mjs +1 -1
  124. package/dist/workers/index.d.mts +1 -1
  125. package/dist/workers/index.mjs +2 -2
  126. package/package.json +1 -1
  127. package/dist/di-DseMn-z9.mjs.map +0 -1
  128. package/dist/errors-mXYxG0XB.mjs.map +0 -1
  129. package/dist/events-BXJGZjpG.mjs.map +0 -1
  130. package/dist/hono-app-CvV3hOfT.mjs.map +0 -1
  131. package/dist/i18n.module-DRQAZoSZ.mjs.map +0 -1
  132. package/dist/index-B_JoEl3V.d.mts.map +0 -1
  133. package/dist/stratal-DL9M38_s.mjs.map +0 -1
  134. package/dist/stratal-DwDJPY9N.d.mts.map +0 -1
  135. package/dist/trailing-slash-CFyw8nYu.mjs +0 -34
  136. package/dist/trailing-slash-CFyw8nYu.mjs.map +0 -1
  137. package/dist/uri-h7Q8Jug9.mjs.map +0 -1
@@ -1,11 +1,11 @@
1
- import { r as DI_TOKENS, t as Container, v as ROUTER_TOKENS } from "./di-DseMn-z9.mjs";
1
+ import { b as ROUTER_TOKENS, r as DI_TOKENS, t as Container } from "./di-D7qmrAir.mjs";
2
2
  import { n as getContainer, r as runWithContainer, t as containerStorage } from "./container-storage-BmOJ4_Na.mjs";
3
3
  import { JsonFormatter, LOGGER_TOKENS, LoggerService, PrettyFormatter } from "./logger/index.mjs";
4
- import { a as DefaultExceptionHandler, r as StratalNotInitializedError } from "./errors-mXYxG0XB.mjs";
5
- import { i as createQueueExceptionContext, n as createCronExceptionContext, t as createCliExceptionContext } from "./exception-context-kEoMFwze.mjs";
6
- import { a as getListenerHandlers } from "./events-BXJGZjpG.mjs";
7
- import { d as LazyModuleLoader, t as ModuleRegistry } from "./module-registry-Dm-pqHd3.mjs";
8
- import { t as SEEDER_TOKENS } from "./seeder-registry-CyUmKsJq.mjs";
4
+ import { i as StratalNotInitializedError, o as DefaultExceptionHandler, r as StratalSupersededError } from "./errors-C01O2T-n.mjs";
5
+ import { i as createQueueExceptionContext, n as createCronExceptionContext, t as createCliExceptionContext } from "./exception-context-D-kvney-.mjs";
6
+ import { a as getListenerHandlers } from "./events-BhEQuT1X.mjs";
7
+ import { d as LazyModuleLoader, t as ModuleRegistry } from "./module-registry-NxX5O0Qk.mjs";
8
+ import { t as SEEDER_TOKENS } from "./seeder-registry-DEvCycsT.mjs";
9
9
  //#region src/application.ts
10
10
  var Application = class {
11
11
  _container;
@@ -48,10 +48,17 @@ var Application = class {
48
48
  }
49
49
  async initialize() {
50
50
  if (this.initialized) return;
51
- await runWithContainer(this._container, () => this.initializeInternal());
51
+ try {
52
+ await runWithContainer(this._container, () => this.initializeInternal());
53
+ } catch (error) {
54
+ await this.teardown().catch((teardownError) => {
55
+ console.error("[stratal] Teardown after failed initialization also failed:", teardownError);
56
+ });
57
+ throw error;
58
+ }
52
59
  }
53
60
  async initializeInternal() {
54
- const [{ QuarryModule }, { SeederModule }] = await Promise.all([import("./quarry.module-BuRPGMDm.mjs").then((n) => n.n), import("./seeder.module-CYYwk3Qk.mjs").then((n) => n.n)]);
61
+ const [{ QuarryModule }, { SeederModule }] = await Promise.all([import("./quarry.module-CcGxU2dJ.mjs").then((n) => n.n), import("./seeder.module-CIwQbdN4.mjs").then((n) => n.n)]);
55
62
  this.moduleRegistry.registerAll([QuarryModule, SeederModule]);
56
63
  this.moduleRegistry.register(this.appConfig.module);
57
64
  await this.moduleRegistry.initialize();
@@ -64,14 +71,14 @@ var Application = class {
64
71
  }
65
72
  async registerRoutingServices() {
66
73
  const [{ HonoApp }, { RouteRegistry }, { RouterResolver }, { VersioningService }, { LocalePathService }, { LocaleUrlService }, { RouteRegistrationService }, { Uri }] = await Promise.all([
67
- import("./hono-app-CvV3hOfT.mjs").then((n) => n.n),
68
- import("./route-registry-CYqLp2Nj.mjs").then((n) => n.n),
69
- import("./router-resolver-D4YlPNlm.mjs"),
70
- import("./versioning.service-C6aHky8-.mjs").then((n) => n.n),
71
- import("./locale-path.service-D-dHiIPc.mjs").then((n) => n.n),
72
- import("./locale-url.service-C2EWmGdq.mjs").then((n) => n.n),
73
- import("./route-registration.service-D6vSwiKP.mjs").then((n) => n.n),
74
- import("./uri-h7Q8Jug9.mjs").then((n) => n.r)
74
+ import("./hono-app-COAgmutc.mjs").then((n) => n.n),
75
+ import("./route-registry-BvLJisvK.mjs").then((n) => n.n),
76
+ import("./router-resolver-sUV_jTrU.mjs"),
77
+ import("./versioning.service-CCa2oYMJ.mjs").then((n) => n.n),
78
+ import("./locale-path.service-CH0CaxwH.mjs").then((n) => n.n),
79
+ import("./locale-url.service-6bgia24_.mjs").then((n) => n.n),
80
+ import("./route-registration.service-CDPQKpm4.mjs").then((n) => n.n),
81
+ import("./uri-iwofWJ_T.mjs").then((n) => n.r)
75
82
  ]);
76
83
  this._container.register(ROUTER_TOKENS.VersioningService, VersioningService);
77
84
  this._container.register(ROUTER_TOKENS.HonoApp, HonoApp);
@@ -92,7 +99,7 @@ var Application = class {
92
99
  */
93
100
  initializeEventListeners() {
94
101
  this.eventsInitPromise ??= runWithContainer(this._container, async () => {
95
- const { EventsModule } = await import("./events-BXJGZjpG.mjs").then((n) => n.n);
102
+ const { EventsModule } = await import("./events-BhEQuT1X.mjs").then((n) => n.n);
96
103
  await this.moduleRegistry.registerLazy(EventsModule);
97
104
  this.registerEventListeners();
98
105
  });
@@ -122,7 +129,7 @@ var Application = class {
122
129
  initializeQueue() {
123
130
  this.queueInitPromise ??= runWithContainer(this._container, async () => {
124
131
  await this.ensureI18n();
125
- const { QueueModule } = await import("./queue.module-nddvxzCB.mjs").then((n) => n.n);
132
+ const { QueueModule } = await import("./queue.module-CEs4_kEM.mjs").then((n) => n.n);
126
133
  this.moduleRegistry.register(QueueModule);
127
134
  this.consumerRegistry = this._container.resolve(DI_TOKENS.ConsumerRegistry);
128
135
  await this.registerQueueConsumers();
@@ -137,7 +144,7 @@ var Application = class {
137
144
  */
138
145
  ensureI18n() {
139
146
  this.i18nInitPromise ??= runWithContainer(this._container, async () => {
140
- const { I18nModule } = await import("./i18n.module-DRQAZoSZ.mjs").then((n) => n.n);
147
+ const { I18nModule } = await import("./i18n.module-B2DvWUPa.mjs").then((n) => n.n);
141
148
  await this.moduleRegistry.registerLazy(I18nModule);
142
149
  });
143
150
  return this.i18nInitPromise;
@@ -148,7 +155,7 @@ var Application = class {
148
155
  */
149
156
  ensureCron() {
150
157
  this.cronInitPromise ??= runWithContainer(this._container, async () => {
151
- const { CronModule } = await import("./cron.module-Bgzq5hiT.mjs").then((n) => n.n);
158
+ const { CronModule } = await import("./cron.module-C81HTzR7.mjs").then((n) => n.n);
152
159
  this.moduleRegistry.register(CronModule);
153
160
  this.cronManager = this._container.resolve(DI_TOKENS.Cron);
154
161
  this.registerCronJobs(this.cronManager);
@@ -196,15 +203,20 @@ var Application = class {
196
203
  return {
197
204
  getLocale: () => locale,
198
205
  setLocale: () => {},
206
+ header: () => void 0,
199
207
  getContainer: () => containerStorage.getStore() ?? this._container
200
208
  };
201
209
  }
202
210
  async shutdown() {
203
211
  if (!this.initialized) return;
204
212
  this.initialized = false;
213
+ await this.teardown();
214
+ }
215
+ /** Releases everything boot acquired: module resources, then the container. */
216
+ async teardown() {
205
217
  await this.moduleRegistry.shutdown();
206
218
  this._container.resolve(LOGGER_TOKENS.LoggerService).info("Disposing container...");
207
- this._container.dispose();
219
+ await this._container.dispose();
208
220
  }
209
221
  async handleCommand(name, input) {
210
222
  await this.initializeRouting();
@@ -315,16 +327,31 @@ var Stratal = class Stratal {
315
327
  initPromise;
316
328
  static _application = null;
317
329
  static _generation = 0;
318
- static _previousInstance = null;
330
+ static _current = null;
331
+ /**
332
+ * Serializes generation teardown: each new instance appends the previous
333
+ * instance's shutdown here, and `prepareApp` awaits the chain before
334
+ * allocating the new Application. Old and new DI graphs never coexist,
335
+ * so repeated Vite HMR reloads can't accumulate live object graphs.
336
+ */
337
+ static _teardownChain = Promise.resolve();
319
338
  constructor(config) {
320
339
  this.fetch = this.fetch.bind(this);
321
340
  this.queue = this.queue.bind(this);
322
341
  this.scheduled = this.scheduled.bind(this);
323
342
  const generation = ++Stratal._generation;
324
- if (Stratal._previousInstance) Stratal._previousInstance.shutdown();
325
- Stratal._previousInstance = this;
326
- this.initPromise = this.prepareApp(config, generation);
343
+ const previous = Stratal._current;
344
+ Stratal._current = this;
345
+ if (previous) Stratal._teardownChain = Stratal._teardownChain.then(() => previous.shutdown()).catch((error) => {
346
+ console.error("[stratal] Failed to shut down superseded instance:", error);
347
+ });
348
+ const teardownBarrier = Stratal._teardownChain;
349
+ this.initPromise = this.prepareApp(config, generation, teardownBarrier);
327
350
  Stratal._application = this.initPromise;
351
+ this.initPromise.catch((error) => {
352
+ if (error instanceof StratalSupersededError) return;
353
+ console.error("[stratal] Initialization failed:", error);
354
+ });
328
355
  }
329
356
  async fetch(request, env, ctx) {
330
357
  return (await (await this.ensureReady()).ensureHono()).fetch(request, env, ctx);
@@ -336,7 +363,7 @@ var Stratal = class Stratal {
336
363
  return (await this.ensureReady()).handleScheduled(controller);
337
364
  }
338
365
  get hono() {
339
- return this.initPromise.then((app) => app.ensureHono());
366
+ return this.ensureReady().then((app) => app.ensureHono());
340
367
  }
341
368
  async shutdown() {
342
369
  try {
@@ -346,24 +373,47 @@ var Stratal = class Stratal {
346
373
  await this.app.shutdown();
347
374
  this.app = null;
348
375
  }
376
+ if (Stratal._current === this) {
377
+ Stratal._current = null;
378
+ Stratal._application = null;
379
+ }
349
380
  }
350
381
  /**
351
382
  * @internal
352
383
  * Resolves the Application instance from the static singleton.
353
384
  * Used by worker base classes (DurableObject, Workflow, WorkerEntrypoint)
354
385
  * to access the DI container without going through Cloudflare RPC.
386
+ * Generations superseded mid-boot reject with {@link StratalSupersededError};
387
+ * this retries against the replacing generation so callers always land on
388
+ * the live Application.
355
389
  */
356
- static resolveApplication() {
357
- if (!Stratal._application) throw new StratalNotInitializedError();
358
- return Stratal._application;
390
+ static async resolveApplication() {
391
+ let current = Stratal._application;
392
+ while (current) try {
393
+ return await current;
394
+ } catch (error) {
395
+ if (error instanceof StratalSupersededError && Stratal._application !== current) {
396
+ current = Stratal._application;
397
+ continue;
398
+ }
399
+ throw error;
400
+ }
401
+ throw new StratalNotInitializedError();
359
402
  }
360
403
  async ensureReady() {
361
- this.app ??= await this.initPromise;
362
- return this.app;
404
+ if (this.app) return this.app;
405
+ try {
406
+ this.app = await this.initPromise;
407
+ return this.app;
408
+ } catch (error) {
409
+ if (error instanceof StratalSupersededError) return Stratal.resolveApplication();
410
+ throw error;
411
+ }
363
412
  }
364
- async prepareApp(config, generation) {
413
+ async prepareApp(config, generation, teardownBarrier) {
365
414
  const { env, waitUntil } = await import("cloudflare:workers");
366
- if (generation !== Stratal._generation) return new Promise(() => {});
415
+ await teardownBarrier;
416
+ if (generation !== Stratal._generation) throw new StratalSupersededError(generation);
367
417
  const app = new Application({
368
418
  ...config,
369
419
  env,
@@ -372,7 +422,7 @@ var Stratal = class Stratal {
372
422
  await app.initialize();
373
423
  if (generation !== Stratal._generation) {
374
424
  await app.shutdown();
375
- return new Promise(() => {});
425
+ throw new StratalSupersededError(generation);
376
426
  }
377
427
  return app;
378
428
  }
@@ -380,4 +430,4 @@ var Stratal = class Stratal {
380
430
  //#endregion
381
431
  export { Application as n, Stratal as t };
382
432
 
383
- //# sourceMappingURL=stratal-DL9M38_s.mjs.map
433
+ //# sourceMappingURL=stratal-BL6FKUM_.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stratal-BL6FKUM_.mjs","names":[],"sources":["../src/application.ts","../src/stratal.ts"],"sourcesContent":["import type { CronJob } from './cron/cron-job'\nimport type { CronManager } from './cron/cron-manager'\nimport { Container } from './di/container'\nimport { containerStorage, getContainer, runWithContainer } from './di/container-storage'\nimport { DI_TOKENS } from './di/tokens'\nimport { type StratalEnv } from './env'\nimport { DefaultExceptionHandler } from './errors/default-exception-handler'\nimport { createCliExceptionContext, createCronExceptionContext, createQueueExceptionContext } from './errors/exception-context'\nimport type { ExceptionHandler } from './errors/exception-handler'\nimport type { EventHandler, EventRegistry } from './events'\nimport { getListenerHandlers } from './events'\nimport type { StratalExecutionContext } from './execution-context'\nimport { JsonFormatter, LOGGER_TOKENS, LoggerService, LogLevel, PrettyFormatter } from './logger'\nimport { LazyModuleLoader } from './module/lazy-module-loader'\nimport { ModuleRegistry } from './module/module-registry'\nimport type { DynamicModule, ModuleClass } from './module/types'\nimport type { Command } from './quarry/command'\nimport type { QuarryRegistry } from './quarry/quarry-registry'\nimport type { CommandInput, CommandResult } from './quarry/types'\nimport type { ConsumerRegistry } from './queue/consumer-registry'\nimport type { IQueueConsumer, QueueMessage } from './queue/queue-consumer'\nimport type { QueueManager } from './queue/queue-manager'\nimport type { RouterContext } from './router'\nimport type { HonoApp } from './router/hono-app'\nimport { ROUTER_TOKENS } from './router/router.tokens'\nimport type { TrailingSlashConfig, VersioningOptions } from './router/types'\nimport type { Seeder } from './seeder/seeder'\nimport { SEEDER_TOKENS } from './seeder/seeder-registry'\nimport type { SeederRegistry } from './seeder/seeder-registry'\nimport type { Constructor } from './types'\n\nexport interface ApplicationConfig {\n module: ModuleClass | DynamicModule\n logging?: {\n level?: LogLevel\n formatter?: 'json' | 'pretty'\n }\n versioning?: VersioningOptions\n trailingSlash?: TrailingSlashConfig\n exceptionHandler?: Constructor<ExceptionHandler>\n}\n\nexport interface ApplicationOptions extends ApplicationConfig {\n env: StratalEnv\n ctx: StratalExecutionContext\n}\n\nexport class Application {\n private _container: Container\n private honoApp!: HonoApp\n private moduleRegistry: ModuleRegistry\n private consumerRegistry!: ConsumerRegistry\n private cronManager?: CronManager\n private quarry!: QuarryRegistry\n private initialized = false\n\n // Independently memoized lazy-init promises — each built-in subsystem is\n // loaded on demand at its trigger point to keep cold start lean.\n private routingInitPromise: Promise<void> | null = null\n private eventsInitPromise: Promise<void> | null = null\n private queueInitPromise: Promise<void> | null = null\n private i18nInitPromise: Promise<void> | null = null\n private cronInitPromise: Promise<void> | null = null\n\n readonly env: StratalEnv\n private readonly appConfig: ApplicationConfig\n\n constructor({ env, ctx, ...config }: ApplicationOptions) {\n this.env = env\n this.appConfig = config\n\n this._container = new Container()\n\n this._container.registerValue(DI_TOKENS.Application, this)\n this._container.registerValue(DI_TOKENS.CloudflareEnv, env)\n this._container.registerValue(DI_TOKENS.ExecutionContext, ctx)\n this._container.registerValue(ROUTER_TOKENS.RouterContext, null)\n\n this.registerLoggerService()\n this.registerCoreServices()\n\n const logger = this._container.resolve<LoggerService>(LOGGER_TOKENS.LoggerService)\n this.moduleRegistry = new ModuleRegistry(this._container, logger)\n\n this._container.registerValue(DI_TOKENS.ModuleRegistry, this.moduleRegistry)\n }\n\n get container(): Container {\n return this._container\n }\n\n async ensureHono(): Promise<HonoApp> {\n await this.initializeRouting()\n return this.honoApp\n }\n\n get config(): ApplicationConfig {\n return this.appConfig\n }\n\n async initialize(): Promise<void> {\n if (this.initialized) {\n return\n }\n\n try {\n await runWithContainer(this._container, () => this.initializeInternal())\n } catch (error) {\n // A failed boot can still hold live resources: every module whose\n // onInitialize already ran (DB pools, timers) and every container-cached\n // singleton. Tear them down here so callers never have to special-case\n // a failed initialize() — shutdown() stays a no-op for it.\n await this.teardown().catch((teardownError: unknown) => {\n console.error('[stratal] Teardown after failed initialization also failed:', teardownError)\n })\n throw error\n }\n }\n\n private async initializeInternal(): Promise<void> {\n // Eager subsystem modules — Quarry/Seeder are resolved synchronously\n // post-init (CLI runner, test harness), so they must be registered before\n // initialize() completes. Dynamic import keeps application.ts free of static\n // subsystem imports (uniform with the other built-ins). They load at boot\n // regardless, so this is for consistency, not cold-start deferral.\n const [{ QuarryModule }, { SeederModule }] = await Promise.all([\n import('./quarry/quarry.module'),\n import('./seeder/seeder.module'),\n ])\n this.moduleRegistry.registerAll([QuarryModule, SeederModule])\n\n // Register the user's root module (traverses imports). Other built-in\n // subsystems (i18n, queue, cache, openapi, cron, events, router) load on\n // demand at their trigger points.\n this.moduleRegistry.register(this.appConfig.module)\n\n // Initialize all modules (only those with lifecycle hooks)\n await this.moduleRegistry.initialize()\n\n // Initialize ExceptionHandler\n this.initializeExceptionHandler()\n\n // Register CLI commands + seeders (class refs only; no instantiation)\n this.registerCommands()\n this.registerSeeders()\n\n // Wire event listeners as part of initialization whenever the application\n // declares any, so emitted events reach their handlers regardless of which\n // entry point drives the app. Listener wiring otherwise happens lazily only\n // on the HTTP router (`initializeRouting`) and queue/scheduled/command\n // (`ensureScopedHandlers`) paths; any code that emits events without going\n // through one of those — a direct service or repository call, an RPC\n // entrypoint, or a Durable Object — would dispatch into a registry with no\n // handlers attached, silently dropping the event. Guarded on there being\n // listeners so applications with none still skip loading the events\n // subsystem; `initializeEventListeners` dedups, so any later lazy trigger\n // becomes a no-op.\n if (this.moduleRegistry.getAllListeners().length > 0) {\n await this.initializeEventListeners()\n }\n\n // Cron only loads when the app actually declares scheduled jobs\n if (this.moduleRegistry.getAllJobs().length > 0) {\n await this.ensureCron()\n }\n\n this.initialized = true\n }\n\n private async registerRoutingServices(): Promise<void> {\n const [\n { HonoApp },\n { RouteRegistry },\n { RouterResolver },\n { VersioningService },\n { LocalePathService },\n { LocaleUrlService },\n { RouteRegistrationService },\n { Uri },\n ] = await Promise.all([\n import('./router/hono-app'),\n import('./router/route-registry'),\n import('./router/router-resolver'),\n import('./router/services/versioning.service'),\n import('./router/services/locale-path.service'),\n import('./router/services/locale-url.service'),\n import('./router/services/route-registration.service'),\n import('./router/uri'),\n ])\n\n this._container.register(ROUTER_TOKENS.VersioningService, VersioningService)\n this._container.register(ROUTER_TOKENS.HonoApp, HonoApp)\n this._container.register(ROUTER_TOKENS.LocalePathService, LocalePathService)\n this._container.register(ROUTER_TOKENS.LocaleUrlService, LocaleUrlService)\n this._container.register(ROUTER_TOKENS.RouteRegistry, RouteRegistry)\n this._container.register(ROUTER_TOKENS.Uri, Uri)\n\n const routerConfigs = this.moduleRegistry.getAllRouterConfigs()\n if (routerConfigs.length > 0) {\n this._container.registerValue(ROUTER_TOKENS.RouterResolver, new RouterResolver(routerConfigs))\n }\n\n this._container.register(RouteRegistrationService, RouteRegistrationService)\n }\n\n /**\n * Load the events subsystem and wire listeners. Needed by the HTTP, queue,\n * and scheduled paths (handlers emit events). Independent of the queue\n * subsystem so HTTP-only apps never load queue code. EventsModule is\n * registered before the (possibly empty) listener wiring so `emit()` works\n * even with zero listeners; dedups against the framework's own load.\n */\n private initializeEventListeners(): Promise<void> {\n this.eventsInitPromise ??= runWithContainer(this._container, async () => {\n const { EventsModule } = await import('./events/events.module')\n await this.moduleRegistry.registerLazy(EventsModule)\n this.registerEventListeners()\n })\n return this.eventsInitPromise\n }\n\n /**\n * Wire event and queue handlers for any non-HTTP request scope — queue\n * batches, scheduled/cron runs, CLI commands, Durable Objects, Workflows,\n * WorkerEntrypoints — all of which may emit events or dispatch to queues from\n * arbitrary user code. This is the single source of truth for that wiring:\n * every non-HTTP entry point routes through it rather than open-coding which\n * subsystems to init, so a subsystem can't be silently dropped by a future\n * refactor (the queue half was once lost from the command path that way).\n *\n * HTTP (`fetch`) deliberately does NOT use this: a fetch worker only enqueues\n * to the async Cloudflare queue and never processes consumers inline, so it\n * skips queue init via `initializeRouting`.\n */\n async ensureScopedHandlers(): Promise<void> {\n await this.initializeEventListeners()\n await this.initializeQueue()\n }\n\n /**\n * Load the queue subsystem on demand (first queue/scheduled trigger). i18n is\n * ensured first because the queue registry depends on it.\n */\n private initializeQueue(): Promise<void> {\n this.queueInitPromise ??= runWithContainer(this._container, async () => {\n await this.ensureI18n()\n const { QueueModule } = await import('./queue/queue.module')\n this.moduleRegistry.register(QueueModule as unknown as ModuleClass)\n this.consumerRegistry = this._container.resolve<ConsumerRegistry>(DI_TOKENS.ConsumerRegistry)\n await this.registerQueueConsumers()\n })\n return this.queueInitPromise\n }\n\n /**\n * Load i18n on demand. Coupled to the request path (Zod validation error maps,\n * OpenAPI descriptions, queue registry), so it loads before routing/queue\n * handling. Uses `registerLazy` because I18nModule has an `onInitialize` hook\n * (configures the Zod error map). Dedups if the app already imported i18n.\n */\n private ensureI18n(): Promise<void> {\n this.i18nInitPromise ??= runWithContainer(this._container, async () => {\n const { I18nModule } = await import('./i18n/i18n.module')\n await this.moduleRegistry.registerLazy(I18nModule as unknown as ModuleClass)\n })\n return this.i18nInitPromise\n }\n\n /**\n * Load the cron subsystem on demand (first scheduled trigger, or at bootstrap\n * when the app declares jobs).\n */\n private ensureCron(): Promise<CronManager> {\n this.cronInitPromise ??= runWithContainer(this._container, async () => {\n const { CronModule } = await import('./cron/cron.module')\n this.moduleRegistry.register(CronModule)\n this.cronManager = this._container.resolve<CronManager>(DI_TOKENS.Cron)\n this.registerCronJobs(this.cronManager)\n })\n return this.cronInitPromise.then(() => this.cronManager!)\n }\n\n resolve<T>(token: symbol): T {\n try {\n return this._container.resolve(token)\n } catch (error) {\n const handler = this._container.resolve<ExceptionHandler>(DI_TOKENS.ExceptionHandler)\n const ctx = createCliExceptionContext('resolve')\n void handler.handle(error, ctx)\n throw error\n }\n }\n\n async handleQueue(batch: MessageBatch, queueName: string): Promise<void> {\n await this.ensureScopedHandlers()\n\n const firstMessage = batch.messages[0]?.body as QueueMessage | undefined\n const locale = firstMessage?.metadata?.locale ?? 'en'\n const mockRouterContext = this.createMockRouterContext(locale)\n\n await this._container.runInRequestScope(mockRouterContext, async (requestContainer) => {\n try {\n const queueManager = requestContainer.resolve<QueueManager>(DI_TOKENS.Queue)\n await queueManager.processBatch(queueName, batch)\n } catch (error) {\n const handler = requestContainer.resolve<ExceptionHandler>(DI_TOKENS.ExceptionHandler)\n await handler.handle(error, createQueueExceptionContext(queueName))\n throw error\n }\n })\n }\n\n async handleScheduled(controller: ScheduledController): Promise<void> {\n const cronManager = await this.ensureCron()\n // A scheduled job is a non-HTTP scope that can emit events and dispatch to\n // queues (timeout/reminder/digest emails) — wire both so dispatches aren't\n // silently dropped by the sync provider. i18n is ensured independently for\n // the job execution context. All inits are memoized, so this is idempotent.\n await this.ensureScopedHandlers()\n await this.ensureI18n()\n\n const mockRouterContext = this.createMockRouterContext('en')\n\n await this._container.runInRequestScope(mockRouterContext, async (requestContainer) => {\n try {\n await cronManager.executeScheduled(controller, requestContainer)\n } catch (error) {\n const handler = requestContainer.resolve<ExceptionHandler>(DI_TOKENS.ExceptionHandler)\n await handler.handle(error, createCronExceptionContext())\n throw error\n }\n })\n }\n\n createMockRouterContext(locale = 'en'): RouterContext {\n return {\n getLocale: () => locale,\n setLocale: () => { /* no-op */ },\n // Non-HTTP scopes have no request headers\n header: () => undefined,\n // Inside runInRequestScope the active container is the request-scoped child\n // (set in AsyncLocalStorage); return it so request-scoped providers resolve\n // against the scope, not the global container.\n getContainer: () => containerStorage.getStore() ?? this._container,\n } as unknown as RouterContext\n }\n\n async shutdown(): Promise<void> {\n // A failed initialize() has already torn itself down — only a fully\n // initialized application owns resources here.\n if (!this.initialized) return\n this.initialized = false\n\n await this.teardown()\n }\n\n /** Releases everything boot acquired: module resources, then the container. */\n private async teardown(): Promise<void> {\n await this.moduleRegistry.shutdown()\n\n const logger = this._container.resolve<LoggerService>(LOGGER_TOKENS.LoggerService)\n logger.info('Disposing container...')\n\n await this._container.dispose()\n }\n\n async handleCommand(name: string, input?: CommandInput): Promise<CommandResult> {\n // Routing first: commands generate URLs via route() (e.g. tenant:bootstrap\n // prints tenant URLs / builds email links).\n await this.initializeRouting()\n // A CLI command is a non-HTTP scope that can dispatch to queues and emit\n // events from arbitrary user code (e.g. tenant:bootstrap → email.send /\n // tenant.geo.seed). Without this the dev/CLI sync provider finds zero\n // consumers and silently drops every dispatched message. All inits are\n // memoized, so the events half (already wired by routing) is a no-op here.\n await this.ensureScopedHandlers()\n // Resolve QuarryRegistry lazily (deferred from bootstrap)\n this.quarry ??= this._container.resolve<QuarryRegistry>(DI_TOKENS.Quarry)\n const mockContext = this.createMockRouterContext('en')\n return this._container.runInRequestScope(mockContext, async () => {\n return this.quarry.call(name, input)\n })\n }\n\n private initializeRouting(): Promise<void> {\n this.routingInitPromise ??= runWithContainer(this._container, async () => {\n await this.initializeEventListeners()\n await this.ensureI18n()\n const { OpenAPIModule } = await import('./openapi')\n this.moduleRegistry.register(OpenAPIModule as unknown as ModuleClass)\n await this.registerRoutingServices()\n this.honoApp = this._container.resolve<HonoApp>(ROUTER_TOKENS.HonoApp)\n await this.honoApp.configure()\n })\n return this.routingInitPromise\n }\n\n private registerCommands(): void {\n this.quarry ??= this._container.resolve<QuarryRegistry>(DI_TOKENS.Quarry)\n const commands = this.moduleRegistry.getAllCommands()\n for (const CommandClass of commands) {\n this.quarry.register(CommandClass as Constructor<Command>)\n }\n }\n\n private registerSeeders(): void {\n const seeders = this.moduleRegistry.getAllSeeders()\n if (seeders.length === 0) return\n const registry = this._container.resolve<SeederRegistry>(SEEDER_TOKENS.SeederRegistry)\n for (const SeederClass of seeders) {\n registry.register(SeederClass as Constructor<Seeder>)\n }\n }\n\n private async registerQueueConsumers(): Promise<void> {\n const consumerClasses = this.moduleRegistry.getAllConsumers()\n if (consumerClasses.length === 0) return\n\n // Consumers are resolved fresh per message at dispatch time (see\n // QueueManager / SyncQueueProvider). Here we only need each consumer's\n // declared message types; read them from a throwaway instance built inside a\n // request scope so a consumer that injects request-scoped providers\n // (@InjectQueue, i18n, auth context, …) doesn't fail to instantiate at boot.\n const mockContext = this.createMockRouterContext('en')\n await this._container.runInRequestScope(mockContext, (requestContainer) => {\n for (const ConsumerClass of consumerClasses) {\n const consumer = requestContainer.resolve(ConsumerClass) as IQueueConsumer\n this.consumerRegistry.register(\n ConsumerClass as Constructor<IQueueConsumer>,\n consumer.messageTypes,\n )\n }\n })\n }\n\n private registerCronJobs(cronManager: CronManager): void {\n for (const JobClass of this.moduleRegistry.getAllJobs()) {\n const schedule = (JobClass as unknown as { schedule: string }).schedule\n if (schedule) {\n cronManager.registerJob(schedule, JobClass as Constructor<CronJob>)\n } else {\n const logger = this._container.resolve<LoggerService>(LOGGER_TOKENS.LoggerService)\n logger.warn(`Cron job \"${JobClass.name}\" has no static schedule property — skipped`)\n }\n }\n }\n\n private registerEventListeners(): void {\n const listeners = this.moduleRegistry.getAllListeners()\n if (listeners.length === 0) {\n return\n }\n\n const eventRegistry = this._container.resolve<EventRegistry>(DI_TOKENS.EventRegistry)\n\n for (const ListenerClass of listeners) {\n const handlers = getListenerHandlers(ListenerClass)\n\n for (const { methodName, event, options } of handlers) {\n // Resolve the listener fresh per event from the active scope (events are\n // emitted inside an HTTP request or runInScope), so request-scoped\n // listener dependencies bind to the emitting request rather than being\n // frozen at boot. Listener metadata is read from the class statically —\n // no instance is created until an event actually fires.\n const handler: EventHandler = ((...args: unknown[]) => {\n const instance = getContainer().resolve(ListenerClass) as Record<\n string,\n (...a: unknown[]) => unknown\n >\n return instance[methodName](...args)\n }) as EventHandler\n\n eventRegistry.on(event, handler, options)\n }\n }\n }\n\n private registerLoggerService(): void {\n const logLevel = this.appConfig.logging?.level ?? LogLevel.INFO\n const formatter = this.appConfig.logging?.formatter ?? 'json'\n\n this._container.registerValue(LOGGER_TOKENS.LogLevelOptions, logLevel)\n\n this._container\n .when(() => formatter === 'pretty')\n .use(LOGGER_TOKENS.Formatter)\n .give(PrettyFormatter)\n .otherwise(JsonFormatter)\n\n this._container.registerSingleton(LOGGER_TOKENS.LoggerService, LoggerService)\n }\n\n /**\n * Bootstrap kernel — registered imperatively because they cannot be expressed\n * as ordinary module providers: ExceptionHandler is a user-overridable forced\n * singleton (a module ClassProvider derives scope from the class decorator,\n * which a user handler may not carry), and LazyModuleLoader is the loader\n * itself. Subsystem registries (events/cron/quarry/seeder) are modules.\n */\n private registerCoreServices(): void {\n this._container.registerSingleton(\n DI_TOKENS.ExceptionHandler,\n (this.appConfig.exceptionHandler ?? DefaultExceptionHandler) as Constructor,\n )\n this._container.registerSingleton(DI_TOKENS.LazyModuleLoader, LazyModuleLoader)\n }\n\n private initializeExceptionHandler(): void {\n const handler = this._container.resolve<ExceptionHandler>(DI_TOKENS.ExceptionHandler)\n handler.register()\n this.moduleRegistry.configureExceptionHandlers(handler)\n }\n}\n","import { Application, type ApplicationConfig } from './application'\nimport type { StratalEnv } from './env'\nimport { StratalNotInitializedError, StratalSupersededError } from './errors'\nimport type { HonoApp } from './router/hono-app'\n\n/**\n * Stratal — Hono-style entry point for Cloudflare Workers.\n *\n * Eagerly bootstraps the Application at construction time, dynamically\n * importing `cloudflare:workers` for env and waitUntil.\n *\n * @example\n * ```typescript\n * import { Stratal } from 'stratal'\n * import { AppModule } from './app.module'\n *\n * export default new Stratal({ module: AppModule })\n * ```\n */\nexport class Stratal<Env extends StratalEnv = StratalEnv> {\n private app: Application | null = null\n private initPromise: Promise<Application>\n\n private static _application: Promise<Application> | null = null\n private static _generation = 0\n private static _current: Stratal | null = null\n /**\n * Serializes generation teardown: each new instance appends the previous\n * instance's shutdown here, and `prepareApp` awaits the chain before\n * allocating the new Application. Old and new DI graphs never coexist,\n * so repeated Vite HMR reloads can't accumulate live object graphs.\n */\n private static _teardownChain: Promise<void> = Promise.resolve()\n\n constructor(config: ApplicationConfig) {\n this.fetch = this.fetch.bind(this)\n this.queue = this.queue.bind(this)\n this.scheduled = this.scheduled.bind(this)\n\n // Invalidate any in-flight initialization from a previous instance (Vite HMR reload)\n const generation = ++Stratal._generation\n\n const previous = Stratal._current\n Stratal._current = this\n if (previous) {\n Stratal._teardownChain = Stratal._teardownChain\n .then(() => previous.shutdown())\n .catch((error: unknown) => {\n console.error('[stratal] Failed to shut down superseded instance:', error)\n })\n }\n\n // Capture the chain as of THIS construction: it contains only strictly\n // older generations' teardowns. Reading the static inside prepareApp\n // instead would deadlock when this generation is superseded before its\n // first await — it would wait on a chain containing its own shutdown,\n // which waits on this initPromise.\n const teardownBarrier = Stratal._teardownChain\n\n this.initPromise = this.prepareApp(config, generation, teardownBarrier)\n Stratal._application = this.initPromise\n // A superseded generation rejects; without an awaiter (no in-flight\n // request during the reload) that would surface as an unhandled rejection.\n // Anything else is a real bootstrap failure — log it so it isn't invisible\n // until the first request arrives (awaiters still get the rejection).\n this.initPromise.catch((error: unknown) => {\n if (error instanceof StratalSupersededError) return\n console.error('[stratal] Initialization failed:', error)\n })\n }\n\n async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {\n const app = await this.ensureReady()\n const hono = await app.ensureHono()\n return hono.fetch(request, env, ctx)\n }\n\n async queue(batch: MessageBatch): Promise<void> {\n const app = await this.ensureReady()\n return app.handleQueue(batch, batch.queue)\n }\n\n async scheduled(controller: ScheduledController): Promise<void> {\n const app = await this.ensureReady()\n return app.handleScheduled(controller)\n }\n\n get hono(): Promise<HonoApp> {\n return this.ensureReady().then(app => app.ensureHono())\n }\n\n async shutdown(): Promise<void> {\n try { this.app = await this.initPromise } catch { /* ignore */ }\n if (this.app) {\n await this.app.shutdown()\n this.app = null\n }\n // If we're still the live generation (explicit shutdown, not supersession),\n // clear the statics so resolveApplication() throws StratalNotInitializedError\n // instead of handing out the disposed Application.\n if (Stratal._current === this) {\n Stratal._current = null\n Stratal._application = null\n }\n }\n\n /**\n * @internal\n * Resolves the Application instance from the static singleton.\n * Used by worker base classes (DurableObject, Workflow, WorkerEntrypoint)\n * to access the DI container without going through Cloudflare RPC.\n * Generations superseded mid-boot reject with {@link StratalSupersededError};\n * this retries against the replacing generation so callers always land on\n * the live Application.\n */\n static async resolveApplication(): Promise<Application> {\n let current = Stratal._application\n while (current) {\n try {\n return await current\n } catch (error) {\n // Superseded by a newer generation while booting — its constructor has\n // already swapped _application; retry against the replacement.\n if (error instanceof StratalSupersededError && Stratal._application !== current) {\n current = Stratal._application\n continue\n }\n throw error\n }\n }\n throw new StratalNotInitializedError()\n }\n\n private async ensureReady(): Promise<Application> {\n if (this.app) return this.app\n try {\n this.app = await this.initPromise\n return this.app\n } catch (error) {\n if (error instanceof StratalSupersededError) {\n // This instance was replaced mid-boot (Vite HMR reload) — serve the\n // request from the generation that replaced it.\n return Stratal.resolveApplication()\n }\n throw error\n }\n }\n\n private async prepareApp(\n config: ApplicationConfig,\n generation: number,\n teardownBarrier: Promise<void>,\n ): Promise<Application> {\n const { env, waitUntil } = await import('cloudflare:workers')\n\n // Let every previous generation finish tearing down before allocating the\n // new DI graph — concurrent old+new graphs are what leak across reloads.\n await teardownBarrier\n\n // After the awaits, check if a newer instance has replaced us (Vite HMR reload)\n if (generation !== Stratal._generation) {\n throw new StratalSupersededError(generation)\n }\n\n const app = new Application({ ...config, env: env as Env, ctx: { waitUntil } })\n await app.initialize()\n\n // Check again after initialization completes\n if (generation !== Stratal._generation) {\n await app.shutdown()\n throw new StratalSupersededError(generation)\n }\n\n return app\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { ct as Application, gn as HonoApp, lt as ApplicationConfig } from "./index-B_JoEl3V.mjs";
1
+ import { Sn as HonoApp, ft as Application, pt as ApplicationConfig } from "./index-uybm0bhQ.mjs";
2
2
  import { t as StratalEnv } from "./env-ug22bJj7.mjs";
3
3
 
4
4
  //#region src/stratal.d.ts
@@ -21,7 +21,14 @@ declare class Stratal<Env extends StratalEnv = StratalEnv> {
21
21
  private initPromise;
22
22
  private static _application;
23
23
  private static _generation;
24
- private static _previousInstance;
24
+ private static _current;
25
+ /**
26
+ * Serializes generation teardown: each new instance appends the previous
27
+ * instance's shutdown here, and `prepareApp` awaits the chain before
28
+ * allocating the new Application. Old and new DI graphs never coexist,
29
+ * so repeated Vite HMR reloads can't accumulate live object graphs.
30
+ */
31
+ private static _teardownChain;
25
32
  constructor(config: ApplicationConfig);
26
33
  fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>;
27
34
  queue(batch: MessageBatch): Promise<void>;
@@ -33,6 +40,9 @@ declare class Stratal<Env extends StratalEnv = StratalEnv> {
33
40
  * Resolves the Application instance from the static singleton.
34
41
  * Used by worker base classes (DurableObject, Workflow, WorkerEntrypoint)
35
42
  * to access the DI container without going through Cloudflare RPC.
43
+ * Generations superseded mid-boot reject with {@link StratalSupersededError};
44
+ * this retries against the replacing generation so callers always land on
45
+ * the live Application.
36
46
  */
37
47
  static resolveApplication(): Promise<Application>;
38
48
  private ensureReady;
@@ -40,4 +50,4 @@ declare class Stratal<Env extends StratalEnv = StratalEnv> {
40
50
  }
41
51
  //#endregion
42
52
  export { Stratal as t };
43
- //# sourceMappingURL=stratal-DwDJPY9N.d.mts.map
53
+ //# sourceMappingURL=stratal-D5j_I14G.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stratal-D5j_I14G.d.mts","names":[],"sources":["../src/stratal.ts"],"mappings":";;;;;AAmBA;;;;;;;;;;;;;cAAa,OAAA,aAAoB,UAAA,GAAa,UAAA;EAAA,QACpC,GAAA;EAAA,QACA,WAAA;EAAA,eAEO,YAAA;EAAA,eACA,WAAA;EAAA,eACA,QAAA;EA0FoB;;;;;;EAAA,eAnFpB,cAAA;cAEH,MAAA,EAAQ,iBAAA;EAqCd,KAAA,CAAM,OAAA,EAAS,OAAA,EAAS,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,gBAAA,GAAmB,OAAA,CAAQ,QAAA;EAMlE,KAAA,CAAM,KAAA,EAAO,YAAA,GAAe,OAAA;EAK5B,SAAA,CAAU,UAAA,EAAY,mBAAA,GAAsB,OAAA;EAAA,IAK9C,IAAA,IAAQ,OAAA,CAAQ,OAAA;EAId,QAAA,IAAY,OAAA;EAzDE;;;;;;;;;EAAA,OAiFP,kBAAA,IAAsB,OAAA,CAAQ,WAAA;EAAA,QAkB7B,WAAA;EAAA,QAeA,UAAA;AAAA"}
@@ -0,0 +1,80 @@
1
+ import { r as stripLocalePrefix } from "./locale-url-nZrZxqJP.mjs";
2
+ //#region src/router/trailing-slash.ts
3
+ /** Normalise the `trailingSlash` config (bare mode or `{ mode, exclude }`). */
4
+ function resolveTrailingSlash(config) {
5
+ if (!config) return { mode: "ignore" };
6
+ if (typeof config === "string") return { mode: config };
7
+ return config;
8
+ }
9
+ /** Strip a single trailing `/` from a path. Root (`/`) is left untouched. */
10
+ const stripTrailingSlash = (path) => path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
11
+ /**
12
+ * Whether a pathname is exempt from trailing-slash canonicalisation.
13
+ *
14
+ * String patterns are segment-aware prefixes. RegExps are tested against
15
+ * both forms of the pathname (with and without the trailing slash), so a
16
+ * pattern anchored to either form exempts both — same guarantee as strings.
17
+ *
18
+ * When `locales` is given (path-based locale detection), a leading locale
19
+ * segment is stripped and the bare path matched too, so `'/callback'` also
20
+ * exempts `/fr/callback` — exclusions are written in route space, like the
21
+ * routes themselves.
22
+ */
23
+ function isTrailingSlashExcluded(pathname, exclude, locales) {
24
+ if (!exclude?.length) return false;
25
+ const candidates = [pathname];
26
+ if (locales?.length) {
27
+ const stripped = stripLocalePrefix(pathname, locales);
28
+ if (stripped !== pathname) candidates.push(stripped);
29
+ }
30
+ return candidates.some((candidate) => {
31
+ const bare = stripTrailingSlash(candidate);
32
+ const slashed = bare === "/" ? bare : `${bare}/`;
33
+ return exclude.some((pattern) => {
34
+ if (typeof pattern !== "string") return pattern.test(bare) || pattern.test(slashed);
35
+ const prefix = stripTrailingSlash(pattern);
36
+ return bare === prefix || bare.startsWith(`${prefix}/`);
37
+ });
38
+ });
39
+ }
40
+ /**
41
+ * Apply a trailing-slash config to a URL or path.
42
+ *
43
+ * - `'ignore'` — return as-is.
44
+ * - `'always'` — append `/` to the pathname unless it already has one.
45
+ * Skipped when the last segment contains `.` (file-like paths) and for the
46
+ * root `/` path.
47
+ * - `'never'` — strip a trailing `/` from the pathname. Skipped for root.
48
+ *
49
+ * Paths matching the config's `exclude` list are returned as-is — their
50
+ * canonical form is owned elsewhere (e.g. an OAuth redirect URI registered
51
+ * with an IdP and matched byte-for-byte). Pass `locales` when path-based
52
+ * locale detection is active so locale-prefixed forms of excluded paths are
53
+ * exempt too (see {@link isTrailingSlashExcluded}).
54
+ *
55
+ * Preserves query string and hash. Handles both relative paths
56
+ * (`/foo?x=1`) and absolute URLs (`https://host/foo?x=1`).
57
+ *
58
+ * Used by URL-generation helpers and the redirect middleware so canonical
59
+ * form is computed in one place.
60
+ */
61
+ function applyTrailingSlash(url, config, locales) {
62
+ const { mode, exclude } = resolveTrailingSlash(config);
63
+ if (mode === "ignore") return url;
64
+ const isAbsolute = /^https?:\/\//i.test(url);
65
+ const parsed = isAbsolute ? new URL(url) : new URL(url, "http://placeholder.local");
66
+ const path = parsed.pathname;
67
+ if (path === "/") return url;
68
+ if (isTrailingSlashExcluded(path, exclude, locales)) return url;
69
+ const hasTrailing = path.endsWith("/");
70
+ if (mode === "always" && !hasTrailing) {
71
+ if (path.slice(path.lastIndexOf("/") + 1).includes(".")) return url;
72
+ parsed.pathname = `${path}/`;
73
+ } else if (mode === "never" && hasTrailing) parsed.pathname = path.slice(0, -1);
74
+ else return url;
75
+ return isAbsolute ? parsed.toString() : `${parsed.pathname}${parsed.search}${parsed.hash}`;
76
+ }
77
+ //#endregion
78
+ export { isTrailingSlashExcluded as n, resolveTrailingSlash as r, applyTrailingSlash as t };
79
+
80
+ //# sourceMappingURL=trailing-slash-2SctvePW.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trailing-slash-2SctvePW.mjs","names":[],"sources":["../src/router/trailing-slash.ts"],"sourcesContent":["import { stripLocalePrefix } from './locale-url'\nimport type { TrailingSlashConfig, TrailingSlashExclude, TrailingSlashOptions } from './types'\n\n/** Normalise the `trailingSlash` config (bare mode or `{ mode, exclude }`). */\nexport function resolveTrailingSlash(config: TrailingSlashConfig | undefined): TrailingSlashOptions {\n if (!config) return { mode: 'ignore' }\n if (typeof config === 'string') return { mode: config }\n return config\n}\n\n/** Strip a single trailing `/` from a path. Root (`/`) is left untouched. */\nconst stripTrailingSlash = (path: string): string =>\n path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path\n\n/**\n * Whether a pathname is exempt from trailing-slash canonicalisation.\n *\n * String patterns are segment-aware prefixes. RegExps are tested against\n * both forms of the pathname (with and without the trailing slash), so a\n * pattern anchored to either form exempts both — same guarantee as strings.\n *\n * When `locales` is given (path-based locale detection), a leading locale\n * segment is stripped and the bare path matched too, so `'/callback'` also\n * exempts `/fr/callback` — exclusions are written in route space, like the\n * routes themselves.\n */\nexport function isTrailingSlashExcluded(\n pathname: string,\n exclude: readonly TrailingSlashExclude[] | undefined,\n locales?: readonly string[],\n): boolean {\n if (!exclude?.length) return false\n\n const candidates = [pathname]\n if (locales?.length) {\n const stripped = stripLocalePrefix(pathname, locales)\n if (stripped !== pathname) candidates.push(stripped)\n }\n\n return candidates.some((candidate) => {\n const bare = stripTrailingSlash(candidate)\n const slashed = bare === '/' ? bare : `${bare}/`\n return exclude.some((pattern) => {\n if (typeof pattern !== 'string') return pattern.test(bare) || pattern.test(slashed)\n const prefix = stripTrailingSlash(pattern)\n return bare === prefix || bare.startsWith(`${prefix}/`)\n })\n })\n}\n\n/**\n * Apply a trailing-slash config to a URL or path.\n *\n * - `'ignore'` — return as-is.\n * - `'always'` — append `/` to the pathname unless it already has one.\n * Skipped when the last segment contains `.` (file-like paths) and for the\n * root `/` path.\n * - `'never'` — strip a trailing `/` from the pathname. Skipped for root.\n *\n * Paths matching the config's `exclude` list are returned as-is — their\n * canonical form is owned elsewhere (e.g. an OAuth redirect URI registered\n * with an IdP and matched byte-for-byte). Pass `locales` when path-based\n * locale detection is active so locale-prefixed forms of excluded paths are\n * exempt too (see {@link isTrailingSlashExcluded}).\n *\n * Preserves query string and hash. Handles both relative paths\n * (`/foo?x=1`) and absolute URLs (`https://host/foo?x=1`).\n *\n * Used by URL-generation helpers and the redirect middleware so canonical\n * form is computed in one place.\n */\nexport function applyTrailingSlash(url: string, config: TrailingSlashConfig, locales?: readonly string[]): string {\n const { mode, exclude } = resolveTrailingSlash(config)\n if (mode === 'ignore') return url\n\n const isAbsolute = /^https?:\\/\\//i.test(url)\n const parsed = isAbsolute\n ? new URL(url)\n : new URL(url, 'http://placeholder.local')\n\n const path = parsed.pathname\n if (path === '/') return url\n if (isTrailingSlashExcluded(path, exclude, locales)) return url\n\n const hasTrailing = path.endsWith('/')\n\n if (mode === 'always' && !hasTrailing) {\n const lastSegment = path.slice(path.lastIndexOf('/') + 1)\n if (lastSegment.includes('.')) return url\n parsed.pathname = `${path}/`\n } else if (mode === 'never' && hasTrailing) {\n parsed.pathname = path.slice(0, -1)\n } else {\n return url\n }\n\n return isAbsolute\n ? parsed.toString()\n : `${parsed.pathname}${parsed.search}${parsed.hash}`\n}\n"],"mappings":";;;AAIA,SAAgB,qBAAqB,QAA+D;CAClG,IAAI,CAAC,QAAQ,OAAO,EAAE,MAAM,SAAS;CACrC,IAAI,OAAO,WAAW,UAAU,OAAO,EAAE,MAAM,OAAO;CACtD,OAAO;AACT;;AAGA,MAAM,sBAAsB,SAC1B,KAAK,SAAS,KAAK,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;;;;;;;;;;;;;AAc9D,SAAgB,wBACd,UACA,SACA,SACS;CACT,IAAI,CAAC,SAAS,QAAQ,OAAO;CAE7B,MAAM,aAAa,CAAC,QAAQ;CAC5B,IAAI,SAAS,QAAQ;EACnB,MAAM,WAAW,kBAAkB,UAAU,OAAO;EACpD,IAAI,aAAa,UAAU,WAAW,KAAK,QAAQ;CACrD;CAEA,OAAO,WAAW,MAAM,cAAc;EACpC,MAAM,OAAO,mBAAmB,SAAS;EACzC,MAAM,UAAU,SAAS,MAAM,OAAO,GAAG,KAAK;EAC9C,OAAO,QAAQ,MAAM,YAAY;GAC/B,IAAI,OAAO,YAAY,UAAU,OAAO,QAAQ,KAAK,IAAI,KAAK,QAAQ,KAAK,OAAO;GAClF,MAAM,SAAS,mBAAmB,OAAO;GACzC,OAAO,SAAS,UAAU,KAAK,WAAW,GAAG,OAAO,EAAE;EACxD,CAAC;CACH,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,mBAAmB,KAAa,QAA6B,SAAqC;CAChH,MAAM,EAAE,MAAM,YAAY,qBAAqB,MAAM;CACrD,IAAI,SAAS,UAAU,OAAO;CAE9B,MAAM,aAAa,gBAAgB,KAAK,GAAG;CAC3C,MAAM,SAAS,aACX,IAAI,IAAI,GAAG,IACX,IAAI,IAAI,KAAK,0BAA0B;CAE3C,MAAM,OAAO,OAAO;CACpB,IAAI,SAAS,KAAK,OAAO;CACzB,IAAI,wBAAwB,MAAM,SAAS,OAAO,GAAG,OAAO;CAE5D,MAAM,cAAc,KAAK,SAAS,GAAG;CAErC,IAAI,SAAS,YAAY,CAAC,aAAa;EAErC,IADoB,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CACzC,EAAE,SAAS,GAAG,GAAG,OAAO;EACtC,OAAO,WAAW,GAAG,KAAK;CAC5B,OAAO,IAAI,SAAS,WAAW,aAC7B,OAAO,WAAW,KAAK,MAAM,GAAG,EAAE;MAElC,OAAO;CAGT,OAAO,aACH,OAAO,SAAS,IAChB,GAAG,OAAO,WAAW,OAAO,SAAS,OAAO;AAClD"}
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./chunk-BBjsoOtd.mjs";
2
- import { d as inject, o as Request, r as DI_TOKENS, v as ROUTER_TOKENS } from "./di-DseMn-z9.mjs";
2
+ import { b as ROUTER_TOKENS, c as Request, p as inject, r as DI_TOKENS } from "./di-D7qmrAir.mjs";
3
3
  import { n as __decorateParam, t as __decorate } from "./decorate-CuAoSZvs.mjs";
4
- import { o as RouterError } from "./module-registry-Dm-pqHd3.mjs";
5
- import { t as applyTrailingSlash } from "./trailing-slash-CFyw8nYu.mjs";
4
+ import { o as RouterError } from "./module-registry-NxX5O0Qk.mjs";
6
5
  import { t as applyLocalePrefix } from "./locale-url-nZrZxqJP.mjs";
6
+ import { r as resolveTrailingSlash, t as applyTrailingSlash } from "./trailing-slash-2SctvePW.mjs";
7
7
  import { n as verifySignedUrl, t as signUrl } from "./signed-url-DIU0sK_6.mjs";
8
8
  //#region src/router/uri.ts
9
9
  var uri_exports = /* @__PURE__ */ __exportAll({
@@ -70,11 +70,13 @@ let Uri = class Uri {
70
70
  routerContext;
71
71
  _defaults = {};
72
72
  trailingSlash;
73
+ locales;
73
74
  localeConfig;
74
75
  constructor(registry, routerContext, application, localePathService) {
75
76
  this.registry = registry;
76
77
  this.routerContext = routerContext;
77
- this.trailingSlash = application.config.trailingSlash ?? "ignore";
78
+ this.trailingSlash = resolveTrailingSlash(application.config.trailingSlash);
79
+ this.locales = localePathService.localePathConfig?.allLocales;
78
80
  this.localeConfig = {
79
81
  defaultLocale: localePathService.localePathConfig?.defaultLocale ?? null,
80
82
  prefixDefaultLocale: localePathService.prefixDefaultLocale
@@ -123,7 +125,7 @@ let Uri = class Uri {
123
125
  let url = applyTrailingSlash(buildRouteUrl(registeredRoute, name, {
124
126
  ...this._defaults,
125
127
  ...params
126
- }, this.localeConfig), this.trailingSlash);
128
+ }, this.localeConfig), this.trailingSlash, this.locales);
127
129
  if (options?.absolute && !url.startsWith("http")) url = `${new URL(this.routerContext.c.req.url).origin}${url}`;
128
130
  return url;
129
131
  }
@@ -171,14 +173,14 @@ let Uri = class Uri {
171
173
  * Get the current request URL pathname (without query string).
172
174
  */
173
175
  current() {
174
- return applyTrailingSlash(new URL(this.routerContext.c.req.url).pathname, this.trailingSlash);
176
+ return applyTrailingSlash(new URL(this.routerContext.c.req.url).pathname, this.trailingSlash, this.locales);
175
177
  }
176
178
  /**
177
179
  * Get the current request URL with query string (pathname + search).
178
180
  */
179
181
  full() {
180
182
  const parsed = new URL(this.routerContext.c.req.url);
181
- return applyTrailingSlash(`${parsed.pathname}${parsed.search}`, this.trailingSlash);
183
+ return applyTrailingSlash(`${parsed.pathname}${parsed.search}`, this.trailingSlash, this.locales);
182
184
  }
183
185
  /**
184
186
  * Get the previous request URL from the Referer header.
@@ -210,7 +212,7 @@ let Uri = class Uri {
210
212
  * @param options - URL generation options
211
213
  */
212
214
  to(path, queryParams, options) {
213
- let url = applyTrailingSlash(path, this.trailingSlash);
215
+ let url = applyTrailingSlash(path, this.trailingSlash, this.locales);
214
216
  if (queryParams) {
215
217
  const entries = Object.entries(queryParams);
216
218
  if (entries.length > 0) {
@@ -230,7 +232,7 @@ let Uri = class Uri {
230
232
  query(path, queryParams) {
231
233
  const parsed = new URL(path, "https://placeholder.local");
232
234
  for (const [key, value] of Object.entries(queryParams)) parsed.searchParams.set(key, value);
233
- return applyTrailingSlash(`${parsed.pathname}${parsed.search}`, this.trailingSlash);
235
+ return applyTrailingSlash(`${parsed.pathname}${parsed.search}`, this.trailingSlash, this.locales);
234
236
  }
235
237
  getAppSecret() {
236
238
  const secret = this.routerContext.c.env.APP_SECRET;
@@ -248,4 +250,4 @@ Uri = __decorate([
248
250
  //#endregion
249
251
  export { buildRouteUrl as n, uri_exports as r, Uri as t };
250
252
 
251
- //# sourceMappingURL=uri-h7Q8Jug9.mjs.map
253
+ //# sourceMappingURL=uri-iwofWJ_T.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uri-iwofWJ_T.mjs","names":[],"sources":["../src/router/uri.ts"],"sourcesContent":["import type { Application } from '../application';\nimport { inject } from '../di';\nimport { Request } from '../di/decorators';\nimport { DI_TOKENS } from '../di/tokens';\nimport { applyLocalePrefix } from './locale-url';\nimport type { RouteName, RouteParams } from './route-map';\nimport type { RegisteredRoute, RouteRegistry } from './route-registry';\nimport type { RouterContext } from './router-context';\nimport { RouterError } from './router.error';\nimport { ROUTER_TOKENS } from './router.tokens';\nimport type { LocalePathService } from './services/locale-path.service';\nimport { signUrl, verifySignedUrl, type SignedUrlOptions } from './signed-url';\nimport { applyTrailingSlash, resolveTrailingSlash } from './trailing-slash';\nimport type { LocaleUrlConfig, TrailingSlashOptions } from './types';\n\n/**\n * Options for URL generation methods.\n */\nexport interface UriOptions {\n /** Generate absolute URL (scheme + host). Defaults to false. */\n absolute?: boolean\n}\n\n/**\n * Options for signed URL generation methods.\n */\nexport interface SignedUriOptions extends UriOptions, SignedUrlOptions { }\n\n/**\n * Encode a value for use as a path parameter.\n *\n * Splits on `/` and encodes each segment with `encodeURIComponent`, so callers\n * can pass slash-containing values for catch-all params (e.g. `:slug{.+}`) and\n * still get a usable URL — `'auth/login'` becomes `'auth/login'`, not\n * `'auth%2Flogin'`. Single segments behave exactly like `encodeURIComponent`.\n */\nfunction encodePathParam(value: string): string {\n return value.split('/').map(encodeURIComponent).join('/')\n}\n\n/**\n * Build a URL from a registered route, filling path/domain params and appending extras as query string.\n *\n * Pure function — no request context needed. Used by both the `Uri` class and the standalone `route()` function.\n *\n * @param route - The registered route to build a URL for\n * @param name - Route name (used in error messages)\n * @param params - Path params, domain params, and extra query params\n * @returns Relative URL string (or absolute with domain prefix if route has a domain pattern)\n *\n * @throws RouterError if a required path or domain param is missing\n */\nexport function buildRouteUrl(\n route: RegisteredRoute,\n name: string,\n params?: Record<string, string>,\n localeConfig?: LocaleUrlConfig,\n): string {\n const allParams = { ...params }\n const consumedKeys = new Set<string>()\n let url = route.path\n\n if (allParams.locale && route.localePaths?.length) {\n url = applyLocalePrefix(url, allParams.locale, localeConfig)\n consumedKeys.add('locale')\n }\n\n // Fill path :param placeholders (handles optional regex constraints like :locale{en|de|fr})\n for (const paramName of route.paramNames) {\n const value = allParams[paramName]\n if (value === undefined) {\n throw new RouterError(`Missing required route parameter \"${paramName}\" for route \"${name}\" (path: ${route.path})`)\n }\n url = url.replace(\n new RegExp(`:${paramName}(\\\\{[^}]*\\\\})?`),\n encodePathParam(value),\n )\n consumedKeys.add(paramName)\n }\n\n // Build domain if present\n let domain: string | undefined\n if (route.domain) {\n domain = route.domain\n for (const domainParam of route.domainParamNames) {\n const value = allParams[domainParam]\n if (value === undefined) {\n throw new RouterError(`Missing required domain parameter \"${domainParam}\" for route \"${name}\" (domain: ${route.domain})`)\n }\n domain = domain.replace(`{${domainParam}}`, encodeURIComponent(value))\n consumedKeys.add(domainParam)\n }\n }\n\n // Remaining params (not consumed by path or domain) become query string\n const queryEntries = Object.entries(allParams).filter(([key]) => !consumedKeys.has(key))\n if (queryEntries.length > 0) {\n const queryString = queryEntries\n .filter(([, value]) => Boolean(value))\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join('&')\n url = `${url}${queryString.length ? `?${queryString}` : ''}`\n }\n\n // Prepend domain if present\n if (domain) {\n url = `https://${domain}${url}`\n }\n\n return url\n}\n\n/**\n * URL generation service for named routes, signed URLs, and request URL access.\n *\n * Registered as request-scoped in the container — has access to the current request\n * via RouterContext for features like `current()`, `full()`, and signed URLs.\n *\n * @example\n * ```typescript\n * // In a controller:\n * const uri = ctx.getContainer().resolve<Uri>(ROUTER_TOKENS.Uri)\n * uri.route('users.show', { id: '1' })\n * uri.current()\n * await uri.signedRoute('unsubscribe', { user: '1' }, { expiresIn: 3600 })\n *\n * // Set defaults (e.g., in middleware):\n * uri.defaults({ locale: 'en' })\n * uri.route('posts.index') // auto-fills :locale param\n * ```\n */\n@Request(ROUTER_TOKENS.Uri)\nexport class Uri {\n private _defaults: Record<string, string> = {}\n private readonly trailingSlash: TrailingSlashOptions\n private readonly locales?: readonly string[]\n private readonly localeConfig: LocaleUrlConfig\n\n constructor(\n @inject(ROUTER_TOKENS.RouteRegistry) private readonly registry: RouteRegistry,\n @inject(ROUTER_TOKENS.RouterContext) private readonly routerContext: RouterContext,\n @inject(DI_TOKENS.Application) application: Application,\n @inject(ROUTER_TOKENS.LocalePathService) localePathService: LocalePathService,\n ) {\n // Resolved once — applyTrailingSlash accepts the resolved form directly.\n this.trailingSlash = resolveTrailingSlash(application.config.trailingSlash)\n this.locales = localePathService.localePathConfig?.allLocales\n this.localeConfig = {\n defaultLocale: localePathService.localePathConfig?.defaultLocale ?? null,\n prefixDefaultLocale: localePathService.prefixDefaultLocale,\n }\n }\n\n /**\n * Set default URL parameters for this request.\n * Applied to all subsequent `route()` calls — explicit params override defaults.\n *\n * @param params - Default parameters (e.g., `{ locale: 'en' }`)\n */\n defaults(params: Record<string, string>): void {\n this._defaults = { ...this._defaults, ...params }\n }\n\n /**\n * Read the currently configured default URL parameters.\n *\n * Used by frameworks that need to share these with the client (e.g. the\n * Inertia adapter ships them as a shared prop so `route()` calls in the\n * browser auto-fill the same sticky params as the server).\n */\n getDefaults(): Record<string, string> {\n return { ...this._defaults }\n }\n\n /**\n * Generate a URL from a named route.\n *\n * Keys matching `:param` placeholders fill the path.\n * Domain params (`{tenant}`) are consumed from the same object.\n * Extra keys become query string parameters.\n * Default params (from `defaults()`) are merged — explicit params override.\n *\n * @param name - Named route identifier\n * @param params - Route params + domain params + extra query params\n * @param options - URL generation options\n * @returns Generated URL string\n *\n * @throws RouterError if route name not found or required params missing\n */\n route<N extends RouteName>(name: N, params?: RouteParams<N>, options?: UriOptions): string {\n const registeredRoute = this.registry.get(name)\n if (!registeredRoute) {\n throw new RouterError(`Route name \"${name}\" was not found in the registry`)\n }\n\n const mergedParams = { ...this._defaults, ...params } as Record<string, string>\n let url = applyTrailingSlash(buildRouteUrl(registeredRoute, name, mergedParams, this.localeConfig), this.trailingSlash, this.locales)\n\n if (options?.absolute && !url.startsWith('http')) {\n const origin = new URL(this.routerContext.c.req.url).origin\n url = `${origin}${url}`\n }\n\n return url\n }\n\n /**\n * Generate a signed URL from a named route.\n *\n * @param name - Named route identifier\n * @param params - Route params + domain params + extra query params\n * @param options - Signing options (e.g., expiresIn) and URL options\n * @returns Signed URL string with signature query param\n *\n * @throws Error if APP_SECRET environment variable is not set\n */\n async signedRoute<N extends RouteName>(name: N, params?: RouteParams<N>, options?: SignedUriOptions): Promise<string> {\n const url = this.route(name, params, options)\n const secret = this.getAppSecret()\n return signUrl(url, secret, options)\n }\n\n /**\n * Generate a temporary signed URL from a named route.\n *\n * @param name - Named route identifier\n * @param expiresIn - Time-to-live in seconds\n * @param params - Route params + domain params + extra query params\n * @param options - URL generation options\n * @returns Signed URL string with signature and expires query params\n *\n * @throws Error if APP_SECRET environment variable is not set\n */\n async temporarySignedRoute<N extends RouteName>(name: N, expiresIn: number, params?: RouteParams<N>, options?: UriOptions): Promise<string> {\n return this.signedRoute(name, params, { ...options, expiresIn })\n }\n\n /**\n * Check if the current request has a valid signature.\n *\n * @returns true if the URL signature is valid and not expired\n */\n async hasValidSignature(): Promise<boolean> {\n const secret = (this.routerContext.c.env as unknown as Record<string, string>).APP_SECRET\n if (!secret) return false\n return verifySignedUrl(this.routerContext.c.req.url, secret)\n }\n\n /**\n * Get the current request URL pathname (without query string).\n */\n current(): string {\n const parsed = new URL(this.routerContext.c.req.url)\n return applyTrailingSlash(parsed.pathname, this.trailingSlash, this.locales)\n }\n\n /**\n * Get the current request URL with query string (pathname + search).\n */\n full(): string {\n const parsed = new URL(this.routerContext.c.req.url)\n return applyTrailingSlash(`${parsed.pathname}${parsed.search}`, this.trailingSlash, this.locales)\n }\n\n /**\n * Get the previous request URL from the Referer header.\n *\n * @param fallback - URL to return if no Referer header (default: '/')\n */\n previous(fallback = '/'): string {\n return this.routerContext.c.req.header('referer') ?? fallback\n }\n\n /**\n * Get the previous request URL pathname (no query string or host) from the Referer header.\n *\n * @param fallback - Path to return if no Referer header (default: '/')\n */\n previousPath(fallback = '/'): string {\n const referer = this.routerContext.c.req.header('referer')\n if (!referer) return fallback\n\n try {\n const parsed = new URL(referer)\n return parsed.pathname\n } catch {\n return referer\n }\n }\n\n /**\n * Build a URL to a raw path (not a named route) with optional query params.\n *\n * @param path - URL path (e.g., '/users')\n * @param queryParams - Query parameters to append\n * @param options - URL generation options\n */\n to(path: string, queryParams?: Record<string, string>, options?: UriOptions): string {\n let url = applyTrailingSlash(path, this.trailingSlash, this.locales)\n\n if (queryParams) {\n const entries = Object.entries(queryParams)\n if (entries.length > 0) {\n const queryString = entries\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join('&')\n url = url.includes('?') ? `${url}&${queryString}` : `${url}?${queryString}`\n }\n }\n\n if (options?.absolute && !url.startsWith('http')) {\n const origin = new URL(this.routerContext.c.req.url).origin\n url = `${origin}${url}`\n }\n\n return url\n }\n\n /**\n * Build a URL with query string parameters. Merges with existing query params in path.\n *\n * @param path - URL path, may already contain query params\n * @param queryParams - Query parameters to merge (new values override existing)\n */\n query(path: string, queryParams: Record<string, string>): string {\n const parsed = new URL(path, 'https://placeholder.local')\n for (const [key, value] of Object.entries(queryParams)) {\n parsed.searchParams.set(key, value)\n }\n return applyTrailingSlash(`${parsed.pathname}${parsed.search}`, this.trailingSlash, this.locales)\n }\n\n private getAppSecret(): string {\n const secret = this.routerContext.c.env.APP_SECRET\n if (!secret) {\n throw new Error('APP_SECRET environment variable is required for signed URLs')\n }\n return secret\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./chunk-BBjsoOtd.mjs";
2
- import { d as inject, r as DI_TOKENS, s as Singleton } from "./di-DseMn-z9.mjs";
2
+ import { l as Singleton, p as inject, r as DI_TOKENS } from "./di-D7qmrAir.mjs";
3
3
  import { n as __decorateParam, t as __decorate } from "./decorate-CuAoSZvs.mjs";
4
- import { f as VERSION_NEUTRAL } from "./exception-context-kEoMFwze.mjs";
4
+ import { f as VERSION_NEUTRAL } from "./exception-context-D-kvney-.mjs";
5
5
  //#region src/router/services/versioning.service.ts
6
6
  var versioning_service_exports = /* @__PURE__ */ __exportAll({ VersioningService: () => VersioningService });
7
7
  let VersioningService = class VersioningService {
@@ -33,4 +33,4 @@ VersioningService = __decorate([Singleton(), __decorateParam(0, inject(DI_TOKENS
33
33
  //#endregion
34
34
  export { versioning_service_exports as n, VersioningService as t };
35
35
 
36
- //# sourceMappingURL=versioning.service-C6aHky8-.mjs.map
36
+ //# sourceMappingURL=versioning.service-CCa2oYMJ.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"versioning.service-C6aHky8-.mjs","names":[],"sources":["../src/router/services/versioning.service.ts"],"sourcesContent":["import { inject } from '../../di'\nimport { Singleton } from '../../di/decorators'\nimport { DI_TOKENS } from '../../di/tokens'\nimport type { Application } from '../../application'\nimport { VERSION_NEUTRAL } from '../constants'\nimport type { VersioningOptions } from '../types'\n\n/**\n * Resolves version prefixes for route paths.\n *\n * Handles VERSION_NEUTRAL, multi-version arrays, default version fallback,\n * and configurable prefix (default: 'v').\n *\n * Registered as a singleton in the container.\n */\n@Singleton()\nexport class VersioningService {\n private readonly options: VersioningOptions | null\n\n constructor(@inject(DI_TOKENS.Application) app: Application) {\n this.options = app.config.versioning ?? null\n }\n\n /** Whether versioning is enabled */\n get enabled(): boolean {\n return this.options !== null\n }\n\n /**\n * Resolve versioned paths for a base path.\n *\n * @param basePath - The base path (e.g., '/users')\n * @param version - Explicit version from controller/router config\n * @returns Array of versioned path strings (e.g., ['/v1/users', '/v2/users'])\n */\n resolve(basePath: string, version?: string | string[] | typeof VERSION_NEUTRAL): string[] {\n // Versioning disabled — return base path as-is\n if (!this.options) {\n return [basePath]\n }\n\n // VERSION_NEUTRAL — explicitly opt out of versioning\n if (version === VERSION_NEUTRAL) {\n return [basePath]\n }\n\n const prefix = this.options.prefix ?? 'v'\n\n // Explicit version(s) on the controller/router\n if (version !== undefined) {\n const versions = Array.isArray(version) ? version : [version]\n return versions.map(v => `/${prefix}${v}${basePath}`)\n }\n\n // No explicit version — apply defaultVersion if set\n if (this.options.defaultVersion !== undefined) {\n const defaults = Array.isArray(this.options.defaultVersion)\n ? this.options.defaultVersion\n : [this.options.defaultVersion]\n return defaults.map(v => `/${prefix}${v}${basePath}`)\n }\n\n // Versioning enabled but no version and no default — no prefix\n return [basePath]\n }\n}\n"],"mappings":";;;;;;AAgBO,IAAA,oBAAA,MAAM,kBAAkB;CAC7B;CAEA,YAAY,KAAiD;EAC3D,KAAK,UAAU,IAAI,OAAO,cAAc;CAC1C;;CAGA,IAAI,UAAmB;EACrB,OAAO,KAAK,YAAY;CAC1B;;;;;;;;CASA,QAAQ,UAAkB,SAAgE;EAExF,IAAI,CAAC,KAAK,SACR,OAAO,CAAC,QAAQ;EAIlB,IAAI,YAAY,iBACd,OAAO,CAAC,QAAQ;EAGlB,MAAM,SAAS,KAAK,QAAQ,UAAU;EAGtC,IAAI,YAAY,KAAA,GAEd,QADiB,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAC5C,KAAI,MAAK,IAAI,SAAS,IAAI,UAAU;EAItD,IAAI,KAAK,QAAQ,mBAAmB,KAAA,GAIlC,QAHiB,MAAM,QAAQ,KAAK,QAAQ,cAAc,IACtD,KAAK,QAAQ,iBACb,CAAC,KAAK,QAAQ,cAAc,GAChB,KAAI,MAAK,IAAI,SAAS,IAAI,UAAU;EAItD,OAAO,CAAC,QAAQ;CAClB;AACF;gCAlDC,UAAU,GAAA,gBAAA,GAII,OAAO,UAAU,WAAW,CAAA,CAAA,GAAA,iBAAA"}
1
+ {"version":3,"file":"versioning.service-CCa2oYMJ.mjs","names":[],"sources":["../src/router/services/versioning.service.ts"],"sourcesContent":["import { inject } from '../../di'\nimport { Singleton } from '../../di/decorators'\nimport { DI_TOKENS } from '../../di/tokens'\nimport type { Application } from '../../application'\nimport { VERSION_NEUTRAL } from '../constants'\nimport type { VersioningOptions } from '../types'\n\n/**\n * Resolves version prefixes for route paths.\n *\n * Handles VERSION_NEUTRAL, multi-version arrays, default version fallback,\n * and configurable prefix (default: 'v').\n *\n * Registered as a singleton in the container.\n */\n@Singleton()\nexport class VersioningService {\n private readonly options: VersioningOptions | null\n\n constructor(@inject(DI_TOKENS.Application) app: Application) {\n this.options = app.config.versioning ?? null\n }\n\n /** Whether versioning is enabled */\n get enabled(): boolean {\n return this.options !== null\n }\n\n /**\n * Resolve versioned paths for a base path.\n *\n * @param basePath - The base path (e.g., '/users')\n * @param version - Explicit version from controller/router config\n * @returns Array of versioned path strings (e.g., ['/v1/users', '/v2/users'])\n */\n resolve(basePath: string, version?: string | string[] | typeof VERSION_NEUTRAL): string[] {\n // Versioning disabled — return base path as-is\n if (!this.options) {\n return [basePath]\n }\n\n // VERSION_NEUTRAL — explicitly opt out of versioning\n if (version === VERSION_NEUTRAL) {\n return [basePath]\n }\n\n const prefix = this.options.prefix ?? 'v'\n\n // Explicit version(s) on the controller/router\n if (version !== undefined) {\n const versions = Array.isArray(version) ? version : [version]\n return versions.map(v => `/${prefix}${v}${basePath}`)\n }\n\n // No explicit version — apply defaultVersion if set\n if (this.options.defaultVersion !== undefined) {\n const defaults = Array.isArray(this.options.defaultVersion)\n ? this.options.defaultVersion\n : [this.options.defaultVersion]\n return defaults.map(v => `/${prefix}${v}${basePath}`)\n }\n\n // Versioning enabled but no version and no default — no prefix\n return [basePath]\n }\n}\n"],"mappings":";;;;;;AAgBO,IAAA,oBAAA,MAAM,kBAAkB;CAC7B;CAEA,YAAY,KAAiD;EAC3D,KAAK,UAAU,IAAI,OAAO,cAAc;CAC1C;;CAGA,IAAI,UAAmB;EACrB,OAAO,KAAK,YAAY;CAC1B;;;;;;;;CASA,QAAQ,UAAkB,SAAgE;EAExF,IAAI,CAAC,KAAK,SACR,OAAO,CAAC,QAAQ;EAIlB,IAAI,YAAY,iBACd,OAAO,CAAC,QAAQ;EAGlB,MAAM,SAAS,KAAK,QAAQ,UAAU;EAGtC,IAAI,YAAY,KAAA,GAEd,QADiB,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAC5C,KAAI,MAAK,IAAI,SAAS,IAAI,UAAU;EAItD,IAAI,KAAK,QAAQ,mBAAmB,KAAA,GAIlC,QAHiB,MAAM,QAAQ,KAAK,QAAQ,cAAc,IACtD,KAAK,QAAQ,iBACb,CAAC,KAAK,QAAQ,cAAc,GAChB,KAAI,MAAK,IAAI,SAAS,IAAI,UAAU;EAItD,OAAO,CAAC,QAAQ;CAClB;AACF;gCAlDC,UAAU,GAAA,gBAAA,GAII,OAAO,UAAU,WAAW,CAAA,CAAA,GAAA,iBAAA"}
@@ -1,4 +1,4 @@
1
- import { Dr as ApplicationError, Yn as ControllerOptions, nt as ContextQueryResult, or as RouterEnv, rt as RouterContext } from "../index-B_JoEl3V.mjs";
1
+ import { Ir as ApplicationError, fr as RouterEnv, ot as ContextQueryResult, st as RouterContext, tr as ControllerOptions } from "../index-uybm0bhQ.mjs";
2
2
  import { t as Constructor } from "../types-CmV_9xBD.mjs";
3
3
  import { Context } from "hono";
4
4
  import { WSContext, WSContext as WSContext$1, WSEvents, WSMessageReceive, WSReadyState, WSReadyState as WSReadyState$1 } from "hono/ws";
@@ -1,2 +1,2 @@
1
- import { a as getWsOnCloseMethod, c as WebSocketError, i as OnMessage, l as Gateway, n as OnClose, o as getWsOnErrorMethod, r as OnError, s as getWsOnMessageMethod, t as GatewayContext, u as isGateway } from "../gateway-context-TMu_AlJt.mjs";
1
+ import { a as getWsOnCloseMethod, c as WebSocketError, i as OnMessage, l as Gateway, n as OnClose, o as getWsOnErrorMethod, r as OnError, s as getWsOnMessageMethod, t as GatewayContext, u as isGateway } from "../gateway-context-m7kEzRa2.mjs";
2
2
  export { Gateway, GatewayContext, OnClose, OnError, OnMessage, WebSocketError, getWsOnCloseMethod, getWsOnErrorMethod, getWsOnMessageMethod, isGateway };