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,15 +1,15 @@
1
- import { d as inject, r as DI_TOKENS, v as ROUTER_TOKENS } from "../di-DseMn-z9.mjs";
1
+ import { b as ROUTER_TOKENS, p as inject, r as DI_TOKENS } from "../di-D7qmrAir.mjs";
2
2
  import { n as __decorateParam, t as __decorate } from "../decorate-CuAoSZvs.mjs";
3
3
  import "../logger/index.mjs";
4
- import { a as getListenerHandlers } from "../events-BXJGZjpG.mjs";
4
+ import { a as getListenerHandlers } from "../events-BhEQuT1X.mjs";
5
5
  import { n as Module } from "../module.decorator-CYHY6pG5.mjs";
6
6
  import { a as cyan, c as green, i as bold, l as red, t as Command, u as yellow } from "../command-BvmUAPPQ.mjs";
7
- import { t as Stratal } from "../stratal-DL9M38_s.mjs";
7
+ import { t as Stratal } from "../stratal-BL6FKUM_.mjs";
8
8
  import { t as QUEUE_TOKENS } from "../queue.tokens-DjHnFmre.mjs";
9
9
  import { t as I18N_TOKENS } from "../i18n.tokens-CZ_v8oyS.mjs";
10
10
  import { n as OPENAPI_TOKENS, t as OpenApiToolsService } from "../openapi-tools.service-BC5EC3R3.mjs";
11
11
  import { t as CommandNotFoundError } from "../command-not-found.error-ONAZ2Bpk.mjs";
12
- import { n as DbSeedListCommand, t as DbSeedCommand } from "../seeder-7ubkms-Y.mjs";
12
+ import { n as DbSeedListCommand, t as DbSeedCommand } from "../seeder-BPGY5rUb.mjs";
13
13
  import { z } from "zod";
14
14
  import { writeFileSync } from "node:fs";
15
15
  import { resolve } from "node:path";
@@ -863,7 +863,7 @@ let ScheduleListCommand = class ScheduleListCommand extends Command {
863
863
  this.loader = loader;
864
864
  }
865
865
  async handle() {
866
- const cron = (await this.loader.load(() => import("../cron.module-Bgzq5hiT.mjs").then((n) => n.n).then((m) => m.CronModule))).get(DI_TOKENS.Cron);
866
+ const cron = (await this.loader.load(() => import("../cron.module-C81HTzR7.mjs").then((n) => n.n).then((m) => m.CronModule))).get(DI_TOKENS.Cron);
867
867
  const schedules = cron.getAllSchedules();
868
868
  if (schedules.length === 0) {
869
869
  this.info("No cron jobs found");
@@ -1,6 +1,6 @@
1
- import { En as Quarry, Sn as CommandResult, Y as Container, bn as CommandInput } from "./index-B_JoEl3V.mjs";
1
+ import { $ as Container, En as CommandInput, Mn as Quarry, On as CommandResult } from "./index-uybm0bhQ.mjs";
2
2
  import { t as Constructor } from "./types-CmV_9xBD.mjs";
3
- import { t as Command } from "./command-CPhFHjG3.mjs";
3
+ import { t as Command } from "./command-DoBD2Cwl.mjs";
4
4
 
5
5
  //#region src/quarry/quarry-registry.d.ts
6
6
  /**
@@ -66,4 +66,4 @@ declare class QuarryRegistry implements Quarry {
66
66
  }
67
67
  //#endregion
68
68
  export { QuarryRegistry as t };
69
- //# sourceMappingURL=quarry-registry-CXg0RFXq.d.mts.map
69
+ //# sourceMappingURL=quarry-registry-DRnV-DDa.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"quarry-registry-CXg0RFXq.d.mts","names":[],"sources":["../src/quarry/quarry-registry.ts"],"mappings":";;;;;;AAwBA;;;;;;;;cACa,cAAA,YAA0B,MAAA;EAAA,iBAKqB,SAAA;EAAA,QAJlD,QAAA;EAAA,QACA,UAAA;EAAA,QACA,OAAA;cAEkD,SAAA,EAAW,SAAA;EAyJlC;;;;EAnJ7B,IAAA,CAAK,IAAA,UAAc,KAAA,GAAQ,YAAA,GAAe,OAAA,CAAQ,aAAA;EAXnB;;;EA8ErC,GAAA,CAAI,IAAA;EA3EI;;;EAmFR,GAAA,CAAI,IAAA,WAAe,WAAA,CAAY,OAAA;EA3EzB;;;EAmFN,GAAA,IAAO,GAAA,SAAY,WAAA,CAAY,OAAA;EAnFiB;;;EA0FhD,IAAA;IAAU,IAAA;IAAc,WAAA;IAAsB,OAAA;EAAA;EAP9C;;;EAiCM,SAAA,CAAU,OAAA;IAAY,UAAA;IAAqB,WAAA;IAAsB,aAAA;EAAA,IAA2B,OAAA;EAA5F;;;EAWA,KAAA,CAAM,IAAA,WAAe,OAAA;EAXX;;;;EA+BhB,QAAA,CAAS,YAAA,EAAc,WAAA,CAAY,OAAA;EAAA,QAkC3B,WAAA;EAAA,QAQA,WAAA;EAAA,QAIA,aAAA;AAAA"}
1
+ {"version":3,"file":"quarry-registry-DRnV-DDa.d.mts","names":[],"sources":["../src/quarry/quarry-registry.ts"],"mappings":";;;;;;AAwBA;;;;;;;;cACa,cAAA,YAA0B,MAAA;EAAA,iBAKqB,SAAA;EAAA,QAJlD,QAAA;EAAA,QACA,UAAA;EAAA,QACA,OAAA;cAEkD,SAAA,EAAW,SAAA;EAyJlC;;;;EAnJ7B,IAAA,CAAK,IAAA,UAAc,KAAA,GAAQ,YAAA,GAAe,OAAA,CAAQ,aAAA;EAXnB;;;EA8ErC,GAAA,CAAI,IAAA;EA3EI;;;EAmFR,GAAA,CAAI,IAAA,WAAe,WAAA,CAAY,OAAA;EA3EzB;;;EAmFN,GAAA,IAAO,GAAA,SAAY,WAAA,CAAY,OAAA;EAnFiB;;;EA0FhD,IAAA;IAAU,IAAA;IAAc,WAAA;IAAsB,OAAA;EAAA;EAP9C;;;EAiCM,SAAA,CAAU,OAAA;IAAY,UAAA;IAAqB,WAAA;IAAsB,aAAA;EAAA,IAA2B,OAAA;EAA5F;;;EAWA,KAAA,CAAM,IAAA,WAAe,OAAA;EAXX;;;;EA+BhB,QAAA,CAAS,YAAA,EAAc,WAAA,CAAY,OAAA;EAAA,QAkC3B,WAAA;EAAA,QAQA,WAAA;EAAA,QAIA,aAAA;AAAA"}
@@ -1,8 +1,8 @@
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 getContainer } from "./container-storage-BmOJ4_Na.mjs";
4
4
  import { n as __decorateParam, t as __decorate } from "./decorate-CuAoSZvs.mjs";
5
- import { t as createCliExceptionContext } from "./exception-context-kEoMFwze.mjs";
5
+ import { t as createCliExceptionContext } from "./exception-context-D-kvney-.mjs";
6
6
  import { n as Module } from "./module.decorator-CYHY6pG5.mjs";
7
7
  import { n as CommandError, r as COMMAND_INTERNALS } from "./command-BvmUAPPQ.mjs";
8
8
  import { t as CommandNotFoundError } from "./command-not-found.error-ONAZ2Bpk.mjs";
@@ -309,4 +309,4 @@ QuarryModule = __decorate([Module({ providers: [{
309
309
  //#endregion
310
310
  export { parseSignature as i, quarry_module_exports as n, QuarryRegistry as r, QuarryModule as t };
311
311
 
312
- //# sourceMappingURL=quarry.module-BuRPGMDm.mjs.map
312
+ //# sourceMappingURL=quarry.module-CcGxU2dJ.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"quarry.module-BuRPGMDm.mjs","names":[],"sources":["../src/quarry/command-internals.ts","../src/quarry/signature-parser.ts","../src/quarry/quarry-registry.ts","../src/quarry/quarry.module.ts"],"sourcesContent":["import type { Command } from './command'\nimport { COMMAND_INTERNALS } from './constants'\nimport type { CommandInput, CommandInternals, CommandResult } from './types'\n\n/** @internal Set the flat input values before calling handle() */\nexport function setCommandInputs(command: Command, values: CommandInput): void {\n command[COMMAND_INTERNALS].inputs = { ...values }\n}\n\n/** @internal Set the Quarry reference for this.call() support */\nexport function setCommandQuarry(\n command: Command,\n quarry: { call(name: string, input?: CommandInput): Promise<CommandResult> },\n): void {\n command[COMMAND_INTERNALS].quarry = quarry\n}\n\n/** @internal Collect the result after handle() completes */\nexport function getCommandResult(command: Command): CommandResult {\n const internals: CommandInternals = command[COMMAND_INTERNALS]\n return {\n exitCode: internals.exitCode,\n output: [...internals.output],\n errors: [...internals.errors],\n }\n}\n\n/** @internal Reset state between invocations */\nexport function resetCommandState(command: Command): void {\n const internals: CommandInternals = command[COMMAND_INTERNALS]\n internals.inputs = {}\n internals.output = []\n internals.errors = []\n internals.exitCode = 0\n}\n","import { CommandError } from './errors/command.error'\nimport type { ParsedArgument, ParsedOption, ParsedSignature } from './types'\n\n/**\n * Parse a Laravel-style command signature string.\n *\n * Signature syntax:\n * command-name {arg} ... — flat command\n * group subcommand {arg} ... — subcommand hierarchy (space-separated)\n * namespace:command {arg} ... — namespaced flat command (colon-separated)\n * {--flag} {--name=} {--name=default} {--name=*} {--A|name} {--name= : desc}\n *\n * Pure function, zero dependencies, edge-compatible.\n */\nexport function parseSignature(signature: string): ParsedSignature {\n const tokens = extractTokens(signature)\n const name = extractCommandName(signature)\n const args: ParsedArgument[] = []\n const options: ParsedOption[] = []\n\n for (const token of tokens) {\n const inner = token.slice(1, -1).trim() // strip { }\n\n if (inner.startsWith('--')) {\n options.push(parseOption(inner))\n } else {\n args.push(parseArgument(inner))\n }\n }\n\n return { name, arguments: args, options }\n}\n\nfunction extractCommandName(signature: string): string {\n const match = /^[\\w:.-]+(?:\\s+[\\w:.-]+)*/.exec(signature)\n if (!match) {\n throw new CommandError(`Invalid signature: cannot extract command name from \"${signature}\"`)\n }\n return match[0]\n}\n\nfunction extractTokens(signature: string): string[] {\n const tokens: string[] = []\n const regex = /\\{[^}]+\\}/g\n let match: RegExpExecArray | null\n\n while ((match = regex.exec(signature)) !== null) {\n tokens.push(match[0])\n }\n\n return tokens\n}\n\nfunction parseArgument(inner: string): ParsedArgument {\n const { value, description } = splitDescription(inner)\n\n // {name*} — array/variadic argument\n if (value.endsWith('*')) {\n return {\n name: value.slice(0, -1).trim(),\n required: true,\n isArray: true,\n description,\n }\n }\n\n // {name=default} — argument with default value\n const eqIdx = value.indexOf('=')\n if (eqIdx !== -1) {\n return {\n name: value.slice(0, eqIdx).trim(),\n required: false,\n default: value.slice(eqIdx + 1).trim(),\n isArray: false,\n description,\n }\n }\n\n // {name?} — optional argument\n if (value.endsWith('?')) {\n return {\n name: value.slice(0, -1).trim(),\n required: false,\n isArray: false,\n description,\n }\n }\n\n // {name} — required argument\n return {\n name: value.trim(),\n required: true,\n isArray: false,\n description,\n }\n}\n\nfunction parseOption(inner: string): ParsedOption {\n // Remove leading --\n const withoutDashes = inner.slice(2)\n const { value, description } = splitDescription(withoutDashes)\n\n // Check for alias: {--A|name...}\n let alias: string | undefined\n let optBody = value\n\n const pipeIdx = optBody.indexOf('|')\n if (pipeIdx !== -1) {\n alias = optBody.slice(0, pipeIdx).trim()\n optBody = optBody.slice(pipeIdx + 1).trim()\n }\n\n // {--name=*} — array option\n if (optBody.endsWith('=*')) {\n return {\n name: optBody.slice(0, -2).trim(),\n alias,\n isFlag: false,\n isArray: true,\n description,\n }\n }\n\n // {--name=default} — option with default value\n const eqIdx = optBody.indexOf('=')\n if (eqIdx !== -1) {\n const name = optBody.slice(0, eqIdx).trim()\n const defaultValue = optBody.slice(eqIdx + 1).trim()\n\n return {\n name,\n alias,\n isFlag: false,\n isArray: false,\n default: defaultValue || undefined,\n description,\n }\n }\n\n // {--flag} — boolean flag\n return {\n name: optBody.trim(),\n alias,\n isFlag: true,\n isArray: false,\n description,\n }\n}\n\nfunction splitDescription(value: string): { value: string; description?: string } {\n const colonIdx = value.indexOf(' : ')\n if (colonIdx === -1) {\n return { value }\n }\n\n return {\n value: value.slice(0, colonIdx).trim(),\n description: value.slice(colonIdx + 3).trim(),\n }\n}\n","import { inject } from '../di'\nimport type { Container } from '../di/container'\nimport { getContainer } from '../di/container-storage'\nimport { Singleton } from '../di/decorators'\nimport { DI_TOKENS } from '../di/tokens'\nimport { createCliExceptionContext } from '../errors/exception-context'\nimport type { ExceptionHandler } from '../errors/exception-handler'\nimport type { Constructor } from '../types'\nimport { type Command } from './command'\nimport { getCommandResult, setCommandInputs, setCommandQuarry } from './command-internals'\nimport { CommandNotFoundError } from './errors/command-not-found.error'\nimport { CommandError } from './errors/command.error'\nimport { parseSignature } from './signature-parser'\nimport type { CommandInput, CommandResult, ParsedSignature, Quarry } from './types'\n\n/**\n * QuarryRegistry — edge-compatible programmatic API for running commands.\n *\n * Registered as a singleton via DI_TOKENS.Quarry.\n * Commands are auto-discovered from module providers and registered at bootstrap.\n * Command constructors are stored at bootstrap; fresh instances are resolved per `call()`.\n *\n * Users should inject and type as `Quarry` (the interface), which only exposes `call()`.\n */\n@Singleton(DI_TOKENS.Quarry)\nexport class QuarryRegistry implements Quarry {\n private commands = new Map<string, Constructor<Command>>()\n private signatures = new Map<string, ParsedSignature>()\n private aliases = new Map<string, string>()\n\n constructor(@inject(DI_TOKENS.Container) private readonly container: Container) { }\n\n /**\n * Execute a command by name with optional flat input.\n * A fresh command instance is resolved from the container per invocation.\n */\n async call(name: string, input?: CommandInput): Promise<CommandResult> {\n const resolvedName = this.resolveName(name)\n const CommandClass = this.commands.get(resolvedName)\n\n if (!CommandClass) {\n throw new CommandNotFoundError(name)\n }\n\n const signature = this.signatures.get(resolvedName)!\n const mergedInput = this.applyDefaults(input ?? {}, signature)\n\n // Validate required arguments\n for (const arg of signature.arguments) {\n if (arg.required && (mergedInput[arg.name] === undefined || mergedInput[arg.name] === null)) {\n throw new CommandError(`Missing required argument: ${arg.name}`)\n }\n }\n\n let command: Command | undefined\n\n try {\n // Resolve from the active request scope (handleCommand runs call() inside\n // runInRequestScope), not the global container — commands may inject\n // request-scoped providers (e.g. @InjectQueue → QueueRegistry).\n command = getContainer().resolve<Command>(CommandClass)\n\n setCommandQuarry(command, this)\n setCommandInputs(command, mergedInput)\n\n const exitCode = await command.handle()\n const result = getCommandResult(command)\n\n if (typeof exitCode === 'number') {\n return { ...result, exitCode }\n }\n\n return result\n } catch (error) {\n if (error instanceof CommandError) {\n if (command) {\n const result = getCommandResult(command)\n return {\n exitCode: result.exitCode === 0 ? 1 : result.exitCode,\n output: result.output,\n errors: [...result.errors, error.message],\n }\n }\n return { exitCode: 1, output: [], errors: [error.message] }\n }\n\n const errorMessage = this.handleError(error, resolvedName)\n\n if (command) {\n const result = getCommandResult(command)\n return {\n exitCode: result.exitCode === 0 ? 1 : result.exitCode,\n output: result.output,\n errors: [...result.errors, errorMessage],\n }\n }\n return { exitCode: 1, output: [], errors: [errorMessage] }\n }\n }\n\n /**\n * Check if a command exists by name or alias.\n */\n has(name: string): boolean {\n const resolved = this.resolveName(name)\n return this.commands.has(resolved)\n }\n\n /**\n * Get a command constructor by name or alias.\n */\n get(name: string): Constructor<Command> | undefined {\n const resolved = this.resolveName(name)\n return this.commands.get(resolved)\n }\n\n /**\n * Get all registered command constructors.\n */\n all(): Map<string, Constructor<Command>> {\n return new Map(this.commands)\n }\n\n /**\n * List all commands with their descriptions and aliases.\n */\n list(): { name: string; description?: string; aliases: string[] }[] {\n const result: { name: string; description?: string; aliases: string[] }[] = []\n\n for (const [name, CommandClass] of this.commands) {\n const staticCommand = CommandClass as unknown as typeof Command\n const commandAliases: string[] = []\n\n for (const [alias, target] of this.aliases) {\n if (target === name) {\n commandAliases.push(alias)\n }\n }\n\n result.push({\n name,\n description: staticCommand.description,\n aliases: commandAliases,\n })\n }\n\n return result.sort((a, b) => a.name.localeCompare(b.name))\n }\n\n /**\n * Generate a compact listing of all commands with visual hierarchy and colors.\n */\n async listUsage(options?: { binaryName?: string; binaryLabel?: string; binaryVersion?: string }): Promise<string> {\n const commands = this.list()\n\n // Dynamic import to keep usage-generator tree-shakeable\n const { generateListing } = await import('./usage-generator')\n return generateListing(commands, this.signatures, options)\n }\n\n /**\n * Get auto-generated usage text for a command.\n */\n async usage(name: string): Promise<string> {\n const resolvedName = this.resolveName(name)\n const CommandClass = this.commands.get(resolvedName)\n\n if (!CommandClass) {\n throw new CommandNotFoundError(name)\n }\n\n const signature = this.signatures.get(resolvedName)!\n const staticCommand = CommandClass as unknown as typeof Command\n\n // Dynamic import to keep usage-generator tree-shakeable\n const { generateUsage } = await import('./usage-generator')\n return generateUsage(signature, staticCommand.description)\n }\n\n /**\n * Register a command constructor with the registry.\n * @internal Called by Application during bootstrap.\n */\n register(commandClass: Constructor<Command>): void {\n const staticCommand = commandClass as unknown as typeof Command\n\n if (!staticCommand.command) {\n throw new CommandError(`Command class ${commandClass.name} is missing static \"command\" signature`)\n }\n\n const signature = parseSignature(staticCommand.command)\n const name = signature.name\n\n if (this.commands.has(name) || this.aliases.has(name)) {\n throw new CommandError(`Duplicate command name: \"${name}\" is already registered`)\n }\n\n // Validate all aliases before any mutation\n if (staticCommand.aliases) {\n for (const alias of staticCommand.aliases) {\n if (this.commands.has(alias) || this.aliases.has(alias)) {\n throw new CommandError(`Duplicate alias: \"${alias}\" conflicts with an existing command or alias`)\n }\n }\n }\n\n // All checks passed — safe to mutate\n this.commands.set(name, commandClass)\n this.signatures.set(name, signature)\n\n if (staticCommand.aliases) {\n for (const alias of staticCommand.aliases) {\n this.aliases.set(alias, name)\n }\n }\n }\n\n private handleError(error: unknown, commandName: string): string {\n const handler = this.container.resolve<ExceptionHandler>(DI_TOKENS.ExceptionHandler)\n const ctx = createCliExceptionContext(commandName)\n // Fire-and-forget — reporting happens via waitUntil internally\n void handler.handle(error, ctx)\n return error instanceof Error ? error.message : String(error)\n }\n\n private resolveName(name: string): string {\n return this.aliases.get(name) ?? name\n }\n\n private applyDefaults(input: CommandInput, signature: ParsedSignature): CommandInput {\n const result = { ...input }\n\n // Apply argument defaults\n for (const arg of signature.arguments) {\n if (result[arg.name] === undefined && arg.default !== undefined) {\n result[arg.name] = arg.default\n }\n }\n\n // Apply option defaults\n for (const opt of signature.options) {\n if (result[opt.name] === undefined) {\n if (opt.default !== undefined) {\n result[opt.name] = opt.default\n } else if (opt.isFlag) {\n result[opt.name] = false\n }\n }\n }\n\n return result\n }\n}\n","import { DI_TOKENS } from '../di/tokens'\nimport { Module } from '../module/module.decorator'\nimport { QuarryRegistry } from './quarry-registry'\n\n/**\n * Registers the Quarry command registry (`DI_TOKENS.Quarry`).\n *\n * Eager: resolved synchronously at bootstrap (`registerCommands`) and by the\n * CLI runner (`bin/quarry.ts`), so it cannot be lazily loaded.\n */\n@Module({\n providers: [\n { provide: DI_TOKENS.Quarry, useClass: QuarryRegistry },\n ],\n})\nexport class QuarryModule { }\n"],"mappings":";;;;;;;;;;AAKA,SAAgB,iBAAiB,SAAkB,QAA4B;CAC7E,QAAQ,mBAAmB,SAAS,EAAE,GAAG,OAAO;AAClD;;AAGA,SAAgB,iBACd,SACA,QACM;CACN,QAAQ,mBAAmB,SAAS;AACtC;;AAGA,SAAgB,iBAAiB,SAAiC;CAChE,MAAM,YAA8B,QAAQ;CAC5C,OAAO;EACL,UAAU,UAAU;EACpB,QAAQ,CAAC,GAAG,UAAU,MAAM;EAC5B,QAAQ,CAAC,GAAG,UAAU,MAAM;CAC9B;AACF;;;;;;;;;;;;;;ACXA,SAAgB,eAAe,WAAoC;CACjE,MAAM,SAAS,cAAc,SAAS;CACtC,MAAM,OAAO,mBAAmB,SAAS;CACzC,MAAM,OAAyB,CAAC;CAChC,MAAM,UAA0B,CAAC;CAEjC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,QAAQ,MAAM,MAAM,GAAG,EAAE,EAAE,KAAK;EAEtC,IAAI,MAAM,WAAW,IAAI,GACvB,QAAQ,KAAK,YAAY,KAAK,CAAC;OAE/B,KAAK,KAAK,cAAc,KAAK,CAAC;CAElC;CAEA,OAAO;EAAE;EAAM,WAAW;EAAM;CAAQ;AAC1C;AAEA,SAAS,mBAAmB,WAA2B;CACrD,MAAM,QAAQ,4BAA4B,KAAK,SAAS;CACxD,IAAI,CAAC,OACH,MAAM,IAAI,aAAa,wDAAwD,UAAU,EAAE;CAE7F,OAAO,MAAM;AACf;AAEA,SAAS,cAAc,WAA6B;CAClD,MAAM,SAAmB,CAAC;CAC1B,MAAM,QAAQ;CACd,IAAI;CAEJ,QAAQ,QAAQ,MAAM,KAAK,SAAS,OAAO,MACzC,OAAO,KAAK,MAAM,EAAE;CAGtB,OAAO;AACT;AAEA,SAAS,cAAc,OAA+B;CACpD,MAAM,EAAE,OAAO,gBAAgB,iBAAiB,KAAK;CAGrD,IAAI,MAAM,SAAS,GAAG,GACpB,OAAO;EACL,MAAM,MAAM,MAAM,GAAG,EAAE,EAAE,KAAK;EAC9B,UAAU;EACV,SAAS;EACT;CACF;CAIF,MAAM,QAAQ,MAAM,QAAQ,GAAG;CAC/B,IAAI,UAAU,IACZ,OAAO;EACL,MAAM,MAAM,MAAM,GAAG,KAAK,EAAE,KAAK;EACjC,UAAU;EACV,SAAS,MAAM,MAAM,QAAQ,CAAC,EAAE,KAAK;EACrC,SAAS;EACT;CACF;CAIF,IAAI,MAAM,SAAS,GAAG,GACpB,OAAO;EACL,MAAM,MAAM,MAAM,GAAG,EAAE,EAAE,KAAK;EAC9B,UAAU;EACV,SAAS;EACT;CACF;CAIF,OAAO;EACL,MAAM,MAAM,KAAK;EACjB,UAAU;EACV,SAAS;EACT;CACF;AACF;AAEA,SAAS,YAAY,OAA6B;CAGhD,MAAM,EAAE,OAAO,gBAAgB,iBADT,MAAM,MAAM,CAC0B,CAAC;CAG7D,IAAI;CACJ,IAAI,UAAU;CAEd,MAAM,UAAU,QAAQ,QAAQ,GAAG;CACnC,IAAI,YAAY,IAAI;EAClB,QAAQ,QAAQ,MAAM,GAAG,OAAO,EAAE,KAAK;EACvC,UAAU,QAAQ,MAAM,UAAU,CAAC,EAAE,KAAK;CAC5C;CAGA,IAAI,QAAQ,SAAS,IAAI,GACvB,OAAO;EACL,MAAM,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK;EAChC;EACA,QAAQ;EACR,SAAS;EACT;CACF;CAIF,MAAM,QAAQ,QAAQ,QAAQ,GAAG;CACjC,IAAI,UAAU,IAAI;EAChB,MAAM,OAAO,QAAQ,MAAM,GAAG,KAAK,EAAE,KAAK;EAC1C,MAAM,eAAe,QAAQ,MAAM,QAAQ,CAAC,EAAE,KAAK;EAEnD,OAAO;GACL;GACA;GACA,QAAQ;GACR,SAAS;GACT,SAAS,gBAAgB,KAAA;GACzB;EACF;CACF;CAGA,OAAO;EACL,MAAM,QAAQ,KAAK;EACnB;EACA,QAAQ;EACR,SAAS;EACT;CACF;AACF;AAEA,SAAS,iBAAiB,OAAwD;CAChF,MAAM,WAAW,MAAM,QAAQ,KAAK;CACpC,IAAI,aAAa,IACf,OAAO,EAAE,MAAM;CAGjB,OAAO;EACL,OAAO,MAAM,MAAM,GAAG,QAAQ,EAAE,KAAK;EACrC,aAAa,MAAM,MAAM,WAAW,CAAC,EAAE,KAAK;CAC9C;AACF;;;ACtIO,IAAA,iBAAA,MAAM,eAAiC;CAKc;CAJ1D,2BAAmB,IAAI,IAAkC;CACzD,6BAAqB,IAAI,IAA6B;CACtD,0BAAkB,IAAI,IAAoB;CAE1C,YAAY,WAAoE;EAAtB,KAAA,YAAA;CAAwB;;;;;CAMlF,MAAM,KAAK,MAAc,OAA8C;EACrE,MAAM,eAAe,KAAK,YAAY,IAAI;EAC1C,MAAM,eAAe,KAAK,SAAS,IAAI,YAAY;EAEnD,IAAI,CAAC,cACH,MAAM,IAAI,qBAAqB,IAAI;EAGrC,MAAM,YAAY,KAAK,WAAW,IAAI,YAAY;EAClD,MAAM,cAAc,KAAK,cAAc,SAAS,CAAC,GAAG,SAAS;EAG7D,KAAK,MAAM,OAAO,UAAU,WAC1B,IAAI,IAAI,aAAa,YAAY,IAAI,UAAU,KAAA,KAAa,YAAY,IAAI,UAAU,OACpF,MAAM,IAAI,aAAa,8BAA8B,IAAI,MAAM;EAInE,IAAI;EAEJ,IAAI;GAIF,UAAU,aAAa,EAAE,QAAiB,YAAY;GAEtD,iBAAiB,SAAS,IAAI;GAC9B,iBAAiB,SAAS,WAAW;GAErC,MAAM,WAAW,MAAM,QAAQ,OAAO;GACtC,MAAM,SAAS,iBAAiB,OAAO;GAEvC,IAAI,OAAO,aAAa,UACtB,OAAO;IAAE,GAAG;IAAQ;GAAS;GAG/B,OAAO;EACT,SAAS,OAAO;GACd,IAAI,iBAAiB,cAAc;IACjC,IAAI,SAAS;KACX,MAAM,SAAS,iBAAiB,OAAO;KACvC,OAAO;MACL,UAAU,OAAO,aAAa,IAAI,IAAI,OAAO;MAC7C,QAAQ,OAAO;MACf,QAAQ,CAAC,GAAG,OAAO,QAAQ,MAAM,OAAO;KAC1C;IACF;IACA,OAAO;KAAE,UAAU;KAAG,QAAQ,CAAC;KAAG,QAAQ,CAAC,MAAM,OAAO;IAAE;GAC5D;GAEA,MAAM,eAAe,KAAK,YAAY,OAAO,YAAY;GAEzD,IAAI,SAAS;IACX,MAAM,SAAS,iBAAiB,OAAO;IACvC,OAAO;KACL,UAAU,OAAO,aAAa,IAAI,IAAI,OAAO;KAC7C,QAAQ,OAAO;KACf,QAAQ,CAAC,GAAG,OAAO,QAAQ,YAAY;IACzC;GACF;GACA,OAAO;IAAE,UAAU;IAAG,QAAQ,CAAC;IAAG,QAAQ,CAAC,YAAY;GAAE;EAC3D;CACF;;;;CAKA,IAAI,MAAuB;EACzB,MAAM,WAAW,KAAK,YAAY,IAAI;EACtC,OAAO,KAAK,SAAS,IAAI,QAAQ;CACnC;;;;CAKA,IAAI,MAAgD;EAClD,MAAM,WAAW,KAAK,YAAY,IAAI;EACtC,OAAO,KAAK,SAAS,IAAI,QAAQ;CACnC;;;;CAKA,MAAyC;EACvC,OAAO,IAAI,IAAI,KAAK,QAAQ;CAC9B;;;;CAKA,OAAoE;EAClE,MAAM,SAAsE,CAAC;EAE7E,KAAK,MAAM,CAAC,MAAM,iBAAiB,KAAK,UAAU;GAChD,MAAM,gBAAgB;GACtB,MAAM,iBAA2B,CAAC;GAElC,KAAK,MAAM,CAAC,OAAO,WAAW,KAAK,SACjC,IAAI,WAAW,MACb,eAAe,KAAK,KAAK;GAI7B,OAAO,KAAK;IACV;IACA,aAAa,cAAc;IAC3B,SAAS;GACX,CAAC;EACH;EAEA,OAAO,OAAO,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;CAC3D;;;;CAKA,MAAM,UAAU,SAAkG;EAChH,MAAM,WAAW,KAAK,KAAK;EAG3B,MAAM,EAAE,oBAAoB,MAAM,OAAO,kCAAA,MAAA,MAAA,EAAA,CAAA;EACzC,OAAO,gBAAgB,UAAU,KAAK,YAAY,OAAO;CAC3D;;;;CAKA,MAAM,MAAM,MAA+B;EACzC,MAAM,eAAe,KAAK,YAAY,IAAI;EAC1C,MAAM,eAAe,KAAK,SAAS,IAAI,YAAY;EAEnD,IAAI,CAAC,cACH,MAAM,IAAI,qBAAqB,IAAI;EAGrC,MAAM,YAAY,KAAK,WAAW,IAAI,YAAY;EAClD,MAAM,gBAAgB;EAGtB,MAAM,EAAE,kBAAkB,MAAM,OAAO,kCAAA,MAAA,MAAA,EAAA,CAAA;EACvC,OAAO,cAAc,WAAW,cAAc,WAAW;CAC3D;;;;;CAMA,SAAS,cAA0C;EACjD,MAAM,gBAAgB;EAEtB,IAAI,CAAC,cAAc,SACjB,MAAM,IAAI,aAAa,iBAAiB,aAAa,KAAK,uCAAuC;EAGnG,MAAM,YAAY,eAAe,cAAc,OAAO;EACtD,MAAM,OAAO,UAAU;EAEvB,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK,QAAQ,IAAI,IAAI,GAClD,MAAM,IAAI,aAAa,4BAA4B,KAAK,wBAAwB;EAIlF,IAAI,cAAc;QACX,MAAM,SAAS,cAAc,SAChC,IAAI,KAAK,SAAS,IAAI,KAAK,KAAK,KAAK,QAAQ,IAAI,KAAK,GACpD,MAAM,IAAI,aAAa,qBAAqB,MAAM,8CAA8C;EAAA;EAMtG,KAAK,SAAS,IAAI,MAAM,YAAY;EACpC,KAAK,WAAW,IAAI,MAAM,SAAS;EAEnC,IAAI,cAAc,SAChB,KAAK,MAAM,SAAS,cAAc,SAChC,KAAK,QAAQ,IAAI,OAAO,IAAI;CAGlC;CAEA,YAAoB,OAAgB,aAA6B;EAC/D,MAAM,UAAU,KAAK,UAAU,QAA0B,UAAU,gBAAgB;EACnF,MAAM,MAAM,0BAA0B,WAAW;EAEjD,QAAa,OAAO,OAAO,GAAG;EAC9B,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;CAC9D;CAEA,YAAoB,MAAsB;EACxC,OAAO,KAAK,QAAQ,IAAI,IAAI,KAAK;CACnC;CAEA,cAAsB,OAAqB,WAA0C;EACnF,MAAM,SAAS,EAAE,GAAG,MAAM;EAG1B,KAAK,MAAM,OAAO,UAAU,WAC1B,IAAI,OAAO,IAAI,UAAU,KAAA,KAAa,IAAI,YAAY,KAAA,GACpD,OAAO,IAAI,QAAQ,IAAI;EAK3B,KAAK,MAAM,OAAO,UAAU,SAC1B,IAAI,OAAO,IAAI,UAAU,KAAA;OACnB,IAAI,YAAY,KAAA,GAClB,OAAO,IAAI,QAAQ,IAAI;QAClB,IAAI,IAAI,QACb,OAAO,IAAI,QAAQ;EAAA;EAKzB,OAAO;CACT;AACF;6BApOC,UAAU,UAAU,MAAM,GAAA,gBAAA,GAMZ,OAAO,UAAU,SAAS,CAAA,CAAA,GAAA,cAAA;;;;ACflC,IAAA,eAAA,MAAM,aAAa,CAAE;2BAL3B,OAAO,EACN,WAAW,CACT;CAAE,SAAS,UAAU;CAAQ,UAAU;AAAe,CACxD,EACF,CAAC,CAAA,GAAA,YAAA"}
1
+ {"version":3,"file":"quarry.module-CcGxU2dJ.mjs","names":[],"sources":["../src/quarry/command-internals.ts","../src/quarry/signature-parser.ts","../src/quarry/quarry-registry.ts","../src/quarry/quarry.module.ts"],"sourcesContent":["import type { Command } from './command'\nimport { COMMAND_INTERNALS } from './constants'\nimport type { CommandInput, CommandInternals, CommandResult } from './types'\n\n/** @internal Set the flat input values before calling handle() */\nexport function setCommandInputs(command: Command, values: CommandInput): void {\n command[COMMAND_INTERNALS].inputs = { ...values }\n}\n\n/** @internal Set the Quarry reference for this.call() support */\nexport function setCommandQuarry(\n command: Command,\n quarry: { call(name: string, input?: CommandInput): Promise<CommandResult> },\n): void {\n command[COMMAND_INTERNALS].quarry = quarry\n}\n\n/** @internal Collect the result after handle() completes */\nexport function getCommandResult(command: Command): CommandResult {\n const internals: CommandInternals = command[COMMAND_INTERNALS]\n return {\n exitCode: internals.exitCode,\n output: [...internals.output],\n errors: [...internals.errors],\n }\n}\n\n/** @internal Reset state between invocations */\nexport function resetCommandState(command: Command): void {\n const internals: CommandInternals = command[COMMAND_INTERNALS]\n internals.inputs = {}\n internals.output = []\n internals.errors = []\n internals.exitCode = 0\n}\n","import { CommandError } from './errors/command.error'\nimport type { ParsedArgument, ParsedOption, ParsedSignature } from './types'\n\n/**\n * Parse a Laravel-style command signature string.\n *\n * Signature syntax:\n * command-name {arg} ... — flat command\n * group subcommand {arg} ... — subcommand hierarchy (space-separated)\n * namespace:command {arg} ... — namespaced flat command (colon-separated)\n * {--flag} {--name=} {--name=default} {--name=*} {--A|name} {--name= : desc}\n *\n * Pure function, zero dependencies, edge-compatible.\n */\nexport function parseSignature(signature: string): ParsedSignature {\n const tokens = extractTokens(signature)\n const name = extractCommandName(signature)\n const args: ParsedArgument[] = []\n const options: ParsedOption[] = []\n\n for (const token of tokens) {\n const inner = token.slice(1, -1).trim() // strip { }\n\n if (inner.startsWith('--')) {\n options.push(parseOption(inner))\n } else {\n args.push(parseArgument(inner))\n }\n }\n\n return { name, arguments: args, options }\n}\n\nfunction extractCommandName(signature: string): string {\n const match = /^[\\w:.-]+(?:\\s+[\\w:.-]+)*/.exec(signature)\n if (!match) {\n throw new CommandError(`Invalid signature: cannot extract command name from \"${signature}\"`)\n }\n return match[0]\n}\n\nfunction extractTokens(signature: string): string[] {\n const tokens: string[] = []\n const regex = /\\{[^}]+\\}/g\n let match: RegExpExecArray | null\n\n while ((match = regex.exec(signature)) !== null) {\n tokens.push(match[0])\n }\n\n return tokens\n}\n\nfunction parseArgument(inner: string): ParsedArgument {\n const { value, description } = splitDescription(inner)\n\n // {name*} — array/variadic argument\n if (value.endsWith('*')) {\n return {\n name: value.slice(0, -1).trim(),\n required: true,\n isArray: true,\n description,\n }\n }\n\n // {name=default} — argument with default value\n const eqIdx = value.indexOf('=')\n if (eqIdx !== -1) {\n return {\n name: value.slice(0, eqIdx).trim(),\n required: false,\n default: value.slice(eqIdx + 1).trim(),\n isArray: false,\n description,\n }\n }\n\n // {name?} — optional argument\n if (value.endsWith('?')) {\n return {\n name: value.slice(0, -1).trim(),\n required: false,\n isArray: false,\n description,\n }\n }\n\n // {name} — required argument\n return {\n name: value.trim(),\n required: true,\n isArray: false,\n description,\n }\n}\n\nfunction parseOption(inner: string): ParsedOption {\n // Remove leading --\n const withoutDashes = inner.slice(2)\n const { value, description } = splitDescription(withoutDashes)\n\n // Check for alias: {--A|name...}\n let alias: string | undefined\n let optBody = value\n\n const pipeIdx = optBody.indexOf('|')\n if (pipeIdx !== -1) {\n alias = optBody.slice(0, pipeIdx).trim()\n optBody = optBody.slice(pipeIdx + 1).trim()\n }\n\n // {--name=*} — array option\n if (optBody.endsWith('=*')) {\n return {\n name: optBody.slice(0, -2).trim(),\n alias,\n isFlag: false,\n isArray: true,\n description,\n }\n }\n\n // {--name=default} — option with default value\n const eqIdx = optBody.indexOf('=')\n if (eqIdx !== -1) {\n const name = optBody.slice(0, eqIdx).trim()\n const defaultValue = optBody.slice(eqIdx + 1).trim()\n\n return {\n name,\n alias,\n isFlag: false,\n isArray: false,\n default: defaultValue || undefined,\n description,\n }\n }\n\n // {--flag} — boolean flag\n return {\n name: optBody.trim(),\n alias,\n isFlag: true,\n isArray: false,\n description,\n }\n}\n\nfunction splitDescription(value: string): { value: string; description?: string } {\n const colonIdx = value.indexOf(' : ')\n if (colonIdx === -1) {\n return { value }\n }\n\n return {\n value: value.slice(0, colonIdx).trim(),\n description: value.slice(colonIdx + 3).trim(),\n }\n}\n","import { inject } from '../di'\nimport type { Container } from '../di/container'\nimport { getContainer } from '../di/container-storage'\nimport { Singleton } from '../di/decorators'\nimport { DI_TOKENS } from '../di/tokens'\nimport { createCliExceptionContext } from '../errors/exception-context'\nimport type { ExceptionHandler } from '../errors/exception-handler'\nimport type { Constructor } from '../types'\nimport { type Command } from './command'\nimport { getCommandResult, setCommandInputs, setCommandQuarry } from './command-internals'\nimport { CommandNotFoundError } from './errors/command-not-found.error'\nimport { CommandError } from './errors/command.error'\nimport { parseSignature } from './signature-parser'\nimport type { CommandInput, CommandResult, ParsedSignature, Quarry } from './types'\n\n/**\n * QuarryRegistry — edge-compatible programmatic API for running commands.\n *\n * Registered as a singleton via DI_TOKENS.Quarry.\n * Commands are auto-discovered from module providers and registered at bootstrap.\n * Command constructors are stored at bootstrap; fresh instances are resolved per `call()`.\n *\n * Users should inject and type as `Quarry` (the interface), which only exposes `call()`.\n */\n@Singleton(DI_TOKENS.Quarry)\nexport class QuarryRegistry implements Quarry {\n private commands = new Map<string, Constructor<Command>>()\n private signatures = new Map<string, ParsedSignature>()\n private aliases = new Map<string, string>()\n\n constructor(@inject(DI_TOKENS.Container) private readonly container: Container) { }\n\n /**\n * Execute a command by name with optional flat input.\n * A fresh command instance is resolved from the container per invocation.\n */\n async call(name: string, input?: CommandInput): Promise<CommandResult> {\n const resolvedName = this.resolveName(name)\n const CommandClass = this.commands.get(resolvedName)\n\n if (!CommandClass) {\n throw new CommandNotFoundError(name)\n }\n\n const signature = this.signatures.get(resolvedName)!\n const mergedInput = this.applyDefaults(input ?? {}, signature)\n\n // Validate required arguments\n for (const arg of signature.arguments) {\n if (arg.required && (mergedInput[arg.name] === undefined || mergedInput[arg.name] === null)) {\n throw new CommandError(`Missing required argument: ${arg.name}`)\n }\n }\n\n let command: Command | undefined\n\n try {\n // Resolve from the active request scope (handleCommand runs call() inside\n // runInRequestScope), not the global container — commands may inject\n // request-scoped providers (e.g. @InjectQueue → QueueRegistry).\n command = getContainer().resolve<Command>(CommandClass)\n\n setCommandQuarry(command, this)\n setCommandInputs(command, mergedInput)\n\n const exitCode = await command.handle()\n const result = getCommandResult(command)\n\n if (typeof exitCode === 'number') {\n return { ...result, exitCode }\n }\n\n return result\n } catch (error) {\n if (error instanceof CommandError) {\n if (command) {\n const result = getCommandResult(command)\n return {\n exitCode: result.exitCode === 0 ? 1 : result.exitCode,\n output: result.output,\n errors: [...result.errors, error.message],\n }\n }\n return { exitCode: 1, output: [], errors: [error.message] }\n }\n\n const errorMessage = this.handleError(error, resolvedName)\n\n if (command) {\n const result = getCommandResult(command)\n return {\n exitCode: result.exitCode === 0 ? 1 : result.exitCode,\n output: result.output,\n errors: [...result.errors, errorMessage],\n }\n }\n return { exitCode: 1, output: [], errors: [errorMessage] }\n }\n }\n\n /**\n * Check if a command exists by name or alias.\n */\n has(name: string): boolean {\n const resolved = this.resolveName(name)\n return this.commands.has(resolved)\n }\n\n /**\n * Get a command constructor by name or alias.\n */\n get(name: string): Constructor<Command> | undefined {\n const resolved = this.resolveName(name)\n return this.commands.get(resolved)\n }\n\n /**\n * Get all registered command constructors.\n */\n all(): Map<string, Constructor<Command>> {\n return new Map(this.commands)\n }\n\n /**\n * List all commands with their descriptions and aliases.\n */\n list(): { name: string; description?: string; aliases: string[] }[] {\n const result: { name: string; description?: string; aliases: string[] }[] = []\n\n for (const [name, CommandClass] of this.commands) {\n const staticCommand = CommandClass as unknown as typeof Command\n const commandAliases: string[] = []\n\n for (const [alias, target] of this.aliases) {\n if (target === name) {\n commandAliases.push(alias)\n }\n }\n\n result.push({\n name,\n description: staticCommand.description,\n aliases: commandAliases,\n })\n }\n\n return result.sort((a, b) => a.name.localeCompare(b.name))\n }\n\n /**\n * Generate a compact listing of all commands with visual hierarchy and colors.\n */\n async listUsage(options?: { binaryName?: string; binaryLabel?: string; binaryVersion?: string }): Promise<string> {\n const commands = this.list()\n\n // Dynamic import to keep usage-generator tree-shakeable\n const { generateListing } = await import('./usage-generator')\n return generateListing(commands, this.signatures, options)\n }\n\n /**\n * Get auto-generated usage text for a command.\n */\n async usage(name: string): Promise<string> {\n const resolvedName = this.resolveName(name)\n const CommandClass = this.commands.get(resolvedName)\n\n if (!CommandClass) {\n throw new CommandNotFoundError(name)\n }\n\n const signature = this.signatures.get(resolvedName)!\n const staticCommand = CommandClass as unknown as typeof Command\n\n // Dynamic import to keep usage-generator tree-shakeable\n const { generateUsage } = await import('./usage-generator')\n return generateUsage(signature, staticCommand.description)\n }\n\n /**\n * Register a command constructor with the registry.\n * @internal Called by Application during bootstrap.\n */\n register(commandClass: Constructor<Command>): void {\n const staticCommand = commandClass as unknown as typeof Command\n\n if (!staticCommand.command) {\n throw new CommandError(`Command class ${commandClass.name} is missing static \"command\" signature`)\n }\n\n const signature = parseSignature(staticCommand.command)\n const name = signature.name\n\n if (this.commands.has(name) || this.aliases.has(name)) {\n throw new CommandError(`Duplicate command name: \"${name}\" is already registered`)\n }\n\n // Validate all aliases before any mutation\n if (staticCommand.aliases) {\n for (const alias of staticCommand.aliases) {\n if (this.commands.has(alias) || this.aliases.has(alias)) {\n throw new CommandError(`Duplicate alias: \"${alias}\" conflicts with an existing command or alias`)\n }\n }\n }\n\n // All checks passed — safe to mutate\n this.commands.set(name, commandClass)\n this.signatures.set(name, signature)\n\n if (staticCommand.aliases) {\n for (const alias of staticCommand.aliases) {\n this.aliases.set(alias, name)\n }\n }\n }\n\n private handleError(error: unknown, commandName: string): string {\n const handler = this.container.resolve<ExceptionHandler>(DI_TOKENS.ExceptionHandler)\n const ctx = createCliExceptionContext(commandName)\n // Fire-and-forget — reporting happens via waitUntil internally\n void handler.handle(error, ctx)\n return error instanceof Error ? error.message : String(error)\n }\n\n private resolveName(name: string): string {\n return this.aliases.get(name) ?? name\n }\n\n private applyDefaults(input: CommandInput, signature: ParsedSignature): CommandInput {\n const result = { ...input }\n\n // Apply argument defaults\n for (const arg of signature.arguments) {\n if (result[arg.name] === undefined && arg.default !== undefined) {\n result[arg.name] = arg.default\n }\n }\n\n // Apply option defaults\n for (const opt of signature.options) {\n if (result[opt.name] === undefined) {\n if (opt.default !== undefined) {\n result[opt.name] = opt.default\n } else if (opt.isFlag) {\n result[opt.name] = false\n }\n }\n }\n\n return result\n }\n}\n","import { DI_TOKENS } from '../di/tokens'\nimport { Module } from '../module/module.decorator'\nimport { QuarryRegistry } from './quarry-registry'\n\n/**\n * Registers the Quarry command registry (`DI_TOKENS.Quarry`).\n *\n * Eager: resolved synchronously at bootstrap (`registerCommands`) and by the\n * CLI runner (`bin/quarry.ts`), so it cannot be lazily loaded.\n */\n@Module({\n providers: [\n { provide: DI_TOKENS.Quarry, useClass: QuarryRegistry },\n ],\n})\nexport class QuarryModule { }\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  import { a as QueueBinding, i as QueueMessage, n as ConsumerRegistry, r as IQueueConsumer, t as ConsumerRegistration } from "../consumer-registry-D3iMTSdy.mjs";
2
- import { _ as QueueStore, a as FailedJobCleanupJob, b as QueueModule, c as QueueProviderFactory, d as CloudflareQueueProvider, f as IQueueProvider, g as DEFAULT_STORE_BINDING, h as QueueManager, i as InjectQueue, l as QueueSender, m as IQueueSender, n as QUEUE_TOKENS, o as failedJobCleanupJob, p as DispatchMessage, r as QueueToken, s as QueueRegistry, t as QueueError, u as SyncQueueProvider, v as FailedJob, x as QueueModuleOptions, y as FailedJobMetadata } from "../index-B5JBRcWD.mjs";
2
+ import { _ as QueueStore, a as FailedJobCleanupJob, b as QueueModule, c as QueueProviderFactory, d as CloudflareQueueProvider, f as IQueueProvider, g as DEFAULT_STORE_BINDING, h as QueueManager, i as InjectQueue, l as QueueSender, m as IQueueSender, n as QUEUE_TOKENS, o as failedJobCleanupJob, p as DispatchMessage, r as QueueToken, s as QueueRegistry, t as QueueError, u as SyncQueueProvider, v as FailedJob, x as QueueModuleOptions, y as FailedJobMetadata } from "../index-CNuFQSNj.mjs";
3
3
  export { CloudflareQueueProvider, ConsumerRegistration, ConsumerRegistry, DEFAULT_STORE_BINDING, DispatchMessage, FailedJob, FailedJobCleanupJob, FailedJobMetadata, IQueueConsumer, IQueueProvider, IQueueSender, InjectQueue, QUEUE_TOKENS, QueueBinding, QueueError, QueueManager, QueueMessage, QueueModule, QueueModuleOptions, QueueProviderFactory, QueueRegistry, QueueSender, QueueStore, QueueToken, SyncQueueProvider, failedJobCleanupJob };
@@ -1,7 +1,7 @@
1
- import { c as Transient, d as inject } from "../di-DseMn-z9.mjs";
1
+ import { p as inject, u as Transient } from "../di-D7qmrAir.mjs";
2
2
  import { n as __decorateParam, t as __decorate } from "../decorate-CuAoSZvs.mjs";
3
3
  import { LOGGER_TOKENS } from "../logger/index.mjs";
4
- import { a as CloudflareQueueProvider, c as QueueRegistry, d as QueueManager, f as QueueError, i as SyncQueueProvider, l as QueueSender, o as DEFAULT_STORE_BINDING, r as QueueProviderFactory, s as QueueStore, t as QueueModule, u as ConsumerRegistry } from "../queue.module-nddvxzCB.mjs";
4
+ import { a as CloudflareQueueProvider, c as QueueRegistry, d as QueueManager, f as QueueError, i as SyncQueueProvider, l as QueueSender, o as DEFAULT_STORE_BINDING, r as QueueProviderFactory, s as QueueStore, t as QueueModule, u as ConsumerRegistry } from "../queue.module-CEs4_kEM.mjs";
5
5
  import { t as QUEUE_TOKENS } from "../queue.tokens-DjHnFmre.mjs";
6
6
  //#region src/queue/jobs/failed-job-cleanup.job.ts
7
7
  /** Retention applied when `failedJobs.retention` is not configured. */
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./chunk-BBjsoOtd.mjs";
2
- import { c as Transient, d as inject, o as Request, r as DI_TOKENS, s as Singleton } from "./di-DseMn-z9.mjs";
2
+ import { c as Request, l as Singleton, p as inject, r as DI_TOKENS, u as Transient } from "./di-D7qmrAir.mjs";
3
3
  import { a as ApplicationError, n as getContainer, t as containerStorage } from "./container-storage-BmOJ4_Na.mjs";
4
4
  import { n as __decorateParam, t as __decorate } from "./decorate-CuAoSZvs.mjs";
5
5
  import { LOGGER_TOKENS } from "./logger/index.mjs";
6
- import "./errors-mXYxG0XB.mjs";
6
+ import "./errors-C01O2T-n.mjs";
7
7
  import { n as Module } from "./module.decorator-CYHY6pG5.mjs";
8
8
  import "./module/index.mjs";
9
9
  import { CACHE_TOKENS, CacheModule } from "./cache/index.mjs";
@@ -398,9 +398,11 @@ CloudflareQueueProvider = __decorate([Transient(), __decorateParam(0, inject(DI_
398
398
  let SyncQueueProvider = class SyncQueueProvider {
399
399
  registry;
400
400
  root;
401
- constructor(registry, root) {
401
+ app;
402
+ constructor(registry, root, app) {
402
403
  this.registry = registry;
403
404
  this.root = root;
405
+ this.app = app;
404
406
  }
405
407
  /**
406
408
  * Process a message synchronously.
@@ -416,6 +418,7 @@ let SyncQueueProvider = class SyncQueueProvider {
416
418
  * @throws Re-throws any error from consumer.handle() after calling onError()
417
419
  */
418
420
  async send(_binding, message) {
421
+ await this.app.ensureScopedHandlers();
419
422
  const ambient = containerStorage.getStore();
420
423
  if (ambient) {
421
424
  await this.process(ambient, message);
@@ -446,7 +449,8 @@ let SyncQueueProvider = class SyncQueueProvider {
446
449
  SyncQueueProvider = __decorate([
447
450
  Transient(),
448
451
  __decorateParam(0, inject(DI_TOKENS.ConsumerRegistry)),
449
- __decorateParam(1, inject(DI_TOKENS.Container))
452
+ __decorateParam(1, inject(DI_TOKENS.Container)),
453
+ __decorateParam(2, inject(DI_TOKENS.Application))
450
454
  ], SyncQueueProvider);
451
455
  //#endregion
452
456
  //#region src/queue/services/queue-provider-factory.ts
@@ -454,11 +458,13 @@ let QueueProviderFactory = class QueueProviderFactory {
454
458
  env;
455
459
  registry;
456
460
  container;
461
+ app;
457
462
  options;
458
- constructor(env, registry, container, options) {
463
+ constructor(env, registry, container, app, options) {
459
464
  this.env = env;
460
465
  this.registry = registry;
461
466
  this.container = container;
467
+ this.app = app;
462
468
  this.options = options;
463
469
  }
464
470
  /**
@@ -471,7 +477,7 @@ let QueueProviderFactory = class QueueProviderFactory {
471
477
  const providerType = this.options?.provider ?? "cloudflare";
472
478
  switch (providerType) {
473
479
  case "cloudflare": return new CloudflareQueueProvider(this.env);
474
- case "sync": return new SyncQueueProvider(this.registry, this.container);
480
+ case "sync": return new SyncQueueProvider(this.registry, this.container, this.app);
475
481
  default: throw new QueueError(`Queue provider "${String(providerType)}" is not supported`);
476
482
  }
477
483
  }
@@ -481,7 +487,8 @@ QueueProviderFactory = __decorate([
481
487
  __decorateParam(0, inject(DI_TOKENS.CloudflareEnv)),
482
488
  __decorateParam(1, inject(DI_TOKENS.ConsumerRegistry)),
483
489
  __decorateParam(2, inject(DI_TOKENS.Container)),
484
- __decorateParam(3, inject(QUEUE_TOKENS.QueueModuleOptions, { isOptional: true }))
490
+ __decorateParam(3, inject(DI_TOKENS.Application)),
491
+ __decorateParam(4, inject(QUEUE_TOKENS.QueueModuleOptions, { isOptional: true }))
485
492
  ], QueueProviderFactory);
486
493
  //#endregion
487
494
  //#region src/queue/queue.module.ts
@@ -610,4 +617,4 @@ QueueModule = _QueueModule = __decorate([Module({
610
617
  //#endregion
611
618
  export { CloudflareQueueProvider as a, QueueRegistry as c, QueueManager as d, QueueError as f, SyncQueueProvider as i, QueueSender as l, queue_module_exports as n, DEFAULT_STORE_BINDING as o, QueueProviderFactory as r, QueueStore as s, QueueModule as t, ConsumerRegistry as u };
612
619
 
613
- //# sourceMappingURL=queue.module-nddvxzCB.mjs.map
620
+ //# sourceMappingURL=queue.module-CEs4_kEM.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"queue.module-nddvxzCB.mjs","names":[],"sources":["../src/queue/queue.error.ts","../src/queue/queue-manager.ts","../src/queue/consumer-registry.ts","../src/queue/queue-sender.ts","../src/queue/queue-registry.ts","../src/queue/queue-store.ts","../src/queue/providers/cloudflare-queue.provider.ts","../src/queue/providers/sync-queue.provider.ts","../src/queue/services/queue-provider-factory.ts","../src/queue/queue.module.ts"],"sourcesContent":["import { ApplicationError } from '../errors'\n\nexport class QueueError extends ApplicationError {}\n","import { inject } from '../di'\nimport { getContainer } from '../di/container-storage'\nimport { Transient } from '../di/decorators'\nimport { DI_TOKENS } from '../di/tokens'\nimport { LOGGER_TOKENS, type LoggerService } from '../logger'\nimport { type ConsumerRegistry } from './consumer-registry'\nimport type { FailedJob } from './failed-job'\nimport type { IQueueConsumer, QueueMessage } from './queue-consumer'\nimport { QueueError } from './queue.error'\nimport type { QueueModuleOptions } from './queue.module'\nimport type { QueueStore } from './queue-store'\nimport { QUEUE_TOKENS } from './queue.tokens'\n\nconst DEFAULT_MAX_RETRIES = 3\n\n@Transient(DI_TOKENS.Queue)\nexport class QueueManager {\n private readonly maxRetries: number\n\n constructor(\n @inject(DI_TOKENS.ConsumerRegistry) private readonly registry: ConsumerRegistry,\n @inject(LOGGER_TOKENS.LoggerService) private readonly logger: LoggerService,\n @inject(QUEUE_TOKENS.QueueStore) private readonly store: QueueStore,\n @inject(QUEUE_TOKENS.QueueModuleOptions) options: QueueModuleOptions,\n ) {\n this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES\n }\n\n async processBatch(queueName: string, batch: MessageBatch): Promise<void> {\n for (const message of batch.messages) {\n const queueMessage = message.body as QueueMessage\n const idempotencyKey = queueMessage.metadata?.idempotencyKey ?? queueMessage.id\n\n if (await this.store.isProcessed(idempotencyKey)) {\n message.ack()\n continue\n }\n\n // Resolve a fresh consumer instance per message from the active request\n // scope (handleQueue runs processBatch inside runInRequestScope), so\n // request-scoped consumer dependencies bind to this message's scope.\n const container = getContainer()\n const consumers = this.registry\n .getConsumerClasses(queueMessage.type)\n .map((ConsumerClass) => container.resolve<IQueueConsumer>(ConsumerClass))\n\n const results = await Promise.allSettled(\n consumers.map((consumer) => consumer.handle(queueMessage)),\n )\n\n let lastError: Error | undefined\n let failedConsumer: string | undefined\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const consumer = consumers[i]\n const errorInstance = result.reason instanceof Error\n ? result.reason\n : new Error(String(result.reason))\n\n this.logger.error('Queue message processing failed', errorInstance, {\n type: queueMessage.type,\n queue: queueName,\n messageId: queueMessage.id,\n idempotencyKey,\n })\n\n if (consumer.onError) {\n await consumer.onError(errorInstance, queueMessage)\n }\n\n lastError = errorInstance\n failedConsumer = consumer.constructor.name\n }\n }\n\n const failed = lastError !== undefined\n\n if (failed) {\n // `message.attempts` is 1-based (the first delivery is attempt 1), so a\n // message is only out of retries once it has been delivered more than\n // `maxRetries` times. `maxRetries: 3` therefore means 3 retries after the\n // initial delivery (4 total attempts). This must be <= the consumer's\n // `max_retries` in wrangler.jsonc, otherwise Cloudflare dead-letters the\n // message before this branch ever runs and it never reaches the store.\n if (message.attempts > this.maxRetries) {\n // A KV failure (or a message dispatched outside Stratal, with no\n // binding to retry through) while persisting the failed job must not\n // abort the rest of the batch — log and ack so the message isn't\n // redelivered forever.\n try {\n const binding = queueMessage.metadata?.binding\n if (!binding) {\n throw new QueueError(\n `Queue message ${queueMessage.id} has no binding metadata and cannot be recorded for retry. ` +\n `Messages must be dispatched through a Stratal queue sender (@InjectQueue).`,\n )\n }\n\n const failedJob: FailedJob = {\n id: queueMessage.id,\n queue: queueName,\n binding,\n type: queueMessage.type,\n message: queueMessage,\n error: {\n name: lastError!.name,\n message: lastError!.message,\n stack: lastError!.stack,\n },\n consumer: failedConsumer!,\n attempts: message.attempts,\n failedAt: new Date().toISOString(),\n }\n\n await this.store.storeFailedJob(failedJob)\n } catch (error) {\n this.logger.error(\n 'Failed to persist failed queue job',\n error instanceof Error ? error : new Error(String(error)),\n { queue: queueName, messageId: queueMessage.id, type: queueMessage.type },\n )\n }\n message.ack()\n } else {\n message.retry()\n }\n } else {\n await this.store.markProcessed(idempotencyKey)\n message.ack()\n }\n }\n }\n}\n","import { Singleton } from '../di/decorators'\nimport { DI_TOKENS } from '../di/tokens'\nimport type { Constructor } from '../types'\nimport type { IQueueConsumer } from './queue-consumer'\n\n/** A registered consumer class together with the message types it handles. */\nexport interface ConsumerRegistration {\n consumerClass: Constructor<IQueueConsumer>\n messageTypes: string[]\n}\n\n/**\n * Consumer Registry\n *\n * Singleton service that holds all registered queue consumers indexed by message type.\n * Consumers declare the message types they handle, and this registry routes messages\n * to the appropriate consumers based on their types.\n *\n * **Message-Type Routing:**\n * - Consumers declare `messageTypes` array (e.g., `['email.send', 'email.batch.send']`)\n * - When a message arrives, consumers matching the message type are invoked\n * - A consumer can handle messages from ANY queue (routing is by type, not queue)\n * - Use `'*'` as a wildcard to handle all message types\n *\n * @example Consumer registration\n * ```typescript\n * // In consumer.ts\n * @Transient()\n * export class EmailConsumer implements IQueueConsumer {\n * readonly messageTypes = ['email.send', 'email.batch.send']\n * // ...\n * }\n *\n * // In module.ts\n * @Module({\n * consumers: [EmailConsumer]\n * })\n *\n * // Application auto-registers via ConsumerRegistry\n * this.consumerRegistry.register(consumer)\n * ```\n */\n@Singleton(DI_TOKENS.ConsumerRegistry)\nexport class ConsumerRegistry {\n /** Map from message type to consumer classes handling that type */\n private classesByType = new Map<string, Constructor<IQueueConsumer>[]>()\n\n /** All registrations (for iteration / listing) */\n private registrations: ConsumerRegistration[] = []\n\n /** Registered consumer classes (dedupe) */\n private registeredClasses = new Set<Constructor<IQueueConsumer>>()\n\n /**\n * Register a queue consumer class.\n *\n * Indexes the class by each of its declared message types. A fresh instance\n * is resolved per message from the request-scoped container at dispatch time —\n * consumers are never held as long-lived singletons, so they may safely inject\n * request-scoped providers (`@InjectQueue`, i18n, auth context, …).\n *\n * @param consumerClass - Queue consumer class to register\n * @param messageTypes - Message types the consumer handles\n */\n register(consumerClass: Constructor<IQueueConsumer>, messageTypes: string[]): void {\n if (this.registeredClasses.has(consumerClass)) {\n return // Already registered\n }\n\n this.registeredClasses.add(consumerClass)\n this.registrations.push({ consumerClass, messageTypes })\n\n for (const messageType of messageTypes) {\n const existing = this.classesByType.get(messageType) ?? []\n existing.push(consumerClass)\n this.classesByType.set(messageType, existing)\n }\n }\n\n /**\n * Get all consumer classes that can handle a specific message type.\n *\n * Returns classes that either declare the message type explicitly or use the\n * `'*'` wildcard. Callers resolve a fresh instance per message from the\n * request-scoped container.\n *\n * @param messageType - The message type to find consumers for\n * @returns Array of consumer classes that can handle this message type\n */\n getConsumerClasses(messageType: string): Constructor<IQueueConsumer>[] {\n const exactMatch = this.classesByType.get(messageType) ?? []\n const wildcardMatch = this.classesByType.get('*') ?? []\n\n // Combine and dedupe\n return Array.from(new Set([...exactMatch, ...wildcardMatch]))\n }\n\n /**\n * Check if any consumers can handle a message type\n *\n * @param messageType - The message type to check\n * @returns true if at least one consumer can handle this type\n */\n hasConsumers(messageType: string): boolean {\n return this.getConsumerClasses(messageType).length > 0\n }\n\n /**\n * Get all registered message types\n *\n * @returns Array of message types with registered consumers\n */\n getMessageTypes(): string[] {\n return Array.from(this.classesByType.keys())\n }\n\n /**\n * Get all consumer registrations (class + message types) for listing.\n */\n getRegistrations(): readonly ConsumerRegistration[] {\n return this.registrations\n }\n}\n","import type { II18nService } from '../i18n/i18n.types';\nimport type { IQueueProvider } from './providers';\nimport type { QueueMessage } from './queue-consumer';\nimport type { DispatchMessage, IQueueSender } from './queue-sender.interface';\n\n/**\n * Queue Sender\n *\n * Implementation of IQueueSender bound to a specific queue binding.\n * Created by QueueRegistry for each registered binding.\n *\n * Automatically enriches messages with:\n * - `id`: UUID generated via crypto.randomUUID()\n * - `metadata.locale`: Current locale from I18n context\n * - `metadata.idempotencyKey`: Deterministic SHA-256 hash of type + payload (if not provided)\n *\n * @example\n * ```typescript\n * // Created by QueueRegistry, not directly instantiated\n * const sender = registry.getQueue('NOTIFICATIONS_QUEUE')\n *\n * await sender.dispatch({\n * type: 'email.send',\n * payload: { to: 'user@example.com', subject: 'Hello' }\n * })\n * ```\n */\nexport class QueueSender implements IQueueSender {\n constructor(\n private readonly binding: string,\n private readonly provider: IQueueProvider,\n private readonly i18n: II18nService\n ) {}\n\n /**\n * Dispatch a message to this queue.\n *\n * @param message - Message to dispatch (without id)\n */\n async dispatch<T>(message: DispatchMessage<T>): Promise<void> {\n const metadata = { ...message.metadata }\n\n if (!metadata.locale) {\n const locale = this.i18n.getLocale()\n if (locale) {\n metadata.locale = locale\n }\n }\n\n metadata.idempotencyKey ??= await this.generateIdempotencyKey(message.type, message.payload);\n\n // Stamp the producer binding so a failed job can be retried to the right\n // Cloudflare binding. Consumers only see the queue *name*, which is not a\n // valid producer binding key.\n metadata.binding = this.binding\n\n const fullMessage: QueueMessage<T> = {\n id: crypto.randomUUID(),\n ...message,\n metadata,\n }\n\n await this.provider.send(this.binding, fullMessage)\n }\n\n private async generateIdempotencyKey(type: string, payload: unknown): Promise<string> {\n // Use a stable, key-sorted serialization: `JSON.stringify` preserves\n // insertion order, so two semantically identical payloads with differently\n // ordered keys would otherwise hash differently and defeat deduplication.\n const data = new TextEncoder().encode(stableStringify({ type, payload }))\n const hash = await crypto.subtle.digest('SHA-256', data)\n return `queue:${Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('')}`\n }\n}\n\n/**\n * Deterministic JSON serialization: object keys are emitted in sorted order at\n * every level so the output depends only on the data, not on key insertion\n * order. Arrays keep their order (order is significant). Used to derive a stable\n * idempotency hash from a message's `type` + `payload`.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value) ?? 'null'\n }\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(',')}]`\n }\n const entries = Object.keys(value as Record<string, unknown>)\n .sort()\n .map((key) => {\n const v = (value as Record<string, unknown>)[key]\n if (v === undefined) return undefined\n return `${JSON.stringify(key)}:${stableStringify(v)}`\n })\n .filter((entry): entry is string => entry !== undefined)\n return `{${entries.join(',')}}`\n}\n","import { inject } from '../di'\nimport { Request } from '../di/decorators'\nimport { I18N_TOKENS } from '../i18n/i18n.tokens'\nimport type { II18nService } from '../i18n/i18n.types'\nimport type { IQueueProvider } from './providers'\nimport type { IQueueSender } from './queue-sender.interface'\nimport { QueueSender } from './queue-sender'\nimport { QUEUE_TOKENS } from './queue.tokens'\nimport { type QueueProviderFactory } from './services'\n\n/**\n * Queue Registry\n *\n * Request-scoped factory service for creating QueueSender instances.\n * Caches senders per binding within the request scope.\n *\n * This service is used internally by QueueModule.registerQueue() to provide\n * IQueueSender instances for each registered binding.\n *\n * **Why request-scoped?**\n * - Needs access to I18nService for locale-aware message metadata\n * - Provider is created once per request for consistency\n * - Queue senders are cached per request to avoid recreating them\n *\n * @example\n * ```typescript\n * // Used internally by QueueModule.registerQueue()\n * QueueModule.registerQueue('NOTIFICATIONS_QUEUE')\n *\n * // The module creates a factory provider:\n * {\n * provide: 'NOTIFICATIONS_QUEUE',\n * useFactory: (registry: QueueRegistry) => registry.getQueue('NOTIFICATIONS_QUEUE'),\n * inject: [QUEUE_TOKENS.QueueRegistry],\n * }\n * ```\n */\n@Request(QUEUE_TOKENS.QueueRegistry)\nexport class QueueRegistry {\n private readonly provider: IQueueProvider\n private readonly senders = new Map<string, IQueueSender>()\n\n constructor(\n @inject(QUEUE_TOKENS.QueueProviderFactory) providerFactory: QueueProviderFactory,\n @inject(I18N_TOKENS.I18nService) private readonly i18n: II18nService\n ) {\n this.provider = providerFactory.create()\n }\n\n /**\n * Get or create a QueueSender for the specified binding.\n *\n * Senders are cached per binding within the request scope.\n *\n * @param binding - The queue binding to get a sender for\n * @returns QueueSender bound to the specified binding\n */\n getQueue(binding: string): IQueueSender {\n let sender = this.senders.get(binding)\n\n if (!sender) {\n sender = new QueueSender(binding, this.provider, this.i18n)\n this.senders.set(binding, sender)\n }\n\n return sender\n }\n}\n","import { inject } from '../di';\nimport { Transient } from '../di/decorators';\nimport { CACHE_TOKENS } from '../cache/cache.tokens';\nimport type { TieredCacheService } from '../cache/services/tiered-cache.service';\nimport type { FailedJob, FailedJobMetadata } from './failed-job';\nimport type { QueueModuleOptions } from './queue.module';\nimport { QUEUE_TOKENS } from './queue.tokens';\n\nconst IDEM_PREFIX = 'queue:idem:'\nconst FAILED_PREFIX = 'queue:failed:'\nconst DEFAULT_IDEMPOTENCY_TTL = 86400\n\n/** Default KV binding name used for queue state when none is configured. */\nexport const DEFAULT_STORE_BINDING = 'CACHE'\n\n/**\n * Persists queue idempotency claims and failed jobs.\n *\n * Backed by {@link TieredCacheService} (isolate-local L1 + KV). The L1 is what\n * makes `markProcessed` → `isProcessed` reliable within an isolate: a claim\n * written on this isolate is read back from memory, so a message redelivered to\n * the same warm isolate is de-duplicated even inside KV's eventual-consistency\n * window. Idempotency markers are set-once, the pattern the L1 tier is designed\n * for. Cross-isolate duplicates still rely on KV (eventually consistent) —\n * delivery remains at-least-once with best-effort de-duplication, not\n * exactly-once.\n */\n@Transient(QUEUE_TOKENS.QueueStore)\nexport class QueueStore {\n private readonly cache: TieredCacheService\n private readonly idempotencyTtl: number\n\n constructor(\n @inject(CACHE_TOKENS.TieredCacheService) cache: TieredCacheService,\n @inject(QUEUE_TOKENS.QueueModuleOptions) options: QueueModuleOptions,\n ) {\n // Bind to the configured KV namespace via the tiered cache so the L1 lives\n // on its singleton (one per isolate) and persists across messages. The\n // binding is validated at module init (QueueModule.onInitialize);\n // `binding()` also throws if it is somehow absent here.\n this.cache = cache.binding(options.store?.binding ?? DEFAULT_STORE_BINDING)\n this.idempotencyTtl = options.idempotency?.ttl ?? DEFAULT_IDEMPOTENCY_TTL\n }\n\n async isProcessed(key: string): Promise<boolean> {\n return (await this.cache.get(`${IDEM_PREFIX}${key}`)) !== null\n }\n\n async markProcessed(key: string): Promise<void> {\n await this.cache.put(`${IDEM_PREFIX}${key}`, '1', {\n expirationTtl: this.idempotencyTtl,\n })\n }\n\n async storeFailedJob(job: FailedJob): Promise<void> {\n const metadata: FailedJobMetadata = {\n queue: job.queue,\n binding: job.binding,\n type: job.type,\n consumer: job.consumer,\n attempts: job.attempts,\n failedAt: job.failedAt,\n }\n\n await this.cache.put(`${FAILED_PREFIX}${job.id}`, JSON.stringify(job), {\n metadata,\n })\n }\n\n async getFailedJob(messageId: string): Promise<FailedJob | null> {\n return this.cache.get<FailedJob>(`${FAILED_PREFIX}${messageId}`, 'json')\n }\n\n async removeFailedJob(messageId: string): Promise<void> {\n await this.cache.delete(`${FAILED_PREFIX}${messageId}`)\n }\n\n async listFailedJobs(options?: {\n limit?: number\n cursor?: string\n }): Promise<{ keys: { id: string; metadata: FailedJobMetadata }[]; cursor?: string }> {\n const result = await this.cache.list<FailedJobMetadata>({\n prefix: FAILED_PREFIX,\n limit: options?.limit ?? 50,\n cursor: options?.cursor,\n })\n\n // Skip keys without metadata: a partially-written entry (or one written by\n // an older code path) is unusable for listing and must not crash the page.\n const keys = result.keys\n .filter((key): key is typeof key & { metadata: FailedJobMetadata } => key.metadata != null)\n .map((key) => ({\n id: key.name.slice(FAILED_PREFIX.length),\n metadata: key.metadata,\n }))\n\n return {\n keys,\n cursor: result.list_complete ? undefined : result.cursor,\n }\n }\n\n async purgeFailedJobs(): Promise<void> {\n let cursor: string | undefined\n\n do {\n const result = await this.cache.list({ prefix: FAILED_PREFIX, cursor })\n await Promise.all(result.keys.map((key) => this.cache.delete(key.name)))\n cursor = result.list_complete ? undefined : result.cursor\n } while (cursor)\n }\n\n /**\n * Delete failed jobs older than `retentionSeconds` (by their `failedAt`\n * timestamp). Returns the number removed. Backs the opt-in\n * {@link FailedJobCleanupJob} cron — failed jobs otherwise persist\n * indefinitely until retried or purged.\n */\n async purgeFailedJobsOlderThan(retentionSeconds: number): Promise<number> {\n const cutoff = Date.now() - retentionSeconds * 1000\n let cursor: string | undefined\n let removed = 0\n\n do {\n const result = await this.cache.list<FailedJobMetadata>({ prefix: FAILED_PREFIX, cursor })\n const expired = result.keys.filter((key) => {\n const failedAt = key.metadata?.failedAt\n return failedAt !== undefined && Date.parse(failedAt) < cutoff\n })\n await Promise.all(expired.map((key) => this.cache.delete(key.name)))\n removed += expired.length\n cursor = result.list_complete ? undefined : result.cursor\n } while (cursor)\n\n return removed\n }\n}\n","import { inject } from '../../di'\nimport { type StratalEnv } from '../../env'\nimport { Transient } from '../../di/decorators'\nimport { DI_TOKENS } from '../../di/tokens'\nimport { QueueError } from '../queue.error'\nimport type { QueueMessage } from '../queue-consumer'\nimport type { IQueueProvider } from './queue-provider.interface'\n\n/**\n * Cloudflare Queue Provider\n *\n * Sends messages to Cloudflare Queues by resolving the binding directly on\n * the worker's `env`. Used in production environments where Cloudflare Workers\n * handle queue processing.\n *\n * @example\n * ```typescript\n * const provider = new CloudflareQueueProvider(env)\n * await provider.send('NOTIFICATIONS_QUEUE', message)\n * ```\n */\n@Transient()\nexport class CloudflareQueueProvider implements IQueueProvider {\n constructor(\n @inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv\n ) { }\n\n /**\n * Send a message to a Cloudflare Queue\n *\n * @param binding - Queue binding identifier (e.g., 'NOTIFICATIONS_QUEUE')\n * @param message - Complete message with id and payload\n * @throws {QueueError} If the binding is not configured on env\n */\n async send<T>(binding: string, message: QueueMessage<T>): Promise<void> {\n const queue = (this.env as unknown as Record<string, unknown>)[binding] as Queue | undefined\n\n if (!queue) {\n throw new QueueError(`Queue binding \"${binding}\" was not found in the environment`)\n }\n\n await queue.send(message)\n }\n}\n","import type { Container } from '../../di/container'\nimport { containerStorage } from '../../di/container-storage'\nimport { inject } from '../../di'\nimport { Transient } from '../../di/decorators'\nimport { DI_TOKENS } from '../../di/tokens'\nimport type { RouterContext } from '../../router/router-context'\nimport { type ConsumerRegistry } from '../consumer-registry'\nimport type { IQueueConsumer, QueueMessage } from '../queue-consumer'\nimport type { IQueueProvider } from './queue-provider.interface'\n\n/**\n * Sync Queue Provider\n *\n * Processes messages immediately by finding matching consumers and calling\n * their handle() method directly. Used for testing and development where\n * real queue infrastructure is not available.\n *\n * **Behavior:**\n * - Messages are processed synchronously when send() is called\n * - Matching consumers are found via ConsumerRegistry by message type\n * - All matching consumers are called sequentially\n * - Errors are re-thrown after onError() is called (fail-fast for testing)\n *\n * **Consumer Matching:**\n * - Consumers are matched by message type, not queue name\n * - Wildcard ('*') matches all message types\n */\n@Transient()\nexport class SyncQueueProvider implements IQueueProvider {\n constructor(\n @inject(DI_TOKENS.ConsumerRegistry) private readonly registry: ConsumerRegistry,\n @inject(DI_TOKENS.Container) private readonly root: Container,\n ) {}\n\n /**\n * Process a message synchronously.\n *\n * Runs inside the active request scope when dispatch happens within one (an\n * HTTP request, or `runInScope` for queues/cron/commands). When dispatched\n * with no ambient scope — e.g. a service invoked directly in a test — it\n * establishes its own request scope (mirroring the production queue handler)\n * so consumers and their request-scoped dependencies resolve correctly.\n *\n * @param _binding - Queue binding (not used for routing, consumers match by message type)\n * @param message - Complete message with id and payload\n * @throws Re-throws any error from consumer.handle() after calling onError()\n */\n async send<T>(_binding: string, message: QueueMessage<T>): Promise<void> {\n const ambient = containerStorage.getStore()\n if (ambient) {\n await this.process(ambient, message)\n return\n }\n\n const locale = message.metadata?.locale ?? 'en'\n const context = {\n getLocale: () => locale,\n setLocale: () => { /* no-op */ },\n getContainer: () => containerStorage.getStore() ?? this.root,\n } as unknown as RouterContext\n\n await this.root.runInRequestScope(context, (container) => this.process(container, message))\n }\n\n /**\n * Resolve a fresh consumer per message from `container` (matched by type) and\n * invoke each sequentially, fail-fast on the first error after `onError`.\n */\n private async process<T>(container: Container, message: QueueMessage<T>): Promise<void> {\n const consumers = this.registry\n .getConsumerClasses(message.type)\n .map((ConsumerClass) => container.resolve<IQueueConsumer>(ConsumerClass))\n\n for (const consumer of consumers) {\n try {\n await consumer.handle(message)\n } catch (error) {\n const errorInstance = error instanceof Error ? error : new Error(String(error))\n\n // Call onError hook if defined\n if (consumer.onError) {\n await consumer.onError(errorInstance, message)\n }\n\n // Re-throw for fail-fast behavior in tests\n throw errorInstance\n }\n }\n }\n}\n","import { inject } from '../../di'\nimport type { Container } from '../../di/container'\nimport { type StratalEnv } from '../../env'\nimport { Transient } from '../../di/decorators'\nimport { DI_TOKENS } from '../../di/tokens'\nimport { type ConsumerRegistry } from '../consumer-registry'\nimport { QueueError } from '../queue.error'\nimport { CloudflareQueueProvider, SyncQueueProvider, type IQueueProvider } from '../providers'\nimport type { QueueModuleOptions } from '../queue.module'\nimport { QUEUE_TOKENS } from '../queue.tokens'\n\n/**\n * Queue Provider Factory\n *\n * Creates the appropriate queue provider based on configuration provided\n * via QueueModule.forRootAsync().\n *\n * **Provider Selection:**\n * - `cloudflare`: Production provider using Cloudflare Queue bindings\n * - `sync`: Testing provider that processes messages immediately\n *\n * @example\n * ```typescript\n * // Configuration via QueueModule.forRootAsync()\n * QueueModule.forRootAsync({\n * inject: [CONFIG_TOKENS.ConfigService],\n * useFactory: (config) => ({ provider: config.get('queue').provider })\n * })\n *\n * // Factory usage (internal)\n * const factory = container.resolve(QueueProviderFactory)\n * const provider = factory.create()\n * ```\n */\n@Transient(QUEUE_TOKENS.QueueProviderFactory)\nexport class QueueProviderFactory {\n constructor(\n @inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv,\n @inject(DI_TOKENS.ConsumerRegistry) private readonly registry: ConsumerRegistry,\n @inject(DI_TOKENS.Container) private readonly container: Container,\n @inject(QUEUE_TOKENS.QueueModuleOptions, { isOptional: true }) private readonly options?: QueueModuleOptions,\n ) { }\n\n /**\n * Create a queue provider based on module configuration\n *\n * @returns Queue provider instance\n * @throws {QueueError} If provider type is not supported\n */\n create(): IQueueProvider {\n const providerType = this.options?.provider ?? 'cloudflare'\n\n switch (providerType) {\n case 'cloudflare':\n return new CloudflareQueueProvider(this.env)\n\n case 'sync':\n return new SyncQueueProvider(this.registry, this.container)\n\n default:\n throw new QueueError(`Queue provider \"${String(providerType)}\" is not supported`)\n }\n }\n}\n","/**\n * Queue Module\n *\n * Provides declarative queue infrastructure with provider abstraction.\n *\n * **Usage:**\n * ```typescript\n * // 1. Configure provider (once, in app root)\n * QueueModule.forRootAsync({\n * inject: [CONFIG_TOKENS.ConfigService],\n * useFactory: (config) => ({ provider: config.get('queue').provider })\n * })\n *\n * // 2. Register queue bindings (the binding IS the injection token)\n * QueueModule.registerQueue('NOTIFICATIONS_QUEUE')\n * QueueModule.registerQueue('BACKGROUND_QUEUE')\n *\n * // 3. Inject and use\n * constructor(@InjectQueue('NOTIFICATIONS_QUEUE') private queue: IQueueSender) {}\n * await this.queue.dispatch({ type: 'email.send', payload: {...} })\n * ```\n *\n * **Providers:**\n * - `cloudflare`: Production provider using Cloudflare Queue bindings\n * - `sync`: Testing provider that processes messages immediately\n */\n\nimport { CacheModule } from '../cache';\nimport { DI_TOKENS } from '../di/tokens';\nimport { type StratalEnv } from '../env';\nimport { Module } from '../module';\nimport type {\n AsyncModuleOptions,\n DynamicModule,\n InjectionToken,\n ModuleContext,\n OnInitialize,\n} from '../module/types';\nimport { ConsumerRegistry } from './consumer-registry';\nimport type { QueueBinding } from './queue-binding';\nimport { QueueManager } from './queue-manager';\nimport { QueueRegistry } from './queue-registry';\nimport type { IQueueSender } from './queue-sender.interface';\nimport { QueueError } from './queue.error';\nimport { DEFAULT_STORE_BINDING, QueueStore } from './queue-store';\nimport { QUEUE_TOKENS } from './queue.tokens';\nimport { QueueProviderFactory } from './services';\n\n/**\n * Queue module configuration options\n */\nexport interface QueueModuleOptions {\n /**\n * Queue provider type\n * - 'cloudflare': Production provider using Cloudflare Queue bindings\n * - 'sync': Testing provider that processes messages immediately\n */\n provider: 'cloudflare' | 'sync'\n\n /**\n * KV binding for queue state (idempotency keys + failed jobs).\n * Defaults to the `CACHE` binding if omitted.\n */\n store?: {\n /**\n * KV namespace binding name.\n * @default CACHE\n */\n binding?: string\n }\n\n /**\n * Idempotency configuration.\n *\n * Delivery is **at-least-once with best-effort de-duplication**, not\n * exactly-once. Every dispatch carries an idempotency key (an explicit\n * `metadata.idempotencyKey`, otherwise a deterministic SHA-256 hash of `type`\n * + `payload`), and a message already recorded as processed is skipped. The\n * processed marker is written only after a handler succeeds, and KV `get`/`put`\n * are eventually consistent — so a message redelivered concurrently (or after\n * a crash before the marker was durable) can still run more than once. Make\n * handlers idempotent; don't rely on this as a hard exactly-once guarantee.\n * `ttl` bounds how long processed keys are remembered.\n */\n idempotency?: {\n /** TTL in seconds for processed idempotency keys. Default: 86400 (24h) */\n ttl?: number\n }\n\n /**\n * Failed-job configuration.\n *\n * Failed jobs persist indefinitely until retried or purged. To bound growth,\n * register the opt-in `FailedJobCleanupJob` cron in a module's `jobs` array;\n * it deletes failed jobs older than `retention`.\n */\n failedJobs?: {\n /**\n * Age in seconds beyond which `FailedJobCleanupJob` deletes a failed job.\n * Default: 604800 (7d). Has no effect unless the cron is registered.\n */\n retention?: number\n }\n\n /** Max retry attempts before storing as failed job. Default: 3 */\n maxRetries?: number\n}\n\n@Module({\n // QueueStore persists idempotency claims and failed jobs through CacheService\n // (isolate-local L1 + KV), so the cache must be available wherever queues are.\n imports: [CacheModule],\n providers: [\n { provide: DI_TOKENS.ConsumerRegistry, useClass: ConsumerRegistry },\n QueueManager,\n { provide: QUEUE_TOKENS.QueueProviderFactory, useClass: QueueProviderFactory },\n { provide: QUEUE_TOKENS.QueueRegistry, useClass: QueueRegistry },\n { provide: QUEUE_TOKENS.QueueStore, useClass: QueueStore },\n ],\n})\nexport class QueueModule implements OnInitialize {\n /**\n * Fail fast at boot if the configured KV store binding is missing, rather\n * than letting every queue invocation hard-fail lazily. The binding backs\n * idempotency claims and failed-job storage, so without it the queue\n * subsystem cannot function.\n */\n onInitialize({ container }: ModuleContext): void {\n const options = container.resolve<QueueModuleOptions>(QUEUE_TOKENS.QueueModuleOptions)\n\n // Only the `cloudflare` provider persists idempotency claims and failed jobs\n // to KV. The `sync` provider (dev/CLI) processes inline and never touches the\n // store, so it must not require the binding.\n if (options.provider !== 'cloudflare') return\n\n const binding = options.store?.binding ?? DEFAULT_STORE_BINDING\n const env = container.resolve<StratalEnv>(DI_TOKENS.CloudflareEnv)\n const kv = (env as unknown as Record<string, unknown>)[binding]\n\n if (!kv) {\n throw new QueueError(\n `Queue KV store binding \"${binding}\" was not found in the environment. ` +\n `The queue subsystem persists idempotency claims and failed jobs to KV. ` +\n `Add a kv_namespaces entry for \"${binding}\" in wrangler.jsonc, or set ` +\n `QueueModule.forRootAsync({ ..., store: { binding: 'YOUR_KV' } }) to point at an existing namespace.`,\n )\n }\n }\n\n /**\n * Configure queue infrastructure with async factory.\n *\n * Use when provider configuration depends on other services like ConfigService.\n *\n * @param options - Async configuration with factory and inject tokens\n * @returns Dynamic module with queue infrastructure\n *\n * @example\n * ```typescript\n * QueueModule.forRootAsync({\n * inject: [CONFIG_TOKENS.ConfigService],\n * useFactory: (config: IConfigService) => ({\n * provider: config.get('queue').provider\n * })\n * })\n * ```\n */\n static forRootAsync(options: AsyncModuleOptions<QueueModuleOptions>): DynamicModule {\n return {\n module: QueueModule,\n providers: [\n {\n provide: QUEUE_TOKENS.QueueModuleOptions,\n useFactory: options.useFactory,\n inject: options.inject,\n },\n ],\n }\n }\n\n /**\n * Register a queue binding for injection.\n *\n * The binding name doubles as the DI injection token and the\n * `env`-lookup key. Binding names are typed against `StratalEnv`\n * (autocomplete works once an app augments `StratalEnv` with its\n * Cloudflare bindings).\n *\n * @param binding - Queue binding identifier (e.g. `NOTIFICATIONS_QUEUE`).\n * @returns Dynamic module that provides the queue sender\n *\n * @example\n * ```typescript\n * // In AppModule imports\n * QueueModule.registerQueue('NOTIFICATIONS_QUEUE')\n *\n * // Then inject using the binding name\n * constructor(@InjectQueue('NOTIFICATIONS_QUEUE') private queue: IQueueSender) {}\n * ```\n */\n static registerQueue(binding: QueueBinding): DynamicModule {\n return {\n module: QueueModule,\n providers: [\n {\n provide: binding as InjectionToken<IQueueSender>,\n useFactory: (registry: QueueRegistry) => registry.getQueue(binding),\n inject: [QUEUE_TOKENS.QueueRegistry],\n },\n ],\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"queue.module-CEs4_kEM.mjs","names":[],"sources":["../src/queue/queue.error.ts","../src/queue/queue-manager.ts","../src/queue/consumer-registry.ts","../src/queue/queue-sender.ts","../src/queue/queue-registry.ts","../src/queue/queue-store.ts","../src/queue/providers/cloudflare-queue.provider.ts","../src/queue/providers/sync-queue.provider.ts","../src/queue/services/queue-provider-factory.ts","../src/queue/queue.module.ts"],"sourcesContent":["import { ApplicationError } from '../errors'\n\nexport class QueueError extends ApplicationError {}\n","import { inject } from '../di'\nimport { getContainer } from '../di/container-storage'\nimport { Transient } from '../di/decorators'\nimport { DI_TOKENS } from '../di/tokens'\nimport { LOGGER_TOKENS, type LoggerService } from '../logger'\nimport { type ConsumerRegistry } from './consumer-registry'\nimport type { FailedJob } from './failed-job'\nimport type { IQueueConsumer, QueueMessage } from './queue-consumer'\nimport { QueueError } from './queue.error'\nimport type { QueueModuleOptions } from './queue.module'\nimport type { QueueStore } from './queue-store'\nimport { QUEUE_TOKENS } from './queue.tokens'\n\nconst DEFAULT_MAX_RETRIES = 3\n\n@Transient(DI_TOKENS.Queue)\nexport class QueueManager {\n private readonly maxRetries: number\n\n constructor(\n @inject(DI_TOKENS.ConsumerRegistry) private readonly registry: ConsumerRegistry,\n @inject(LOGGER_TOKENS.LoggerService) private readonly logger: LoggerService,\n @inject(QUEUE_TOKENS.QueueStore) private readonly store: QueueStore,\n @inject(QUEUE_TOKENS.QueueModuleOptions) options: QueueModuleOptions,\n ) {\n this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES\n }\n\n async processBatch(queueName: string, batch: MessageBatch): Promise<void> {\n for (const message of batch.messages) {\n const queueMessage = message.body as QueueMessage\n const idempotencyKey = queueMessage.metadata?.idempotencyKey ?? queueMessage.id\n\n if (await this.store.isProcessed(idempotencyKey)) {\n message.ack()\n continue\n }\n\n // Resolve a fresh consumer instance per message from the active request\n // scope (handleQueue runs processBatch inside runInRequestScope), so\n // request-scoped consumer dependencies bind to this message's scope.\n const container = getContainer()\n const consumers = this.registry\n .getConsumerClasses(queueMessage.type)\n .map((ConsumerClass) => container.resolve<IQueueConsumer>(ConsumerClass))\n\n const results = await Promise.allSettled(\n consumers.map((consumer) => consumer.handle(queueMessage)),\n )\n\n let lastError: Error | undefined\n let failedConsumer: string | undefined\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const consumer = consumers[i]\n const errorInstance = result.reason instanceof Error\n ? result.reason\n : new Error(String(result.reason))\n\n this.logger.error('Queue message processing failed', errorInstance, {\n type: queueMessage.type,\n queue: queueName,\n messageId: queueMessage.id,\n idempotencyKey,\n })\n\n if (consumer.onError) {\n await consumer.onError(errorInstance, queueMessage)\n }\n\n lastError = errorInstance\n failedConsumer = consumer.constructor.name\n }\n }\n\n const failed = lastError !== undefined\n\n if (failed) {\n // `message.attempts` is 1-based (the first delivery is attempt 1), so a\n // message is only out of retries once it has been delivered more than\n // `maxRetries` times. `maxRetries: 3` therefore means 3 retries after the\n // initial delivery (4 total attempts). This must be <= the consumer's\n // `max_retries` in wrangler.jsonc, otherwise Cloudflare dead-letters the\n // message before this branch ever runs and it never reaches the store.\n if (message.attempts > this.maxRetries) {\n // A KV failure (or a message dispatched outside Stratal, with no\n // binding to retry through) while persisting the failed job must not\n // abort the rest of the batch — log and ack so the message isn't\n // redelivered forever.\n try {\n const binding = queueMessage.metadata?.binding\n if (!binding) {\n throw new QueueError(\n `Queue message ${queueMessage.id} has no binding metadata and cannot be recorded for retry. ` +\n `Messages must be dispatched through a Stratal queue sender (@InjectQueue).`,\n )\n }\n\n const failedJob: FailedJob = {\n id: queueMessage.id,\n queue: queueName,\n binding,\n type: queueMessage.type,\n message: queueMessage,\n error: {\n name: lastError!.name,\n message: lastError!.message,\n stack: lastError!.stack,\n },\n consumer: failedConsumer!,\n attempts: message.attempts,\n failedAt: new Date().toISOString(),\n }\n\n await this.store.storeFailedJob(failedJob)\n } catch (error) {\n this.logger.error(\n 'Failed to persist failed queue job',\n error instanceof Error ? error : new Error(String(error)),\n { queue: queueName, messageId: queueMessage.id, type: queueMessage.type },\n )\n }\n message.ack()\n } else {\n message.retry()\n }\n } else {\n await this.store.markProcessed(idempotencyKey)\n message.ack()\n }\n }\n }\n}\n","import { Singleton } from '../di/decorators'\nimport { DI_TOKENS } from '../di/tokens'\nimport type { Constructor } from '../types'\nimport type { IQueueConsumer } from './queue-consumer'\n\n/** A registered consumer class together with the message types it handles. */\nexport interface ConsumerRegistration {\n consumerClass: Constructor<IQueueConsumer>\n messageTypes: string[]\n}\n\n/**\n * Consumer Registry\n *\n * Singleton service that holds all registered queue consumers indexed by message type.\n * Consumers declare the message types they handle, and this registry routes messages\n * to the appropriate consumers based on their types.\n *\n * **Message-Type Routing:**\n * - Consumers declare `messageTypes` array (e.g., `['email.send', 'email.batch.send']`)\n * - When a message arrives, consumers matching the message type are invoked\n * - A consumer can handle messages from ANY queue (routing is by type, not queue)\n * - Use `'*'` as a wildcard to handle all message types\n *\n * @example Consumer registration\n * ```typescript\n * // In consumer.ts\n * @Transient()\n * export class EmailConsumer implements IQueueConsumer {\n * readonly messageTypes = ['email.send', 'email.batch.send']\n * // ...\n * }\n *\n * // In module.ts\n * @Module({\n * consumers: [EmailConsumer]\n * })\n *\n * // Application auto-registers via ConsumerRegistry\n * this.consumerRegistry.register(consumer)\n * ```\n */\n@Singleton(DI_TOKENS.ConsumerRegistry)\nexport class ConsumerRegistry {\n /** Map from message type to consumer classes handling that type */\n private classesByType = new Map<string, Constructor<IQueueConsumer>[]>()\n\n /** All registrations (for iteration / listing) */\n private registrations: ConsumerRegistration[] = []\n\n /** Registered consumer classes (dedupe) */\n private registeredClasses = new Set<Constructor<IQueueConsumer>>()\n\n /**\n * Register a queue consumer class.\n *\n * Indexes the class by each of its declared message types. A fresh instance\n * is resolved per message from the request-scoped container at dispatch time —\n * consumers are never held as long-lived singletons, so they may safely inject\n * request-scoped providers (`@InjectQueue`, i18n, auth context, …).\n *\n * @param consumerClass - Queue consumer class to register\n * @param messageTypes - Message types the consumer handles\n */\n register(consumerClass: Constructor<IQueueConsumer>, messageTypes: string[]): void {\n if (this.registeredClasses.has(consumerClass)) {\n return // Already registered\n }\n\n this.registeredClasses.add(consumerClass)\n this.registrations.push({ consumerClass, messageTypes })\n\n for (const messageType of messageTypes) {\n const existing = this.classesByType.get(messageType) ?? []\n existing.push(consumerClass)\n this.classesByType.set(messageType, existing)\n }\n }\n\n /**\n * Get all consumer classes that can handle a specific message type.\n *\n * Returns classes that either declare the message type explicitly or use the\n * `'*'` wildcard. Callers resolve a fresh instance per message from the\n * request-scoped container.\n *\n * @param messageType - The message type to find consumers for\n * @returns Array of consumer classes that can handle this message type\n */\n getConsumerClasses(messageType: string): Constructor<IQueueConsumer>[] {\n const exactMatch = this.classesByType.get(messageType) ?? []\n const wildcardMatch = this.classesByType.get('*') ?? []\n\n // Combine and dedupe\n return Array.from(new Set([...exactMatch, ...wildcardMatch]))\n }\n\n /**\n * Check if any consumers can handle a message type\n *\n * @param messageType - The message type to check\n * @returns true if at least one consumer can handle this type\n */\n hasConsumers(messageType: string): boolean {\n return this.getConsumerClasses(messageType).length > 0\n }\n\n /**\n * Get all registered message types\n *\n * @returns Array of message types with registered consumers\n */\n getMessageTypes(): string[] {\n return Array.from(this.classesByType.keys())\n }\n\n /**\n * Get all consumer registrations (class + message types) for listing.\n */\n getRegistrations(): readonly ConsumerRegistration[] {\n return this.registrations\n }\n}\n","import type { II18nService } from '../i18n/i18n.types';\nimport type { IQueueProvider } from './providers';\nimport type { QueueMessage } from './queue-consumer';\nimport type { DispatchMessage, IQueueSender } from './queue-sender.interface';\n\n/**\n * Queue Sender\n *\n * Implementation of IQueueSender bound to a specific queue binding.\n * Created by QueueRegistry for each registered binding.\n *\n * Automatically enriches messages with:\n * - `id`: UUID generated via crypto.randomUUID()\n * - `metadata.locale`: Current locale from I18n context\n * - `metadata.idempotencyKey`: Deterministic SHA-256 hash of type + payload (if not provided)\n *\n * @example\n * ```typescript\n * // Created by QueueRegistry, not directly instantiated\n * const sender = registry.getQueue('NOTIFICATIONS_QUEUE')\n *\n * await sender.dispatch({\n * type: 'email.send',\n * payload: { to: 'user@example.com', subject: 'Hello' }\n * })\n * ```\n */\nexport class QueueSender implements IQueueSender {\n constructor(\n private readonly binding: string,\n private readonly provider: IQueueProvider,\n private readonly i18n: II18nService\n ) {}\n\n /**\n * Dispatch a message to this queue.\n *\n * @param message - Message to dispatch (without id)\n */\n async dispatch<T>(message: DispatchMessage<T>): Promise<void> {\n const metadata = { ...message.metadata }\n\n if (!metadata.locale) {\n const locale = this.i18n.getLocale()\n if (locale) {\n metadata.locale = locale\n }\n }\n\n metadata.idempotencyKey ??= await this.generateIdempotencyKey(message.type, message.payload);\n\n // Stamp the producer binding so a failed job can be retried to the right\n // Cloudflare binding. Consumers only see the queue *name*, which is not a\n // valid producer binding key.\n metadata.binding = this.binding\n\n const fullMessage: QueueMessage<T> = {\n id: crypto.randomUUID(),\n ...message,\n metadata,\n }\n\n await this.provider.send(this.binding, fullMessage)\n }\n\n private async generateIdempotencyKey(type: string, payload: unknown): Promise<string> {\n // Use a stable, key-sorted serialization: `JSON.stringify` preserves\n // insertion order, so two semantically identical payloads with differently\n // ordered keys would otherwise hash differently and defeat deduplication.\n const data = new TextEncoder().encode(stableStringify({ type, payload }))\n const hash = await crypto.subtle.digest('SHA-256', data)\n return `queue:${Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('')}`\n }\n}\n\n/**\n * Deterministic JSON serialization: object keys are emitted in sorted order at\n * every level so the output depends only on the data, not on key insertion\n * order. Arrays keep their order (order is significant). Used to derive a stable\n * idempotency hash from a message's `type` + `payload`.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value) ?? 'null'\n }\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(',')}]`\n }\n const entries = Object.keys(value as Record<string, unknown>)\n .sort()\n .map((key) => {\n const v = (value as Record<string, unknown>)[key]\n if (v === undefined) return undefined\n return `${JSON.stringify(key)}:${stableStringify(v)}`\n })\n .filter((entry): entry is string => entry !== undefined)\n return `{${entries.join(',')}}`\n}\n","import { inject } from '../di'\nimport { Request } from '../di/decorators'\nimport { I18N_TOKENS } from '../i18n/i18n.tokens'\nimport type { II18nService } from '../i18n/i18n.types'\nimport type { IQueueProvider } from './providers'\nimport type { IQueueSender } from './queue-sender.interface'\nimport { QueueSender } from './queue-sender'\nimport { QUEUE_TOKENS } from './queue.tokens'\nimport { type QueueProviderFactory } from './services'\n\n/**\n * Queue Registry\n *\n * Request-scoped factory service for creating QueueSender instances.\n * Caches senders per binding within the request scope.\n *\n * This service is used internally by QueueModule.registerQueue() to provide\n * IQueueSender instances for each registered binding.\n *\n * **Why request-scoped?**\n * - Needs access to I18nService for locale-aware message metadata\n * - Provider is created once per request for consistency\n * - Queue senders are cached per request to avoid recreating them\n *\n * @example\n * ```typescript\n * // Used internally by QueueModule.registerQueue()\n * QueueModule.registerQueue('NOTIFICATIONS_QUEUE')\n *\n * // The module creates a factory provider:\n * {\n * provide: 'NOTIFICATIONS_QUEUE',\n * useFactory: (registry: QueueRegistry) => registry.getQueue('NOTIFICATIONS_QUEUE'),\n * inject: [QUEUE_TOKENS.QueueRegistry],\n * }\n * ```\n */\n@Request(QUEUE_TOKENS.QueueRegistry)\nexport class QueueRegistry {\n private readonly provider: IQueueProvider\n private readonly senders = new Map<string, IQueueSender>()\n\n constructor(\n @inject(QUEUE_TOKENS.QueueProviderFactory) providerFactory: QueueProviderFactory,\n @inject(I18N_TOKENS.I18nService) private readonly i18n: II18nService\n ) {\n this.provider = providerFactory.create()\n }\n\n /**\n * Get or create a QueueSender for the specified binding.\n *\n * Senders are cached per binding within the request scope.\n *\n * @param binding - The queue binding to get a sender for\n * @returns QueueSender bound to the specified binding\n */\n getQueue(binding: string): IQueueSender {\n let sender = this.senders.get(binding)\n\n if (!sender) {\n sender = new QueueSender(binding, this.provider, this.i18n)\n this.senders.set(binding, sender)\n }\n\n return sender\n }\n}\n","import { inject } from '../di';\nimport { Transient } from '../di/decorators';\nimport { CACHE_TOKENS } from '../cache/cache.tokens';\nimport type { TieredCacheService } from '../cache/services/tiered-cache.service';\nimport type { FailedJob, FailedJobMetadata } from './failed-job';\nimport type { QueueModuleOptions } from './queue.module';\nimport { QUEUE_TOKENS } from './queue.tokens';\n\nconst IDEM_PREFIX = 'queue:idem:'\nconst FAILED_PREFIX = 'queue:failed:'\nconst DEFAULT_IDEMPOTENCY_TTL = 86400\n\n/** Default KV binding name used for queue state when none is configured. */\nexport const DEFAULT_STORE_BINDING = 'CACHE'\n\n/**\n * Persists queue idempotency claims and failed jobs.\n *\n * Backed by {@link TieredCacheService} (isolate-local L1 + KV). The L1 is what\n * makes `markProcessed` → `isProcessed` reliable within an isolate: a claim\n * written on this isolate is read back from memory, so a message redelivered to\n * the same warm isolate is de-duplicated even inside KV's eventual-consistency\n * window. Idempotency markers are set-once, the pattern the L1 tier is designed\n * for. Cross-isolate duplicates still rely on KV (eventually consistent) —\n * delivery remains at-least-once with best-effort de-duplication, not\n * exactly-once.\n */\n@Transient(QUEUE_TOKENS.QueueStore)\nexport class QueueStore {\n private readonly cache: TieredCacheService\n private readonly idempotencyTtl: number\n\n constructor(\n @inject(CACHE_TOKENS.TieredCacheService) cache: TieredCacheService,\n @inject(QUEUE_TOKENS.QueueModuleOptions) options: QueueModuleOptions,\n ) {\n // Bind to the configured KV namespace via the tiered cache so the L1 lives\n // on its singleton (one per isolate) and persists across messages. The\n // binding is validated at module init (QueueModule.onInitialize);\n // `binding()` also throws if it is somehow absent here.\n this.cache = cache.binding(options.store?.binding ?? DEFAULT_STORE_BINDING)\n this.idempotencyTtl = options.idempotency?.ttl ?? DEFAULT_IDEMPOTENCY_TTL\n }\n\n async isProcessed(key: string): Promise<boolean> {\n return (await this.cache.get(`${IDEM_PREFIX}${key}`)) !== null\n }\n\n async markProcessed(key: string): Promise<void> {\n await this.cache.put(`${IDEM_PREFIX}${key}`, '1', {\n expirationTtl: this.idempotencyTtl,\n })\n }\n\n async storeFailedJob(job: FailedJob): Promise<void> {\n const metadata: FailedJobMetadata = {\n queue: job.queue,\n binding: job.binding,\n type: job.type,\n consumer: job.consumer,\n attempts: job.attempts,\n failedAt: job.failedAt,\n }\n\n await this.cache.put(`${FAILED_PREFIX}${job.id}`, JSON.stringify(job), {\n metadata,\n })\n }\n\n async getFailedJob(messageId: string): Promise<FailedJob | null> {\n return this.cache.get<FailedJob>(`${FAILED_PREFIX}${messageId}`, 'json')\n }\n\n async removeFailedJob(messageId: string): Promise<void> {\n await this.cache.delete(`${FAILED_PREFIX}${messageId}`)\n }\n\n async listFailedJobs(options?: {\n limit?: number\n cursor?: string\n }): Promise<{ keys: { id: string; metadata: FailedJobMetadata }[]; cursor?: string }> {\n const result = await this.cache.list<FailedJobMetadata>({\n prefix: FAILED_PREFIX,\n limit: options?.limit ?? 50,\n cursor: options?.cursor,\n })\n\n // Skip keys without metadata: a partially-written entry (or one written by\n // an older code path) is unusable for listing and must not crash the page.\n const keys = result.keys\n .filter((key): key is typeof key & { metadata: FailedJobMetadata } => key.metadata != null)\n .map((key) => ({\n id: key.name.slice(FAILED_PREFIX.length),\n metadata: key.metadata,\n }))\n\n return {\n keys,\n cursor: result.list_complete ? undefined : result.cursor,\n }\n }\n\n async purgeFailedJobs(): Promise<void> {\n let cursor: string | undefined\n\n do {\n const result = await this.cache.list({ prefix: FAILED_PREFIX, cursor })\n await Promise.all(result.keys.map((key) => this.cache.delete(key.name)))\n cursor = result.list_complete ? undefined : result.cursor\n } while (cursor)\n }\n\n /**\n * Delete failed jobs older than `retentionSeconds` (by their `failedAt`\n * timestamp). Returns the number removed. Backs the opt-in\n * {@link FailedJobCleanupJob} cron — failed jobs otherwise persist\n * indefinitely until retried or purged.\n */\n async purgeFailedJobsOlderThan(retentionSeconds: number): Promise<number> {\n const cutoff = Date.now() - retentionSeconds * 1000\n let cursor: string | undefined\n let removed = 0\n\n do {\n const result = await this.cache.list<FailedJobMetadata>({ prefix: FAILED_PREFIX, cursor })\n const expired = result.keys.filter((key) => {\n const failedAt = key.metadata?.failedAt\n return failedAt !== undefined && Date.parse(failedAt) < cutoff\n })\n await Promise.all(expired.map((key) => this.cache.delete(key.name)))\n removed += expired.length\n cursor = result.list_complete ? undefined : result.cursor\n } while (cursor)\n\n return removed\n }\n}\n","import { inject } from '../../di'\nimport { type StratalEnv } from '../../env'\nimport { Transient } from '../../di/decorators'\nimport { DI_TOKENS } from '../../di/tokens'\nimport { QueueError } from '../queue.error'\nimport type { QueueMessage } from '../queue-consumer'\nimport type { IQueueProvider } from './queue-provider.interface'\n\n/**\n * Cloudflare Queue Provider\n *\n * Sends messages to Cloudflare Queues by resolving the binding directly on\n * the worker's `env`. Used in production environments where Cloudflare Workers\n * handle queue processing.\n *\n * @example\n * ```typescript\n * const provider = new CloudflareQueueProvider(env)\n * await provider.send('NOTIFICATIONS_QUEUE', message)\n * ```\n */\n@Transient()\nexport class CloudflareQueueProvider implements IQueueProvider {\n constructor(\n @inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv\n ) { }\n\n /**\n * Send a message to a Cloudflare Queue\n *\n * @param binding - Queue binding identifier (e.g., 'NOTIFICATIONS_QUEUE')\n * @param message - Complete message with id and payload\n * @throws {QueueError} If the binding is not configured on env\n */\n async send<T>(binding: string, message: QueueMessage<T>): Promise<void> {\n const queue = (this.env as unknown as Record<string, unknown>)[binding] as Queue | undefined\n\n if (!queue) {\n throw new QueueError(`Queue binding \"${binding}\" was not found in the environment`)\n }\n\n await queue.send(message)\n }\n}\n","import type { Application } from '../../application'\nimport type { Container } from '../../di/container'\nimport { containerStorage } from '../../di/container-storage'\nimport { inject } from '../../di'\nimport { Transient } from '../../di/decorators'\nimport { DI_TOKENS } from '../../di/tokens'\nimport type { RouterContext } from '../../router/router-context'\nimport { type ConsumerRegistry } from '../consumer-registry'\nimport type { IQueueConsumer, QueueMessage } from '../queue-consumer'\nimport type { IQueueProvider } from './queue-provider.interface'\n\n/**\n * Sync Queue Provider\n *\n * Processes messages immediately by finding matching consumers and calling\n * their handle() method directly. Used for testing and development where\n * real queue infrastructure is not available.\n *\n * **Behavior:**\n * - Messages are processed synchronously when send() is called\n * - Matching consumers are found via ConsumerRegistry by message type\n * - All matching consumers are called sequentially\n * - Errors are re-thrown after onError() is called (fail-fast for testing)\n *\n * **Consumer Matching:**\n * - Consumers are matched by message type, not queue name\n * - Wildcard ('*') matches all message types\n */\n@Transient()\nexport class SyncQueueProvider implements IQueueProvider {\n constructor(\n @inject(DI_TOKENS.ConsumerRegistry) private readonly registry: ConsumerRegistry,\n @inject(DI_TOKENS.Container) private readonly root: Container,\n @inject(DI_TOKENS.Application) private readonly app: Application,\n ) {}\n\n /**\n * Process a message synchronously.\n *\n * Runs inside the active request scope when dispatch happens within one (an\n * HTTP request, or `runInScope` for queues/cron/commands). When dispatched\n * with no ambient scope — e.g. a service invoked directly in a test — it\n * establishes its own request scope (mirroring the production queue handler)\n * so consumers and their request-scoped dependencies resolve correctly.\n *\n * @param _binding - Queue binding (not used for routing, consumers match by message type)\n * @param message - Complete message with id and payload\n * @throws Re-throws any error from consumer.handle() after calling onError()\n */\n async send<T>(_binding: string, message: QueueMessage<T>): Promise<void> {\n // The fetch path deliberately skips queue init (a Cloudflare-queue worker\n // never processes consumers inline) — but the sync provider IS the inline\n // consumer path, so populate the consumer registry before matching message\n // types. Memoized inside the application; a no-op after the first dispatch.\n await this.app.ensureScopedHandlers()\n\n const ambient = containerStorage.getStore()\n if (ambient) {\n await this.process(ambient, message)\n return\n }\n\n const locale = message.metadata?.locale ?? 'en'\n const context = {\n getLocale: () => locale,\n setLocale: () => { /* no-op */ },\n getContainer: () => containerStorage.getStore() ?? this.root,\n } as unknown as RouterContext\n\n await this.root.runInRequestScope(context, (container) => this.process(container, message))\n }\n\n /**\n * Resolve a fresh consumer per message from `container` (matched by type) and\n * invoke each sequentially, fail-fast on the first error after `onError`.\n */\n private async process<T>(container: Container, message: QueueMessage<T>): Promise<void> {\n const consumers = this.registry\n .getConsumerClasses(message.type)\n .map((ConsumerClass) => container.resolve<IQueueConsumer>(ConsumerClass))\n\n for (const consumer of consumers) {\n try {\n await consumer.handle(message)\n } catch (error) {\n const errorInstance = error instanceof Error ? error : new Error(String(error))\n\n // Call onError hook if defined\n if (consumer.onError) {\n await consumer.onError(errorInstance, message)\n }\n\n // Re-throw for fail-fast behavior in tests\n throw errorInstance\n }\n }\n }\n}\n","import { inject } from '../../di'\nimport type { Container } from '../../di/container'\nimport { type StratalEnv } from '../../env'\nimport { Transient } from '../../di/decorators'\nimport { DI_TOKENS } from '../../di/tokens'\nimport { type ConsumerRegistry } from '../consumer-registry'\nimport { QueueError } from '../queue.error'\nimport type { Application } from '../../application'\nimport { CloudflareQueueProvider, SyncQueueProvider, type IQueueProvider } from '../providers'\nimport type { QueueModuleOptions } from '../queue.module'\nimport { QUEUE_TOKENS } from '../queue.tokens'\n\n/**\n * Queue Provider Factory\n *\n * Creates the appropriate queue provider based on configuration provided\n * via QueueModule.forRootAsync().\n *\n * **Provider Selection:**\n * - `cloudflare`: Production provider using Cloudflare Queue bindings\n * - `sync`: Testing provider that processes messages immediately\n *\n * @example\n * ```typescript\n * // Configuration via QueueModule.forRootAsync()\n * QueueModule.forRootAsync({\n * inject: [CONFIG_TOKENS.ConfigService],\n * useFactory: (config) => ({ provider: config.get('queue').provider })\n * })\n *\n * // Factory usage (internal)\n * const factory = container.resolve(QueueProviderFactory)\n * const provider = factory.create()\n * ```\n */\n@Transient(QUEUE_TOKENS.QueueProviderFactory)\nexport class QueueProviderFactory {\n constructor(\n @inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv,\n @inject(DI_TOKENS.ConsumerRegistry) private readonly registry: ConsumerRegistry,\n @inject(DI_TOKENS.Container) private readonly container: Container,\n @inject(DI_TOKENS.Application) private readonly app: Application,\n @inject(QUEUE_TOKENS.QueueModuleOptions, { isOptional: true }) private readonly options?: QueueModuleOptions,\n ) { }\n\n /**\n * Create a queue provider based on module configuration\n *\n * @returns Queue provider instance\n * @throws {QueueError} If provider type is not supported\n */\n create(): IQueueProvider {\n const providerType = this.options?.provider ?? 'cloudflare'\n\n switch (providerType) {\n case 'cloudflare':\n return new CloudflareQueueProvider(this.env)\n\n case 'sync':\n return new SyncQueueProvider(this.registry, this.container, this.app)\n\n default:\n throw new QueueError(`Queue provider \"${String(providerType)}\" is not supported`)\n }\n }\n}\n","/**\n * Queue Module\n *\n * Provides declarative queue infrastructure with provider abstraction.\n *\n * **Usage:**\n * ```typescript\n * // 1. Configure provider (once, in app root)\n * QueueModule.forRootAsync({\n * inject: [CONFIG_TOKENS.ConfigService],\n * useFactory: (config) => ({ provider: config.get('queue').provider })\n * })\n *\n * // 2. Register queue bindings (the binding IS the injection token)\n * QueueModule.registerQueue('NOTIFICATIONS_QUEUE')\n * QueueModule.registerQueue('BACKGROUND_QUEUE')\n *\n * // 3. Inject and use\n * constructor(@InjectQueue('NOTIFICATIONS_QUEUE') private queue: IQueueSender) {}\n * await this.queue.dispatch({ type: 'email.send', payload: {...} })\n * ```\n *\n * **Providers:**\n * - `cloudflare`: Production provider using Cloudflare Queue bindings\n * - `sync`: Testing provider that processes messages immediately\n */\n\nimport { CacheModule } from '../cache';\nimport { DI_TOKENS } from '../di/tokens';\nimport { type StratalEnv } from '../env';\nimport { Module } from '../module';\nimport type {\n AsyncModuleOptions,\n DynamicModule,\n InjectionToken,\n ModuleContext,\n OnInitialize,\n} from '../module/types';\nimport { ConsumerRegistry } from './consumer-registry';\nimport type { QueueBinding } from './queue-binding';\nimport { QueueManager } from './queue-manager';\nimport { QueueRegistry } from './queue-registry';\nimport type { IQueueSender } from './queue-sender.interface';\nimport { QueueError } from './queue.error';\nimport { DEFAULT_STORE_BINDING, QueueStore } from './queue-store';\nimport { QUEUE_TOKENS } from './queue.tokens';\nimport { QueueProviderFactory } from './services';\n\n/**\n * Queue module configuration options\n */\nexport interface QueueModuleOptions {\n /**\n * Queue provider type\n * - 'cloudflare': Production provider using Cloudflare Queue bindings\n * - 'sync': Testing provider that processes messages immediately\n */\n provider: 'cloudflare' | 'sync'\n\n /**\n * KV binding for queue state (idempotency keys + failed jobs).\n * Defaults to the `CACHE` binding if omitted.\n */\n store?: {\n /**\n * KV namespace binding name.\n * @default CACHE\n */\n binding?: string\n }\n\n /**\n * Idempotency configuration.\n *\n * Delivery is **at-least-once with best-effort de-duplication**, not\n * exactly-once. Every dispatch carries an idempotency key (an explicit\n * `metadata.idempotencyKey`, otherwise a deterministic SHA-256 hash of `type`\n * + `payload`), and a message already recorded as processed is skipped. The\n * processed marker is written only after a handler succeeds, and KV `get`/`put`\n * are eventually consistent — so a message redelivered concurrently (or after\n * a crash before the marker was durable) can still run more than once. Make\n * handlers idempotent; don't rely on this as a hard exactly-once guarantee.\n * `ttl` bounds how long processed keys are remembered.\n */\n idempotency?: {\n /** TTL in seconds for processed idempotency keys. Default: 86400 (24h) */\n ttl?: number\n }\n\n /**\n * Failed-job configuration.\n *\n * Failed jobs persist indefinitely until retried or purged. To bound growth,\n * register the opt-in `FailedJobCleanupJob` cron in a module's `jobs` array;\n * it deletes failed jobs older than `retention`.\n */\n failedJobs?: {\n /**\n * Age in seconds beyond which `FailedJobCleanupJob` deletes a failed job.\n * Default: 604800 (7d). Has no effect unless the cron is registered.\n */\n retention?: number\n }\n\n /** Max retry attempts before storing as failed job. Default: 3 */\n maxRetries?: number\n}\n\n@Module({\n // QueueStore persists idempotency claims and failed jobs through CacheService\n // (isolate-local L1 + KV), so the cache must be available wherever queues are.\n imports: [CacheModule],\n providers: [\n { provide: DI_TOKENS.ConsumerRegistry, useClass: ConsumerRegistry },\n QueueManager,\n { provide: QUEUE_TOKENS.QueueProviderFactory, useClass: QueueProviderFactory },\n { provide: QUEUE_TOKENS.QueueRegistry, useClass: QueueRegistry },\n { provide: QUEUE_TOKENS.QueueStore, useClass: QueueStore },\n ],\n})\nexport class QueueModule implements OnInitialize {\n /**\n * Fail fast at boot if the configured KV store binding is missing, rather\n * than letting every queue invocation hard-fail lazily. The binding backs\n * idempotency claims and failed-job storage, so without it the queue\n * subsystem cannot function.\n */\n onInitialize({ container }: ModuleContext): void {\n const options = container.resolve<QueueModuleOptions>(QUEUE_TOKENS.QueueModuleOptions)\n\n // Only the `cloudflare` provider persists idempotency claims and failed jobs\n // to KV. The `sync` provider (dev/CLI) processes inline and never touches the\n // store, so it must not require the binding.\n if (options.provider !== 'cloudflare') return\n\n const binding = options.store?.binding ?? DEFAULT_STORE_BINDING\n const env = container.resolve<StratalEnv>(DI_TOKENS.CloudflareEnv)\n const kv = (env as unknown as Record<string, unknown>)[binding]\n\n if (!kv) {\n throw new QueueError(\n `Queue KV store binding \"${binding}\" was not found in the environment. ` +\n `The queue subsystem persists idempotency claims and failed jobs to KV. ` +\n `Add a kv_namespaces entry for \"${binding}\" in wrangler.jsonc, or set ` +\n `QueueModule.forRootAsync({ ..., store: { binding: 'YOUR_KV' } }) to point at an existing namespace.`,\n )\n }\n }\n\n /**\n * Configure queue infrastructure with async factory.\n *\n * Use when provider configuration depends on other services like ConfigService.\n *\n * @param options - Async configuration with factory and inject tokens\n * @returns Dynamic module with queue infrastructure\n *\n * @example\n * ```typescript\n * QueueModule.forRootAsync({\n * inject: [CONFIG_TOKENS.ConfigService],\n * useFactory: (config: IConfigService) => ({\n * provider: config.get('queue').provider\n * })\n * })\n * ```\n */\n static forRootAsync(options: AsyncModuleOptions<QueueModuleOptions>): DynamicModule {\n return {\n module: QueueModule,\n providers: [\n {\n provide: QUEUE_TOKENS.QueueModuleOptions,\n useFactory: options.useFactory,\n inject: options.inject,\n },\n ],\n }\n }\n\n /**\n * Register a queue binding for injection.\n *\n * The binding name doubles as the DI injection token and the\n * `env`-lookup key. Binding names are typed against `StratalEnv`\n * (autocomplete works once an app augments `StratalEnv` with its\n * Cloudflare bindings).\n *\n * @param binding - Queue binding identifier (e.g. `NOTIFICATIONS_QUEUE`).\n * @returns Dynamic module that provides the queue sender\n *\n * @example\n * ```typescript\n * // In AppModule imports\n * QueueModule.registerQueue('NOTIFICATIONS_QUEUE')\n *\n * // Then inject using the binding name\n * constructor(@InjectQueue('NOTIFICATIONS_QUEUE') private queue: IQueueSender) {}\n * ```\n */\n static registerQueue(binding: QueueBinding): DynamicModule {\n return {\n module: QueueModule,\n providers: [\n {\n provide: binding as InjectionToken<IQueueSender>,\n useFactory: (registry: QueueRegistry) => registry.getQueue(binding),\n inject: [QUEUE_TOKENS.QueueRegistry],\n },\n ],\n }\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BBjsoOtd.mjs";
2
- import { t as StorageError } from "./storage.error-Dnib4VHc.mjs";
2
+ import { t as StorageError } from "./storage.error-BStXPmO4.mjs";
3
3
  import { t as signUrl } from "./signed-url-DIU0sK_6.mjs";
4
4
  //#region src/storage/providers/r2-storage.provider.ts
5
5
  var r2_storage_provider_exports = /* @__PURE__ */ __exportAll({ R2StorageProvider: () => R2StorageProvider });
@@ -244,4 +244,4 @@ var R2StorageProvider = class {
244
244
  //#endregion
245
245
  export { r2_storage_provider_exports as n, R2StorageProvider as t };
246
246
 
247
- //# sourceMappingURL=r2-storage.provider-DCxQt9dD.mjs.map
247
+ //# sourceMappingURL=r2-storage.provider-BoZmR6Ut.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"r2-storage.provider-DCxQt9dD.mjs","names":[],"sources":["../src/storage/providers/r2-storage.provider.ts"],"sourcesContent":["import { type StratalEnv } from '../../env'\nimport { signUrl } from '../../router/signed-url'\nimport type { DownloadResult, PresignedUrlResult, UploadOptions, UploadResult } from '../contracts'\nimport { StorageError } from '../storage.error'\nimport type { StorageEntry, StorageRouteConfig } from '../types'\nimport type {\n CompletedPart,\n CompleteMultipartResult,\n CreateMultipartOptions,\n CreateMultipartResult,\n DeleteObjectsResult,\n HeadObjectResult,\n IMultipartProvider,\n ListMultipartUploadsResult,\n ListPartsResult,\n UploadPartResult,\n} from './multipart-provider.interface'\nimport type { StreamingBlobPayloadInputTypes } from './storage-provider.interface'\n\nconst MIN_PART_SIZE = 5 * 1024 * 1024 // 5MB\nconst MULTIPART_PREFIX = '__multipart/'\nconst PARTS_PREFIX = '__parts/'\n\n/**\n * R2 Storage Provider\n * Implements storage operations using native Cloudflare R2 bindings\n *\n * Implements IMultipartProvider for multipart upload support needed by TUS.\n * Tracks multipart upload metadata using companion R2 objects so that\n * listParts/listMultipartUploads work in all environments (local, test, prod).\n */\nexport class R2StorageProvider implements IMultipartProvider {\n private readonly routeBasePath: string\n\n constructor(\n private readonly config: StorageEntry,\n private readonly bucket: R2Bucket,\n private readonly env: StratalEnv,\n routeConfig?: StorageRouteConfig\n ) {\n this.routeBasePath = routeConfig?.basePath ?? '/storage'\n }\n\n async upload(\n body: StreamingBlobPayloadInputTypes,\n path: string,\n options: UploadOptions\n ): Promise<UploadResult> {\n const customMetadata: Record<string, string> = { ...options.metadata }\n if (options.tagging) {\n customMetadata['x-tags'] = options.tagging\n }\n\n await this.bucket.put(path, body as ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | null, {\n httpMetadata: {\n contentType: options.mimeType,\n },\n customMetadata,\n })\n\n return {\n path,\n disk: this.config.disk,\n fullPath: path,\n size: options.size,\n mimeType: options.mimeType ?? 'application/octet-stream',\n uploadedAt: new Date(),\n }\n }\n\n async download(path: string): Promise<DownloadResult> {\n const obj = await this.bucket.get(path)\n\n if (!obj) {\n throw new StorageError(`Storage object not found at path \"${path}\"`)\n }\n\n // R2ObjectBody has body, text(), arrayBuffer(), etc.\n const objBody = obj\n\n return {\n toStream: () => objBody.body as ReadableStream<Uint8Array>,\n contentType: obj.httpMetadata?.contentType ?? 'application/octet-stream',\n size: obj.size,\n metadata: obj.customMetadata,\n toString: () => objBody.text(),\n toArrayBuffer: () => objBody.bytes(),\n }\n }\n\n async delete(path: string): Promise<void> {\n await this.bucket.delete(path)\n }\n\n async exists(path: string): Promise<boolean> {\n const head = await this.bucket.head(path)\n return head !== null\n }\n\n async getPresignedUrl(\n path: string,\n method: 'GET' | 'PUT' | 'DELETE' | 'HEAD',\n expiresIn: number\n ): Promise<PresignedUrlResult> {\n const secret = this.env.APP_SECRET;\n if (!secret) {\n throw new StorageError('APP_SECRET is required for generating presigned URLs')\n }\n\n // Build a relative URL pointing to the StorageController route\n const routePath = `${this.routeBasePath}/${this.config.disk}/${path}`\n const urlWithMethod = `${routePath}?method=${method}`\n const url = await signUrl(urlWithMethod, secret, { expiresIn })\n\n return {\n url,\n expiresIn,\n expiresAt: new Date(Date.now() + expiresIn * 1000),\n method,\n }\n }\n\n async chunkedUpload(\n body: StreamingBlobPayloadInputTypes,\n path: string,\n options: Omit<UploadOptions, 'size'> & { size?: number }\n ): Promise<UploadResult> {\n const multipartUpload = await this.bucket.createMultipartUpload(path, {\n httpMetadata: {\n contentType: options.mimeType,\n },\n customMetadata: options.metadata,\n })\n\n const parts: R2UploadedPart[] = []\n\n try {\n if (body instanceof ReadableStream) {\n const reader = (body as ReadableStream<Uint8Array>).getReader()\n let buffer = new Uint8Array(0)\n let partNumber = 1\n\n while (true) {\n const { done, value } = await reader.read()\n\n if (value) {\n const newBuffer = new Uint8Array(buffer.length + value.length)\n newBuffer.set(buffer, 0)\n newBuffer.set(value, buffer.length)\n buffer = newBuffer\n }\n\n while (buffer.length >= MIN_PART_SIZE) {\n const partData = buffer.slice(0, MIN_PART_SIZE)\n buffer = buffer.slice(MIN_PART_SIZE)\n const part = await multipartUpload.uploadPart(partNumber, partData)\n parts.push(part)\n partNumber++\n }\n\n if (done) {\n // Upload remaining buffer as final part\n if (buffer.length > 0) {\n const part = await multipartUpload.uploadPart(partNumber, buffer)\n parts.push(part)\n }\n break\n }\n }\n } else if (body !== null && body !== undefined) {\n // Non-stream body: upload as single part\n const part = await multipartUpload.uploadPart(1, body as ArrayBuffer | ArrayBufferView | string | Blob)\n parts.push(part)\n }\n\n await multipartUpload.complete(parts)\n } catch (error) {\n await multipartUpload.abort()\n throw error\n }\n\n // Get actual size via head\n const headResult = await this.bucket.head(path)\n\n return {\n path,\n disk: this.config.disk,\n fullPath: path,\n size: headResult?.size ?? options.size ?? 0,\n mimeType: options.mimeType ?? 'application/octet-stream',\n uploadedAt: new Date(),\n }\n }\n\n // ============================================\n // IMultipartProvider - Multipart upload methods\n // ============================================\n\n getBucket(): string {\n return this.config.binding\n }\n\n async headObject(key: string): Promise<HeadObjectResult | null> {\n const obj = await this.bucket.head(key)\n if (!obj) {\n return null\n }\n return {\n size: obj.size,\n contentType: obj.httpMetadata?.contentType,\n metadata: obj.customMetadata,\n }\n }\n\n async deleteObjects(keys: string[]): Promise<DeleteObjectsResult> {\n if (keys.length === 0) {\n return { deleted: 0, errors: [] }\n }\n\n await this.bucket.delete(keys)\n\n return {\n deleted: keys.length,\n errors: [],\n }\n }\n\n async createMultipartUpload(\n key: string,\n options?: CreateMultipartOptions\n ): Promise<CreateMultipartResult> {\n const upload = await this.bucket.createMultipartUpload(key, {\n httpMetadata: {\n contentType: options?.contentType,\n cacheControl: options?.cacheControl,\n },\n customMetadata: options?.metadata,\n })\n\n // Track upload for listMultipartUploads\n await this.bucket.put(\n `${MULTIPART_PREFIX}${upload.uploadId}.json`,\n JSON.stringify({ key: upload.key, uploadId: upload.uploadId, initiated: new Date().toISOString() }),\n { httpMetadata: { contentType: 'application/json' } }\n )\n\n return {\n uploadId: upload.uploadId,\n key: upload.key,\n }\n }\n\n async uploadPart(\n key: string,\n uploadId: string,\n partNumber: number,\n body: Uint8Array\n ): Promise<UploadPartResult> {\n const upload = this.bucket.resumeMultipartUpload(key, uploadId)\n const part = await upload.uploadPart(partNumber, body)\n\n // Track part for listParts\n await this.bucket.put(\n `${PARTS_PREFIX}${uploadId}/${partNumber}`,\n JSON.stringify({ partNumber: part.partNumber, etag: part.etag, size: body.length }),\n { httpMetadata: { contentType: 'application/json' } }\n )\n\n return {\n etag: part.etag,\n partNumber: part.partNumber,\n }\n }\n\n async completeMultipartUpload(\n key: string,\n uploadId: string,\n parts: CompletedPart[]\n ): Promise<CompleteMultipartResult> {\n const upload = this.bucket.resumeMultipartUpload(key, uploadId)\n const result = await upload.complete(\n parts.map((p) => ({\n partNumber: p.partNumber,\n etag: p.etag,\n }))\n )\n\n await this.cleanupTrackingObjects(uploadId)\n\n return {\n key: result.key,\n }\n }\n\n async abortMultipartUpload(key: string, uploadId: string): Promise<void> {\n const upload = this.bucket.resumeMultipartUpload(key, uploadId)\n await upload.abort()\n\n await this.cleanupTrackingObjects(uploadId)\n }\n\n async listParts(\n _key: string,\n uploadId: string,\n partNumberMarker?: string\n ): Promise<ListPartsResult> {\n const prefix = `${PARTS_PREFIX}${uploadId}/`\n const listed = await this.bucket.list({\n prefix,\n cursor: partNumberMarker,\n })\n\n const parts = await Promise.all(\n listed.objects.map(async (obj) => {\n const data = await this.bucket.get(obj.key)\n if (!data) {\n return null\n }\n return JSON.parse(await data.text()) as { partNumber: number; etag: string; size: number }\n })\n )\n\n const validParts = parts\n .filter((p): p is NonNullable<typeof p> => p !== null)\n .sort((a, b) => a.partNumber - b.partNumber)\n\n return {\n parts: validParts,\n isTruncated: listed.truncated,\n nextPartNumberMarker: listed.truncated ? listed.cursor : undefined,\n }\n }\n\n async listMultipartUploads(\n keyMarker?: string,\n _uploadIdMarker?: string\n ): Promise<ListMultipartUploadsResult> {\n const listed = await this.bucket.list({\n prefix: MULTIPART_PREFIX,\n cursor: keyMarker,\n })\n\n const uploads = await Promise.all(\n listed.objects.map(async (obj) => {\n const data = await this.bucket.get(obj.key)\n if (!data) {\n return null\n }\n const parsed = JSON.parse(await data.text()) as { key: string; uploadId: string; initiated: string }\n return {\n key: parsed.key,\n uploadId: parsed.uploadId,\n initiated: new Date(parsed.initiated),\n }\n })\n )\n\n return {\n uploads: uploads.filter((u): u is NonNullable<typeof u> => u !== null),\n isTruncated: listed.truncated,\n nextKeyMarker: listed.truncated ? listed.cursor : undefined,\n nextUploadIdMarker: undefined,\n }\n }\n\n // ============================================\n // Private helpers\n // ============================================\n\n private async cleanupTrackingObjects(uploadId: string): Promise<void> {\n // Delete part tracking objects\n const partsPrefix = `${PARTS_PREFIX}${uploadId}/`\n let cursor: string | undefined\n const keysToDelete: string[] = []\n\n do {\n const listed = await this.bucket.list({ prefix: partsPrefix, cursor })\n for (const obj of listed.objects) {\n keysToDelete.push(obj.key)\n }\n cursor = listed.truncated ? listed.cursor : undefined\n } while (cursor)\n\n // Delete multipart tracking object\n keysToDelete.push(`${MULTIPART_PREFIX}${uploadId}.json`)\n\n if (keysToDelete.length > 0) {\n await this.bucket.delete(keysToDelete)\n }\n }\n}\n"],"mappings":";;;;;AAmBA,MAAM,gBAAgB,IAAI,OAAO;AACjC,MAAM,mBAAmB;AACzB,MAAM,eAAe;;;;;;;;;AAUrB,IAAa,oBAAb,MAA6D;CAIxC;CACA;CACA;CALnB;CAEA,YACE,QACA,QACA,KACA,aACA;EAJiB,KAAA,SAAA;EACA,KAAA,SAAA;EACA,KAAA,MAAA;EAGjB,KAAK,gBAAgB,aAAa,YAAY;CAChD;CAEA,MAAM,OACJ,MACA,MACA,SACuB;EACvB,MAAM,iBAAyC,EAAE,GAAG,QAAQ,SAAS;EACrE,IAAI,QAAQ,SACV,eAAe,YAAY,QAAQ;EAGrC,MAAM,KAAK,OAAO,IAAI,MAAM,MAA+E;GACzG,cAAc,EACZ,aAAa,QAAQ,SACvB;GACA;EACF,CAAC;EAED,OAAO;GACL;GACA,MAAM,KAAK,OAAO;GAClB,UAAU;GACV,MAAM,QAAQ;GACd,UAAU,QAAQ,YAAY;GAC9B,4BAAY,IAAI,KAAK;EACvB;CACF;CAEA,MAAM,SAAS,MAAuC;EACpD,MAAM,MAAM,MAAM,KAAK,OAAO,IAAI,IAAI;EAEtC,IAAI,CAAC,KACH,MAAM,IAAI,aAAa,qCAAqC,KAAK,EAAE;EAIrE,MAAM,UAAU;EAEhB,OAAO;GACL,gBAAgB,QAAQ;GACxB,aAAa,IAAI,cAAc,eAAe;GAC9C,MAAM,IAAI;GACV,UAAU,IAAI;GACd,gBAAgB,QAAQ,KAAK;GAC7B,qBAAqB,QAAQ,MAAM;EACrC;CACF;CAEA,MAAM,OAAO,MAA6B;EACxC,MAAM,KAAK,OAAO,OAAO,IAAI;CAC/B;CAEA,MAAM,OAAO,MAAgC;EAE3C,OAAO,MADY,KAAK,OAAO,KAAK,IAAI,MACxB;CAClB;CAEA,MAAM,gBACJ,MACA,QACA,WAC6B;EAC7B,MAAM,SAAS,KAAK,IAAI;EACxB,IAAI,CAAC,QACH,MAAM,IAAI,aAAa,sDAAsD;EAQ/E,OAAO;GACL,KAAA,MAHgB,QAAQ,GADD,GADJ,KAAK,cAAc,GAAG,KAAK,OAAO,KAAK,GAAG,OAC5B,UAAU,UACJ,QAAQ,EAAE,UAAU,CAAC;GAI5D;GACA,WAAW,IAAI,KAAK,KAAK,IAAI,IAAI,YAAY,GAAI;GACjD;EACF;CACF;CAEA,MAAM,cACJ,MACA,MACA,SACuB;EACvB,MAAM,kBAAkB,MAAM,KAAK,OAAO,sBAAsB,MAAM;GACpE,cAAc,EACZ,aAAa,QAAQ,SACvB;GACA,gBAAgB,QAAQ;EAC1B,CAAC;EAED,MAAM,QAA0B,CAAC;EAEjC,IAAI;GACF,IAAI,gBAAgB,gBAAgB;IAClC,MAAM,SAAU,KAAoC,UAAU;IAC9D,IAAI,SAAS,IAAI,WAAW,CAAC;IAC7B,IAAI,aAAa;IAEjB,OAAO,MAAM;KACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;KAE1C,IAAI,OAAO;MACT,MAAM,YAAY,IAAI,WAAW,OAAO,SAAS,MAAM,MAAM;MAC7D,UAAU,IAAI,QAAQ,CAAC;MACvB,UAAU,IAAI,OAAO,OAAO,MAAM;MAClC,SAAS;KACX;KAEA,OAAO,OAAO,UAAU,eAAe;MACrC,MAAM,WAAW,OAAO,MAAM,GAAG,aAAa;MAC9C,SAAS,OAAO,MAAM,aAAa;MACnC,MAAM,OAAO,MAAM,gBAAgB,WAAW,YAAY,QAAQ;MAClE,MAAM,KAAK,IAAI;MACf;KACF;KAEA,IAAI,MAAM;MAER,IAAI,OAAO,SAAS,GAAG;OACrB,MAAM,OAAO,MAAM,gBAAgB,WAAW,YAAY,MAAM;OAChE,MAAM,KAAK,IAAI;MACjB;MACA;KACF;IACF;GACF,OAAO,IAAI,SAAS,QAAQ,SAAS,KAAA,GAAW;IAE9C,MAAM,OAAO,MAAM,gBAAgB,WAAW,GAAG,IAAqD;IACtG,MAAM,KAAK,IAAI;GACjB;GAEA,MAAM,gBAAgB,SAAS,KAAK;EACtC,SAAS,OAAO;GACd,MAAM,gBAAgB,MAAM;GAC5B,MAAM;EACR;EAGA,MAAM,aAAa,MAAM,KAAK,OAAO,KAAK,IAAI;EAE9C,OAAO;GACL;GACA,MAAM,KAAK,OAAO;GAClB,UAAU;GACV,MAAM,YAAY,QAAQ,QAAQ,QAAQ;GAC1C,UAAU,QAAQ,YAAY;GAC9B,4BAAY,IAAI,KAAK;EACvB;CACF;CAMA,YAAoB;EAClB,OAAO,KAAK,OAAO;CACrB;CAEA,MAAM,WAAW,KAA+C;EAC9D,MAAM,MAAM,MAAM,KAAK,OAAO,KAAK,GAAG;EACtC,IAAI,CAAC,KACH,OAAO;EAET,OAAO;GACL,MAAM,IAAI;GACV,aAAa,IAAI,cAAc;GAC/B,UAAU,IAAI;EAChB;CACF;CAEA,MAAM,cAAc,MAA8C;EAChE,IAAI,KAAK,WAAW,GAClB,OAAO;GAAE,SAAS;GAAG,QAAQ,CAAC;EAAE;EAGlC,MAAM,KAAK,OAAO,OAAO,IAAI;EAE7B,OAAO;GACL,SAAS,KAAK;GACd,QAAQ,CAAC;EACX;CACF;CAEA,MAAM,sBACJ,KACA,SACgC;EAChC,MAAM,SAAS,MAAM,KAAK,OAAO,sBAAsB,KAAK;GAC1D,cAAc;IACZ,aAAa,SAAS;IACtB,cAAc,SAAS;GACzB;GACA,gBAAgB,SAAS;EAC3B,CAAC;EAGD,MAAM,KAAK,OAAO,IAChB,GAAG,mBAAmB,OAAO,SAAS,QACtC,KAAK,UAAU;GAAE,KAAK,OAAO;GAAK,UAAU,OAAO;GAAU,4BAAW,IAAI,KAAK,GAAE,YAAY;EAAE,CAAC,GAClG,EAAE,cAAc,EAAE,aAAa,mBAAmB,EAAE,CACtD;EAEA,OAAO;GACL,UAAU,OAAO;GACjB,KAAK,OAAO;EACd;CACF;CAEA,MAAM,WACJ,KACA,UACA,YACA,MAC2B;EAE3B,MAAM,OAAO,MADE,KAAK,OAAO,sBAAsB,KAAK,QAC9B,EAAE,WAAW,YAAY,IAAI;EAGrD,MAAM,KAAK,OAAO,IAChB,GAAG,eAAe,SAAS,GAAG,cAC9B,KAAK,UAAU;GAAE,YAAY,KAAK;GAAY,MAAM,KAAK;GAAM,MAAM,KAAK;EAAO,CAAC,GAClF,EAAE,cAAc,EAAE,aAAa,mBAAmB,EAAE,CACtD;EAEA,OAAO;GACL,MAAM,KAAK;GACX,YAAY,KAAK;EACnB;CACF;CAEA,MAAM,wBACJ,KACA,UACA,OACkC;EAElC,MAAM,SAAS,MADA,KAAK,OAAO,sBAAsB,KAAK,QAC5B,EAAE,SAC1B,MAAM,KAAK,OAAO;GAChB,YAAY,EAAE;GACd,MAAM,EAAE;EACV,EAAE,CACJ;EAEA,MAAM,KAAK,uBAAuB,QAAQ;EAE1C,OAAO,EACL,KAAK,OAAO,IACd;CACF;CAEA,MAAM,qBAAqB,KAAa,UAAiC;EAEvE,MADe,KAAK,OAAO,sBAAsB,KAAK,QAC3C,EAAE,MAAM;EAEnB,MAAM,KAAK,uBAAuB,QAAQ;CAC5C;CAEA,MAAM,UACJ,MACA,UACA,kBAC0B;EAC1B,MAAM,SAAS,GAAG,eAAe,SAAS;EAC1C,MAAM,SAAS,MAAM,KAAK,OAAO,KAAK;GACpC;GACA,QAAQ;EACV,CAAC;EAgBD,OAAO;GACL,QALiB,MAVC,QAAQ,IAC1B,OAAO,QAAQ,IAAI,OAAO,QAAQ;IAChC,MAAM,OAAO,MAAM,KAAK,OAAO,IAAI,IAAI,GAAG;IAC1C,IAAI,CAAC,MACH,OAAO;IAET,OAAO,KAAK,MAAM,MAAM,KAAK,KAAK,CAAC;GACrC,CAAC,CACH,GAGG,QAAQ,MAAkC,MAAM,IAAI,EACpD,MAAM,GAAG,MAAM,EAAE,aAAa,EAAE,UAGjB;GAChB,aAAa,OAAO;GACpB,sBAAsB,OAAO,YAAY,OAAO,SAAS,KAAA;EAC3D;CACF;CAEA,MAAM,qBACJ,WACA,iBACqC;EACrC,MAAM,SAAS,MAAM,KAAK,OAAO,KAAK;GACpC,QAAQ;GACR,QAAQ;EACV,CAAC;EAiBD,OAAO;GACL,UAAS,MAhBW,QAAQ,IAC5B,OAAO,QAAQ,IAAI,OAAO,QAAQ;IAChC,MAAM,OAAO,MAAM,KAAK,OAAO,IAAI,IAAI,GAAG;IAC1C,IAAI,CAAC,MACH,OAAO;IAET,MAAM,SAAS,KAAK,MAAM,MAAM,KAAK,KAAK,CAAC;IAC3C,OAAO;KACL,KAAK,OAAO;KACZ,UAAU,OAAO;KACjB,WAAW,IAAI,KAAK,OAAO,SAAS;IACtC;GACF,CAAC,CACH,GAGmB,QAAQ,MAAkC,MAAM,IAAI;GACrE,aAAa,OAAO;GACpB,eAAe,OAAO,YAAY,OAAO,SAAS,KAAA;GAClD,oBAAoB,KAAA;EACtB;CACF;CAMA,MAAc,uBAAuB,UAAiC;EAEpE,MAAM,cAAc,GAAG,eAAe,SAAS;EAC/C,IAAI;EACJ,MAAM,eAAyB,CAAC;EAEhC,GAAG;GACD,MAAM,SAAS,MAAM,KAAK,OAAO,KAAK;IAAE,QAAQ;IAAa;GAAO,CAAC;GACrE,KAAK,MAAM,OAAO,OAAO,SACvB,aAAa,KAAK,IAAI,GAAG;GAE3B,SAAS,OAAO,YAAY,OAAO,SAAS,KAAA;EAC9C,SAAS;EAGT,aAAa,KAAK,GAAG,mBAAmB,SAAS,MAAM;EAEvD,IAAI,aAAa,SAAS,GACxB,MAAM,KAAK,OAAO,OAAO,YAAY;CAEzC;AACF"}
1
+ {"version":3,"file":"r2-storage.provider-BoZmR6Ut.mjs","names":[],"sources":["../src/storage/providers/r2-storage.provider.ts"],"sourcesContent":["import { type StratalEnv } from '../../env'\nimport { signUrl } from '../../router/signed-url'\nimport type { DownloadResult, PresignedUrlResult, UploadOptions, UploadResult } from '../contracts'\nimport { StorageError } from '../storage.error'\nimport type { StorageEntry, StorageRouteConfig } from '../types'\nimport type {\n CompletedPart,\n CompleteMultipartResult,\n CreateMultipartOptions,\n CreateMultipartResult,\n DeleteObjectsResult,\n HeadObjectResult,\n IMultipartProvider,\n ListMultipartUploadsResult,\n ListPartsResult,\n UploadPartResult,\n} from './multipart-provider.interface'\nimport type { StreamingBlobPayloadInputTypes } from './storage-provider.interface'\n\nconst MIN_PART_SIZE = 5 * 1024 * 1024 // 5MB\nconst MULTIPART_PREFIX = '__multipart/'\nconst PARTS_PREFIX = '__parts/'\n\n/**\n * R2 Storage Provider\n * Implements storage operations using native Cloudflare R2 bindings\n *\n * Implements IMultipartProvider for multipart upload support needed by TUS.\n * Tracks multipart upload metadata using companion R2 objects so that\n * listParts/listMultipartUploads work in all environments (local, test, prod).\n */\nexport class R2StorageProvider implements IMultipartProvider {\n private readonly routeBasePath: string\n\n constructor(\n private readonly config: StorageEntry,\n private readonly bucket: R2Bucket,\n private readonly env: StratalEnv,\n routeConfig?: StorageRouteConfig\n ) {\n this.routeBasePath = routeConfig?.basePath ?? '/storage'\n }\n\n async upload(\n body: StreamingBlobPayloadInputTypes,\n path: string,\n options: UploadOptions\n ): Promise<UploadResult> {\n const customMetadata: Record<string, string> = { ...options.metadata }\n if (options.tagging) {\n customMetadata['x-tags'] = options.tagging\n }\n\n await this.bucket.put(path, body as ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | null, {\n httpMetadata: {\n contentType: options.mimeType,\n },\n customMetadata,\n })\n\n return {\n path,\n disk: this.config.disk,\n fullPath: path,\n size: options.size,\n mimeType: options.mimeType ?? 'application/octet-stream',\n uploadedAt: new Date(),\n }\n }\n\n async download(path: string): Promise<DownloadResult> {\n const obj = await this.bucket.get(path)\n\n if (!obj) {\n throw new StorageError(`Storage object not found at path \"${path}\"`)\n }\n\n // R2ObjectBody has body, text(), arrayBuffer(), etc.\n const objBody = obj\n\n return {\n toStream: () => objBody.body as ReadableStream<Uint8Array>,\n contentType: obj.httpMetadata?.contentType ?? 'application/octet-stream',\n size: obj.size,\n metadata: obj.customMetadata,\n toString: () => objBody.text(),\n toArrayBuffer: () => objBody.bytes(),\n }\n }\n\n async delete(path: string): Promise<void> {\n await this.bucket.delete(path)\n }\n\n async exists(path: string): Promise<boolean> {\n const head = await this.bucket.head(path)\n return head !== null\n }\n\n async getPresignedUrl(\n path: string,\n method: 'GET' | 'PUT' | 'DELETE' | 'HEAD',\n expiresIn: number\n ): Promise<PresignedUrlResult> {\n const secret = this.env.APP_SECRET;\n if (!secret) {\n throw new StorageError('APP_SECRET is required for generating presigned URLs')\n }\n\n // Build a relative URL pointing to the StorageController route\n const routePath = `${this.routeBasePath}/${this.config.disk}/${path}`\n const urlWithMethod = `${routePath}?method=${method}`\n const url = await signUrl(urlWithMethod, secret, { expiresIn })\n\n return {\n url,\n expiresIn,\n expiresAt: new Date(Date.now() + expiresIn * 1000),\n method,\n }\n }\n\n async chunkedUpload(\n body: StreamingBlobPayloadInputTypes,\n path: string,\n options: Omit<UploadOptions, 'size'> & { size?: number }\n ): Promise<UploadResult> {\n const multipartUpload = await this.bucket.createMultipartUpload(path, {\n httpMetadata: {\n contentType: options.mimeType,\n },\n customMetadata: options.metadata,\n })\n\n const parts: R2UploadedPart[] = []\n\n try {\n if (body instanceof ReadableStream) {\n const reader = (body as ReadableStream<Uint8Array>).getReader()\n let buffer = new Uint8Array(0)\n let partNumber = 1\n\n while (true) {\n const { done, value } = await reader.read()\n\n if (value) {\n const newBuffer = new Uint8Array(buffer.length + value.length)\n newBuffer.set(buffer, 0)\n newBuffer.set(value, buffer.length)\n buffer = newBuffer\n }\n\n while (buffer.length >= MIN_PART_SIZE) {\n const partData = buffer.slice(0, MIN_PART_SIZE)\n buffer = buffer.slice(MIN_PART_SIZE)\n const part = await multipartUpload.uploadPart(partNumber, partData)\n parts.push(part)\n partNumber++\n }\n\n if (done) {\n // Upload remaining buffer as final part\n if (buffer.length > 0) {\n const part = await multipartUpload.uploadPart(partNumber, buffer)\n parts.push(part)\n }\n break\n }\n }\n } else if (body !== null && body !== undefined) {\n // Non-stream body: upload as single part\n const part = await multipartUpload.uploadPart(1, body as ArrayBuffer | ArrayBufferView | string | Blob)\n parts.push(part)\n }\n\n await multipartUpload.complete(parts)\n } catch (error) {\n await multipartUpload.abort()\n throw error\n }\n\n // Get actual size via head\n const headResult = await this.bucket.head(path)\n\n return {\n path,\n disk: this.config.disk,\n fullPath: path,\n size: headResult?.size ?? options.size ?? 0,\n mimeType: options.mimeType ?? 'application/octet-stream',\n uploadedAt: new Date(),\n }\n }\n\n // ============================================\n // IMultipartProvider - Multipart upload methods\n // ============================================\n\n getBucket(): string {\n return this.config.binding\n }\n\n async headObject(key: string): Promise<HeadObjectResult | null> {\n const obj = await this.bucket.head(key)\n if (!obj) {\n return null\n }\n return {\n size: obj.size,\n contentType: obj.httpMetadata?.contentType,\n metadata: obj.customMetadata,\n }\n }\n\n async deleteObjects(keys: string[]): Promise<DeleteObjectsResult> {\n if (keys.length === 0) {\n return { deleted: 0, errors: [] }\n }\n\n await this.bucket.delete(keys)\n\n return {\n deleted: keys.length,\n errors: [],\n }\n }\n\n async createMultipartUpload(\n key: string,\n options?: CreateMultipartOptions\n ): Promise<CreateMultipartResult> {\n const upload = await this.bucket.createMultipartUpload(key, {\n httpMetadata: {\n contentType: options?.contentType,\n cacheControl: options?.cacheControl,\n },\n customMetadata: options?.metadata,\n })\n\n // Track upload for listMultipartUploads\n await this.bucket.put(\n `${MULTIPART_PREFIX}${upload.uploadId}.json`,\n JSON.stringify({ key: upload.key, uploadId: upload.uploadId, initiated: new Date().toISOString() }),\n { httpMetadata: { contentType: 'application/json' } }\n )\n\n return {\n uploadId: upload.uploadId,\n key: upload.key,\n }\n }\n\n async uploadPart(\n key: string,\n uploadId: string,\n partNumber: number,\n body: Uint8Array\n ): Promise<UploadPartResult> {\n const upload = this.bucket.resumeMultipartUpload(key, uploadId)\n const part = await upload.uploadPart(partNumber, body)\n\n // Track part for listParts\n await this.bucket.put(\n `${PARTS_PREFIX}${uploadId}/${partNumber}`,\n JSON.stringify({ partNumber: part.partNumber, etag: part.etag, size: body.length }),\n { httpMetadata: { contentType: 'application/json' } }\n )\n\n return {\n etag: part.etag,\n partNumber: part.partNumber,\n }\n }\n\n async completeMultipartUpload(\n key: string,\n uploadId: string,\n parts: CompletedPart[]\n ): Promise<CompleteMultipartResult> {\n const upload = this.bucket.resumeMultipartUpload(key, uploadId)\n const result = await upload.complete(\n parts.map((p) => ({\n partNumber: p.partNumber,\n etag: p.etag,\n }))\n )\n\n await this.cleanupTrackingObjects(uploadId)\n\n return {\n key: result.key,\n }\n }\n\n async abortMultipartUpload(key: string, uploadId: string): Promise<void> {\n const upload = this.bucket.resumeMultipartUpload(key, uploadId)\n await upload.abort()\n\n await this.cleanupTrackingObjects(uploadId)\n }\n\n async listParts(\n _key: string,\n uploadId: string,\n partNumberMarker?: string\n ): Promise<ListPartsResult> {\n const prefix = `${PARTS_PREFIX}${uploadId}/`\n const listed = await this.bucket.list({\n prefix,\n cursor: partNumberMarker,\n })\n\n const parts = await Promise.all(\n listed.objects.map(async (obj) => {\n const data = await this.bucket.get(obj.key)\n if (!data) {\n return null\n }\n return JSON.parse(await data.text()) as { partNumber: number; etag: string; size: number }\n })\n )\n\n const validParts = parts\n .filter((p): p is NonNullable<typeof p> => p !== null)\n .sort((a, b) => a.partNumber - b.partNumber)\n\n return {\n parts: validParts,\n isTruncated: listed.truncated,\n nextPartNumberMarker: listed.truncated ? listed.cursor : undefined,\n }\n }\n\n async listMultipartUploads(\n keyMarker?: string,\n _uploadIdMarker?: string\n ): Promise<ListMultipartUploadsResult> {\n const listed = await this.bucket.list({\n prefix: MULTIPART_PREFIX,\n cursor: keyMarker,\n })\n\n const uploads = await Promise.all(\n listed.objects.map(async (obj) => {\n const data = await this.bucket.get(obj.key)\n if (!data) {\n return null\n }\n const parsed = JSON.parse(await data.text()) as { key: string; uploadId: string; initiated: string }\n return {\n key: parsed.key,\n uploadId: parsed.uploadId,\n initiated: new Date(parsed.initiated),\n }\n })\n )\n\n return {\n uploads: uploads.filter((u): u is NonNullable<typeof u> => u !== null),\n isTruncated: listed.truncated,\n nextKeyMarker: listed.truncated ? listed.cursor : undefined,\n nextUploadIdMarker: undefined,\n }\n }\n\n // ============================================\n // Private helpers\n // ============================================\n\n private async cleanupTrackingObjects(uploadId: string): Promise<void> {\n // Delete part tracking objects\n const partsPrefix = `${PARTS_PREFIX}${uploadId}/`\n let cursor: string | undefined\n const keysToDelete: string[] = []\n\n do {\n const listed = await this.bucket.list({ prefix: partsPrefix, cursor })\n for (const obj of listed.objects) {\n keysToDelete.push(obj.key)\n }\n cursor = listed.truncated ? listed.cursor : undefined\n } while (cursor)\n\n // Delete multipart tracking object\n keysToDelete.push(`${MULTIPART_PREFIX}${uploadId}.json`)\n\n if (keysToDelete.length > 0) {\n await this.bucket.delete(keysToDelete)\n }\n }\n}\n"],"mappings":"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"}