stratal 0.0.27 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +912 -0
- package/README.md +96 -14
- package/dist/bin/cloudflare-workers-loader.mjs +18 -3
- package/dist/bin/cloudflare-workers-loader.mjs.map +1 -1
- package/dist/bin/quarry.mjs +73 -12
- package/dist/bin/quarry.mjs.map +1 -1
- package/dist/cache/index.d.mts +8 -9
- package/dist/cache/index.d.mts.map +1 -1
- package/dist/cache/index.mjs +56 -17
- package/dist/cache/index.mjs.map +1 -1
- package/dist/{cache.service-uElmBtdS.d.mts → cache.service-Dg0Hj0gp.d.mts} +27 -12
- package/dist/cache.service-Dg0Hj0gp.d.mts.map +1 -0
- package/dist/{command-DoBD2Cwl.d.mts → command-BXMccV_9.d.mts} +6 -3
- package/dist/command-BXMccV_9.d.mts.map +1 -0
- package/dist/{command-BvmUAPPQ.mjs → command-Bb820B6W.mjs} +36 -13
- package/dist/command-Bb820B6W.mjs.map +1 -0
- package/dist/common.schemas-BigQCwYB.mjs +89 -0
- package/dist/common.schemas-BigQCwYB.mjs.map +1 -0
- package/dist/config/index.d.mts +15 -15
- package/dist/config/index.d.mts.map +1 -1
- package/dist/config/index.mjs +4 -6
- package/dist/config/index.mjs.map +1 -1
- package/dist/{consumer-registry-D3iMTSdy.d.mts → consumer-registry-i9oCdarv.d.mts} +2 -3
- package/dist/consumer-registry-i9oCdarv.d.mts.map +1 -0
- package/dist/{container-storage-BmOJ4_Na.mjs → container-storage-DfF7SXdV.mjs} +11 -1
- package/dist/container-storage-DfF7SXdV.mjs.map +1 -0
- package/dist/{controller.decorator-YSTPQntu.mjs → controller.decorator-BB0JcwMg.mjs} +18 -9
- package/dist/controller.decorator-BB0JcwMg.mjs.map +1 -0
- package/dist/cron/index.d.mts +6 -7
- package/dist/cron/index.d.mts.map +1 -1
- package/dist/cron/index.mjs +1 -1
- package/dist/{cron-job-NesZRk8F.d.mts → cron-job-DAgNw4Bn.d.mts} +1 -2
- package/dist/cron-job-DAgNw4Bn.d.mts.map +1 -0
- package/dist/{cron.module-C81HTzR7.mjs → cron.module-Dnyl9sFk.mjs} +6 -7
- package/dist/{cron.module-C81HTzR7.mjs.map → cron.module-Dnyl9sFk.mjs.map} +1 -1
- package/dist/{decorate-CuAoSZvs.mjs → decorate-C79yt3Wt.mjs} +2 -2
- package/dist/di/index.d.mts +1 -1
- package/dist/di/index.mjs +2 -3
- package/dist/{di-D7qmrAir.mjs → di-DQ3xbjgf.mjs} +48 -6
- package/dist/di-DQ3xbjgf.mjs.map +1 -0
- package/dist/email/index.d.mts +158 -133
- package/dist/email/index.d.mts.map +1 -1
- package/dist/email/index.mjs +66 -78
- package/dist/email/index.mjs.map +1 -1
- package/dist/{en-CDZBMcc1.mjs → en-CCL84z4k.mjs} +2 -2
- package/dist/{en-CDZBMcc1.mjs.map → en-CCL84z4k.mjs.map} +1 -1
- package/dist/env-ug22bJj7.d.mts.map +1 -1
- package/dist/errors/index.d.mts +1 -1
- package/dist/errors/index.mjs +3 -3
- package/dist/{errors-C01O2T-n.mjs → errors-CrOXI_y5.mjs} +6 -8
- package/dist/errors-CrOXI_y5.mjs.map +1 -0
- package/dist/events/index.d.mts +8 -9
- package/dist/events/index.d.mts.map +1 -1
- package/dist/events/index.mjs +1 -1
- package/dist/{events-BhEQuT1X.mjs → events-GtwGEHyT.mjs} +5 -6
- package/dist/events-GtwGEHyT.mjs.map +1 -0
- package/dist/{exception-context-D-kvney-.mjs → exception-context-CdnGTQNF.mjs} +13 -7
- package/dist/exception-context-CdnGTQNF.mjs.map +1 -0
- package/dist/{gateway-context-m7kEzRa2.mjs → gateway-context-Cmc1NRS0.mjs} +7 -7
- package/dist/{gateway-context-m7kEzRa2.mjs.map → gateway-context-Cmc1NRS0.mjs.map} +1 -1
- package/dist/gateway-mode-0rUaeW3g.mjs +53 -0
- package/dist/gateway-mode-0rUaeW3g.mjs.map +1 -0
- package/dist/guards/index.d.mts +38 -20
- package/dist/guards/index.d.mts.map +1 -1
- package/dist/guards/index.mjs +2 -2
- package/dist/{guards-DALPXy3_.mjs → guards-DOGly9IF.mjs} +34 -11
- package/dist/guards-DOGly9IF.mjs.map +1 -0
- package/dist/hono-app-IhdqL0Fk.mjs +399 -0
- package/dist/hono-app-IhdqL0Fk.mjs.map +1 -0
- package/dist/{http-method.decorator-BljM8BDj.mjs → http-method.decorator-BDi9LTWj.mjs} +9 -8
- package/dist/http-method.decorator-BDi9LTWj.mjs.map +1 -0
- package/dist/i18n/index.d.mts +26 -137
- package/dist/i18n/index.d.mts.map +1 -1
- package/dist/i18n/index.mjs +4 -5
- package/dist/i18n/index.mjs.map +1 -1
- package/dist/i18n/messages/en/index.d.mts +1 -1
- package/dist/i18n/messages/en/index.mjs +1 -1
- package/dist/i18n/utils/index.d.mts +1 -2
- package/dist/i18n/utils/index.d.mts.map +1 -1
- package/dist/i18n/validation/index.d.mts +80 -5
- package/dist/i18n/validation/index.d.mts.map +1 -0
- package/dist/i18n/validation/index.mjs +4 -5
- package/dist/{i18n.module-B2DvWUPa.mjs → i18n.module-Bm3xlViA.mjs} +28 -13
- package/dist/i18n.module-Bm3xlViA.mjs.map +1 -0
- package/dist/index-0ItCjaqw.d.mts.map +1 -1
- package/dist/{index-CNuFQSNj.d.mts → index-3HJfhLlE.d.mts} +9 -11
- package/dist/index-3HJfhLlE.d.mts.map +1 -0
- package/dist/{index-uybm0bhQ.d.mts → index-B2WtEagR.d.mts} +1187 -234
- package/dist/index-B2WtEagR.d.mts.map +1 -0
- package/dist/index-BUt92sAE.d.mts.map +1 -1
- package/dist/index-CAHNCpvr.d.mts +203 -0
- package/dist/index-CAHNCpvr.d.mts.map +1 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +2 -2
- package/dist/{is-command-CEPO9n8c.mjs → is-command-CSVv14wh.mjs} +2 -2
- package/dist/{is-command-CEPO9n8c.mjs.map → is-command-CSVv14wh.mjs.map} +1 -1
- package/dist/{is-seeder-Gvh_AM71.mjs → is-seeder-gLgXYbqC.mjs} +2 -2
- package/dist/is-seeder-gLgXYbqC.mjs.map +1 -0
- package/dist/{lazy-module-loader-M6YKudNL.d.mts → lazy-module-loader-C3OHoUnR.d.mts} +4 -5
- package/dist/lazy-module-loader-C3OHoUnR.d.mts.map +1 -0
- package/dist/limit-BE0HNAF6.mjs +116 -0
- package/dist/limit-BE0HNAF6.mjs.map +1 -0
- package/dist/locale-path.service-BqzDIrgy.mjs +213 -0
- package/dist/locale-path.service-BqzDIrgy.mjs.map +1 -0
- package/dist/locale-url-nZrZxqJP.mjs.map +1 -1
- package/dist/{locale-url.service-6bgia24_.mjs → locale-url.service-H8PH6GGh.mjs} +15 -5
- package/dist/{locale-url.service-6bgia24_.mjs.map → locale-url.service-H8PH6GGh.mjs.map} +1 -1
- package/dist/logger/index.mjs +3 -5
- package/dist/logger/index.mjs.map +1 -1
- package/dist/macroable-cvDTFZ_A.mjs.map +1 -1
- package/dist/metadata-Ba8Fxs1F.mjs +48 -0
- package/dist/metadata-Ba8Fxs1F.mjs.map +1 -0
- package/dist/module/index.d.mts +8 -9
- package/dist/module/index.d.mts.map +1 -1
- package/dist/module/index.mjs +3 -4
- package/dist/module/index.mjs.map +1 -1
- package/dist/{module-registry-NxX5O0Qk.mjs → module-registry-lkQPDuQe.mjs} +17 -15
- package/dist/module-registry-lkQPDuQe.mjs.map +1 -0
- package/dist/{module.decorator-CYHY6pG5.mjs → module.decorator-CYbrOlX9.mjs} +2 -2
- package/dist/{module.decorator-CYHY6pG5.mjs.map → module.decorator-CYbrOlX9.mjs.map} +1 -1
- package/dist/openapi/index.d.mts +19 -16
- package/dist/openapi/index.d.mts.map +1 -1
- package/dist/openapi/index.mjs +2 -2
- package/dist/{openapi-CMwuCp31.mjs → openapi-DBfBa5Qh.mjs} +41 -65
- package/dist/openapi-DBfBa5Qh.mjs.map +1 -0
- package/dist/openapi-generator-CCwiu8AU.mjs +134 -0
- package/dist/openapi-generator-CCwiu8AU.mjs.map +1 -0
- package/dist/{openapi-tools.service-BC5EC3R3.mjs → openapi-tools.service-VKZJlTIX.mjs} +12 -4
- package/dist/openapi-tools.service-VKZJlTIX.mjs.map +1 -0
- package/dist/openapi.service-Ds-R1tih.d.mts +43 -0
- package/dist/openapi.service-Ds-R1tih.d.mts.map +1 -0
- package/dist/quarry/index.d.mts +10 -11
- package/dist/quarry/index.d.mts.map +1 -1
- package/dist/quarry/index.mjs +4 -4
- package/dist/quarry/runner.d.mts +19 -20
- package/dist/quarry/runner.d.mts.map +1 -1
- package/dist/quarry/runner.mjs +16 -13
- package/dist/quarry/runner.mjs.map +1 -1
- package/dist/{quarry-registry-DRnV-DDa.d.mts → quarry-registry-Ci4k58FZ.d.mts} +3 -4
- package/dist/quarry-registry-Ci4k58FZ.d.mts.map +1 -0
- package/dist/{quarry.module-CcGxU2dJ.mjs → quarry.module-BssRR8f0.mjs} +20 -37
- package/dist/quarry.module-BssRR8f0.mjs.map +1 -0
- package/dist/queue/index.d.mts +2 -2
- package/dist/queue/index.mjs +3 -3
- package/dist/queue/index.mjs.map +1 -1
- package/dist/{queue.module-CEs4_kEM.mjs → queue.module-DnAdSbR-.mjs} +40 -41
- package/dist/queue.module-DnAdSbR-.mjs.map +1 -0
- package/dist/{r2-storage.provider-BoZmR6Ut.mjs → r2-storage.provider-wU_6api4.mjs} +45 -6
- package/dist/r2-storage.provider-wU_6api4.mjs.map +1 -0
- package/dist/{rate-limit.decorator-Djs4oYDB.mjs → rate-limit.decorator-PYQ9EYqs.mjs} +10 -5
- package/dist/{rate-limit.decorator-Djs4oYDB.mjs.map → rate-limit.decorator-PYQ9EYqs.mjs.map} +1 -1
- package/dist/rate-limiter/index.d.mts +64 -19
- package/dist/rate-limiter/index.d.mts.map +1 -1
- package/dist/rate-limiter/index.mjs +67 -83
- package/dist/rate-limiter/index.mjs.map +1 -1
- package/dist/response-cache/index.d.mts +326 -0
- package/dist/response-cache/index.d.mts.map +1 -0
- package/dist/response-cache/index.mjs +511 -0
- package/dist/response-cache/index.mjs.map +1 -0
- package/dist/response-cache.error-B4D0zy9W.mjs +24 -0
- package/dist/response-cache.error-B4D0zy9W.mjs.map +1 -0
- package/dist/response-headers-BqA57S_X.mjs +158 -0
- package/dist/response-headers-BqA57S_X.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/route-cache-binding-CrLh8tB6.mjs +390 -0
- package/dist/route-cache-binding-CrLh8tB6.mjs.map +1 -0
- package/dist/route-metadata-QQhbed1i.mjs +17 -0
- package/dist/route-metadata-QQhbed1i.mjs.map +1 -0
- package/dist/{route-name-DGoBOfPg.mjs → route-name-BslSbmMP.mjs} +15 -11
- package/dist/route-name-BslSbmMP.mjs.map +1 -0
- package/dist/route-registration.service-COKBZTsn.mjs +1265 -0
- package/dist/route-registration.service-COKBZTsn.mjs.map +1 -0
- package/dist/{route-registry-BvLJisvK.mjs → route-registry-DFz9_-N6.mjs} +6 -6
- package/dist/{route-registry-BvLJisvK.mjs.map → route-registry-DFz9_-N6.mjs.map} +1 -1
- package/dist/router/index.d.mts +2 -2
- package/dist/router/index.mjs +16 -15
- package/dist/{router-resolver-sUV_jTrU.mjs → router-resolver-B-dMSMIG.mjs} +7 -3
- package/dist/router-resolver-B-dMSMIG.mjs.map +1 -0
- package/dist/{router-DwyqEXgf.mjs → router-wlsVkGkM.mjs} +5 -14
- package/dist/{router-DwyqEXgf.mjs.map → router-wlsVkGkM.mjs.map} +1 -1
- package/dist/seeder/index.d.mts +12 -13
- package/dist/seeder/index.d.mts.map +1 -1
- package/dist/seeder/index.mjs +4 -4
- package/dist/{seeder-BPGY5rUb.mjs → seeder-BrAk7XIj.mjs} +5 -6
- package/dist/{seeder-BPGY5rUb.mjs.map → seeder-BrAk7XIj.mjs.map} +1 -1
- package/dist/{seeder-registry-DEvCycsT.mjs → seeder-registry-Bo5BgLbC.mjs} +5 -6
- package/dist/{seeder-registry-DEvCycsT.mjs.map → seeder-registry-Bo5BgLbC.mjs.map} +1 -1
- package/dist/{seeder.module-CIwQbdN4.mjs → seeder.module-DOoVgTsK.mjs} +5 -5
- package/dist/{seeder.module-CIwQbdN4.mjs.map → seeder.module-DOoVgTsK.mjs.map} +1 -1
- package/dist/signed-url-DIU0sK_6.mjs.map +1 -1
- package/dist/storage/index.d.mts +63 -13
- package/dist/storage/index.d.mts.map +1 -1
- package/dist/storage/index.mjs +2 -2
- package/dist/storage/providers/index.d.mts +18 -16
- package/dist/storage/providers/index.d.mts.map +1 -1
- package/dist/storage/providers/index.mjs +1 -1
- package/dist/{storage-C30X81CS.mjs → storage-DBWVYMnn.mjs} +167 -49
- package/dist/storage-DBWVYMnn.mjs.map +1 -0
- package/dist/storage-provider.interface-CinHpjl1.d.mts +315 -0
- package/dist/storage-provider.interface-CinHpjl1.d.mts.map +1 -0
- package/dist/storage.error-Ao38Qdjx.mjs +7 -0
- package/dist/storage.error-Ao38Qdjx.mjs.map +1 -0
- package/dist/{stratal-D5j_I14G.d.mts → stratal-Bs1_E8GQ.d.mts} +13 -3
- package/dist/stratal-Bs1_E8GQ.d.mts.map +1 -0
- package/dist/{stratal-BL6FKUM_.mjs → stratal-CiwNpWy2.mjs} +56 -23
- package/dist/stratal-CiwNpWy2.mjs.map +1 -0
- package/dist/{tiered-cache.service-Dv3BhxxE.d.mts → tiered-cache.service-Bz8bbvyb.d.mts} +16 -3
- package/dist/tiered-cache.service-Bz8bbvyb.d.mts.map +1 -0
- package/dist/trailing-slash-2SctvePW.mjs.map +1 -1
- package/dist/types-CmV_9xBD.d.mts.map +1 -1
- package/dist/{uri-iwofWJ_T.mjs → uri-BrOq148R.mjs} +13 -9
- package/dist/{uri-iwofWJ_T.mjs.map → uri-BrOq148R.mjs.map} +1 -1
- package/dist/{usage-generator-DAWYasuP.mjs → usage-generator-BW7EcO-C.mjs} +3 -3
- package/dist/{usage-generator-DAWYasuP.mjs.map → usage-generator-BW7EcO-C.mjs.map} +1 -1
- package/dist/{zod-eKqqhZ5_.mjs → validation-CZ_B0aXY.mjs} +38 -17
- package/dist/validation-CZ_B0aXY.mjs.map +1 -0
- package/dist/{validation.context-CRvmrhq7.mjs → validation.context-CdbwQWIr.mjs} +2 -2
- package/dist/{validation.context-CRvmrhq7.mjs.map → validation.context-CdbwQWIr.mjs.map} +1 -1
- package/dist/{versioning.service-CCa2oYMJ.mjs → versioning.service-DnF_4Lnd.mjs} +5 -5
- package/dist/{versioning.service-CCa2oYMJ.mjs.map → versioning.service-DnF_4Lnd.mjs.map} +1 -1
- package/dist/websocket/index.d.mts +12 -13
- package/dist/websocket/index.d.mts.map +1 -1
- package/dist/websocket/index.mjs +1 -1
- package/dist/workers/index.d.mts +70 -7
- package/dist/workers/index.d.mts.map +1 -1
- package/dist/workers/index.mjs +72 -3
- package/dist/workers/index.mjs.map +1 -1
- package/dist/{zod-wecrEVAs.d.mts → zod-BGisqLs3.d.mts} +9 -16
- package/dist/zod-BGisqLs3.d.mts.map +1 -0
- package/package.json +33 -25
- package/dist/cache.service-uElmBtdS.d.mts.map +0 -1
- package/dist/chunk-BBjsoOtd.mjs +0 -27
- package/dist/command-BvmUAPPQ.mjs.map +0 -1
- package/dist/command-DoBD2Cwl.d.mts.map +0 -1
- package/dist/consumer-registry-D3iMTSdy.d.mts.map +0 -1
- package/dist/container-storage-BmOJ4_Na.mjs.map +0 -1
- package/dist/controller.decorator-YSTPQntu.mjs.map +0 -1
- package/dist/cron-job-NesZRk8F.d.mts.map +0 -1
- package/dist/di-D7qmrAir.mjs.map +0 -1
- package/dist/errors-C01O2T-n.mjs.map +0 -1
- package/dist/events-BhEQuT1X.mjs.map +0 -1
- package/dist/exception-context-D-kvney-.mjs.map +0 -1
- package/dist/guards-DALPXy3_.mjs.map +0 -1
- package/dist/hono-app-COAgmutc.mjs +0 -168
- package/dist/hono-app-COAgmutc.mjs.map +0 -1
- package/dist/http-method.decorator-BljM8BDj.mjs.map +0 -1
- package/dist/i18n.module-B2DvWUPa.mjs.map +0 -1
- package/dist/index-CNuFQSNj.d.mts.map +0 -1
- package/dist/index-DtBNIFuP.d.mts +0 -42
- package/dist/index-DtBNIFuP.d.mts.map +0 -1
- package/dist/index-HgOLNruQ.d.mts +0 -203
- package/dist/index-HgOLNruQ.d.mts.map +0 -1
- package/dist/index-uybm0bhQ.d.mts.map +0 -1
- package/dist/is-seeder-Gvh_AM71.mjs.map +0 -1
- package/dist/lazy-module-loader-M6YKudNL.d.mts.map +0 -1
- package/dist/locale-path.service-CH0CaxwH.mjs +0 -165
- package/dist/locale-path.service-CH0CaxwH.mjs.map +0 -1
- package/dist/metadata-DzzprcID.mjs +0 -39
- package/dist/metadata-DzzprcID.mjs.map +0 -1
- package/dist/module-registry-NxX5O0Qk.mjs.map +0 -1
- package/dist/openapi-CMwuCp31.mjs.map +0 -1
- package/dist/openapi-tools.service-BC5EC3R3.mjs.map +0 -1
- package/dist/openapi.service-2rvJBCEg.d.mts +0 -50
- package/dist/openapi.service-2rvJBCEg.d.mts.map +0 -1
- package/dist/quarry-registry-DRnV-DDa.d.mts.map +0 -1
- package/dist/quarry.module-CcGxU2dJ.mjs.map +0 -1
- package/dist/queue.module-CEs4_kEM.mjs.map +0 -1
- package/dist/r2-storage.provider-BoZmR6Ut.mjs.map +0 -1
- package/dist/route-name-DGoBOfPg.mjs.map +0 -1
- package/dist/route-registration.service-CDPQKpm4.mjs +0 -918
- package/dist/route-registration.service-CDPQKpm4.mjs.map +0 -1
- package/dist/router-resolver-sUV_jTrU.mjs.map +0 -1
- package/dist/storage-C30X81CS.mjs.map +0 -1
- package/dist/storage-provider.interface-ClUwxz4S.d.mts +0 -201
- package/dist/storage-provider.interface-ClUwxz4S.d.mts.map +0 -1
- package/dist/storage.error-BStXPmO4.mjs +0 -8
- package/dist/storage.error-BStXPmO4.mjs.map +0 -1
- package/dist/stratal-BL6FKUM_.mjs.map +0 -1
- package/dist/stratal-D5j_I14G.d.mts.map +0 -1
- package/dist/tiered-cache.service-Dv3BhxxE.d.mts.map +0 -1
- package/dist/validation-CpOjviyT.mjs +0 -49
- package/dist/validation-CpOjviyT.mjs.map +0 -1
- package/dist/zod-eKqqhZ5_.mjs.map +0 -1
- package/dist/zod-wecrEVAs.d.mts.map +0 -1
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { l as Singleton, p as inject } from "../di-DQ3xbjgf.mjs";
|
|
2
|
+
import { n as __decorateParam, t as __decorate } from "../decorate-C79yt3Wt.mjs";
|
|
3
|
+
import { LOGGER_TOKENS } from "../logger/index.mjs";
|
|
4
|
+
import { n as Module } from "../module.decorator-CYbrOlX9.mjs";
|
|
5
|
+
import { n as markGatewayMode, t as isGatewayMode } from "../gateway-mode-0rUaeW3g.mjs";
|
|
6
|
+
import { a as unkeyedVary, n as setResponseHeaders, o as shouldLoopback } from "../response-headers-BqA57S_X.mjs";
|
|
7
|
+
import { n as InvalidCacheTagError, r as ResponseCacheConfigError, t as CachePurgeError } from "../response-cache.error-B4D0zy9W.mjs";
|
|
8
|
+
import { a as PurgesCache, c as getCacheable, d as resolveCachedEntrypoint, i as cachingUnavailableReason, l as RESPONSE_CACHE_TOKENS, n as assertNoGatewayOptions, o as getPurgesCache, r as assertValidGatewayEntrypoint, s as Cacheable, t as bindRouteCache, u as createLoopbackPurgeTarget } from "../route-cache-binding-CrLh8tB6.mjs";
|
|
9
|
+
//#region src/response-cache/tag-template.ts
|
|
10
|
+
/** Cloudflare's per-tag ceiling. Longer tags are dropped silently. */
|
|
11
|
+
const MAX_TAG_BYTES = 1024;
|
|
12
|
+
/** Hoisted: one encoder for the process, not one per tag per request. */
|
|
13
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
14
|
+
/**
|
|
15
|
+
* Printable ASCII (`0x21`–`0x7E`) — which already excludes space, every C0
|
|
16
|
+
* control (`\t`, `\r`, `\n`, `\v`, `\f`), `DEL`, and all non-ASCII, so header
|
|
17
|
+
* splitting via CR/LF is impossible — **minus** the two characters that carry
|
|
18
|
+
* structural meaning inside the header this value is written into:
|
|
19
|
+
*
|
|
20
|
+
* - `,` (`0x2C`) delimits the `Cache-Tag` list. `CacheabilityService.apply`
|
|
21
|
+
* joins tags with `','`, so a rendered tag containing a comma is not one tag
|
|
22
|
+
* — it becomes two, neither of which is the tag the author named, and every
|
|
23
|
+
* later purge for the intended tag misses forever. On the `@PurgesCache`
|
|
24
|
+
* path it is worse: rendered tags are handed straight to `ctx.cache.purge()`,
|
|
25
|
+
* so a comma arriving in request input (`?t=a,b`) purges tags the author
|
|
26
|
+
* never wrote — an arbitrary cache-flush primitive driven by the request.
|
|
27
|
+
* - `"` (`0x22`) opens a quoted-string in an RFC 9110 §5.6.1 comma-delimited
|
|
28
|
+
* list. An unbalanced quote makes a strict parser stop treating the
|
|
29
|
+
* following commas as delimiters, smuggling list structure the same way.
|
|
30
|
+
*
|
|
31
|
+
* `\` is deliberately still allowed: it only has meaning *inside* a
|
|
32
|
+
* quoted-string, and `"` can no longer appear to open one.
|
|
33
|
+
*/
|
|
34
|
+
const VALID_TAG = /^[\x21\x23-\x2B\x2D-\x7E]+$/;
|
|
35
|
+
const PLACEHOLDER = /\{([A-Za-z]+)\.([^}]+)\}/g;
|
|
36
|
+
const SCOPES = [
|
|
37
|
+
"param",
|
|
38
|
+
"query",
|
|
39
|
+
"body",
|
|
40
|
+
"data",
|
|
41
|
+
"partition"
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* Render `Cache-Tag` templates against the current request's scopes.
|
|
45
|
+
*
|
|
46
|
+
* A single `.*` suffix expands an array into one tag per element. Missing
|
|
47
|
+
* values throw rather than rendering a broken tag: Cloudflare drops invalid
|
|
48
|
+
* tags without any signal, so a silently-wrong tag means a purge that misses
|
|
49
|
+
* forever.
|
|
50
|
+
*/
|
|
51
|
+
function renderTags(templates, scopes) {
|
|
52
|
+
const out = [];
|
|
53
|
+
const seen = /* @__PURE__ */ new Set();
|
|
54
|
+
for (const template of templates) for (const tag of renderOne(template, scopes)) {
|
|
55
|
+
assertValidTag(tag);
|
|
56
|
+
if (!seen.has(tag)) {
|
|
57
|
+
seen.add(tag);
|
|
58
|
+
out.push(tag);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/** Throw unless `tag` is something Cloudflare will actually store. */
|
|
64
|
+
function assertValidTag(tag) {
|
|
65
|
+
if (tag.length === 0) throw new InvalidCacheTagError(tag, "tag is empty");
|
|
66
|
+
if (TEXT_ENCODER.encode(tag).length > MAX_TAG_BYTES) throw new InvalidCacheTagError(tag, `exceeds ${MAX_TAG_BYTES} bytes`);
|
|
67
|
+
if (!VALID_TAG.test(tag)) throw new InvalidCacheTagError(tag, "must be printable ASCII with no space, comma, or double quote — a comma would split this into two `Cache-Tag` entries, neither of which is the tag that was declared");
|
|
68
|
+
}
|
|
69
|
+
/** Render one template, expanding at most one `.*` fan-out. */
|
|
70
|
+
function renderOne(template, scopes) {
|
|
71
|
+
const matches = [...template.matchAll(PLACEHOLDER)];
|
|
72
|
+
const fanOuts = matches.filter((m) => m[2].endsWith(".*"));
|
|
73
|
+
if (fanOuts.length > 1) throw new InvalidCacheTagError(template, "only one `.*` fan-out is supported per tag");
|
|
74
|
+
if (fanOuts.length === 0) return [substitute(matches, scopes, void 0, template)];
|
|
75
|
+
const [fan] = fanOuts;
|
|
76
|
+
const path = fan[2].slice(0, -2);
|
|
77
|
+
const value = lookup(fan[1], path, scopes, template);
|
|
78
|
+
if (!Array.isArray(value)) throw new InvalidCacheTagError(template, `\`${fan[1]}.${path}\` is not an array`);
|
|
79
|
+
return value.map((item) => substitute(matches, scopes, {
|
|
80
|
+
placeholder: fan[0],
|
|
81
|
+
item
|
|
82
|
+
}, template));
|
|
83
|
+
}
|
|
84
|
+
/** Replace every placeholder with its resolved value, left to right. */
|
|
85
|
+
function substitute(matches, scopes, fan, template) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
let cursor = 0;
|
|
88
|
+
for (const match of matches) {
|
|
89
|
+
const [placeholder, scope, path] = match;
|
|
90
|
+
const index = match.index ?? 0;
|
|
91
|
+
parts.push(template.slice(cursor, index));
|
|
92
|
+
const value = fan && placeholder === fan.placeholder ? fan.item : lookup(scope, path, scopes, template);
|
|
93
|
+
parts.push(stringify(value, template));
|
|
94
|
+
cursor = index + placeholder.length;
|
|
95
|
+
}
|
|
96
|
+
parts.push(template.slice(cursor));
|
|
97
|
+
return parts.join("");
|
|
98
|
+
}
|
|
99
|
+
/** Walk `scope.path`, throwing on an unknown scope or a missing value. */
|
|
100
|
+
function lookup(scope, path, scopes, template) {
|
|
101
|
+
if (!SCOPES.includes(scope)) throw new InvalidCacheTagError(template, `unknown scope "${scope}" (expected one of ${SCOPES.join(", ")})`);
|
|
102
|
+
let current = scopes[scope];
|
|
103
|
+
for (const segment of path.split(".")) {
|
|
104
|
+
if (current === null || current === void 0 || typeof current !== "object") throw new InvalidCacheTagError(template, `\`${scope}.${path}\` is not present on this request`);
|
|
105
|
+
current = current[segment];
|
|
106
|
+
}
|
|
107
|
+
if (current === null || current === void 0) throw new InvalidCacheTagError(template, `\`${scope}.${path}\` is not present on this request`);
|
|
108
|
+
return current;
|
|
109
|
+
}
|
|
110
|
+
/** Coerce an interpolated value to its tag text. */
|
|
111
|
+
function stringify(value, template) {
|
|
112
|
+
if (typeof value === "string") return value;
|
|
113
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
114
|
+
throw new InvalidCacheTagError(template, `interpolated value has unsupported type "${typeof value}"`);
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/response-cache/services/cacheability.service.ts
|
|
118
|
+
/** What fail-closed emits. Cloudflare treats this as BYPASS. */
|
|
119
|
+
const NO_STORE = "private, no-store";
|
|
120
|
+
let CacheabilityService = class CacheabilityService {
|
|
121
|
+
logger;
|
|
122
|
+
options;
|
|
123
|
+
constructor(logger, options) {
|
|
124
|
+
this.logger = logger;
|
|
125
|
+
this.options = options;
|
|
126
|
+
}
|
|
127
|
+
apply(response, resolved, scopes, signals) {
|
|
128
|
+
const vary = this.mergeVary(response, [...resolved.vary, ...signals.inertia?.varyHeaders ?? []]);
|
|
129
|
+
const reason = this.rejectionReason(response, signals, vary);
|
|
130
|
+
if (reason) {
|
|
131
|
+
this.logger.warn("[stratal:response-cache] Response not cached", { reason });
|
|
132
|
+
return this.withHeaders(response, { "Cache-Control": NO_STORE });
|
|
133
|
+
}
|
|
134
|
+
const headers = {
|
|
135
|
+
"Cache-Control": this.browserCacheControl(resolved),
|
|
136
|
+
"CDN-Cache-Control": this.sharedCacheControl(resolved)
|
|
137
|
+
};
|
|
138
|
+
let tags;
|
|
139
|
+
try {
|
|
140
|
+
tags = renderTags(resolved.tags, scopes);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.logger.error("[stratal:response-cache] Invalid cache tag; not caching", { error });
|
|
143
|
+
return this.withHeaders(response, { "Cache-Control": NO_STORE });
|
|
144
|
+
}
|
|
145
|
+
if (tags.length > 0) headers["Cache-Tag"] = tags.join(",");
|
|
146
|
+
if (vary) headers.Vary = vary;
|
|
147
|
+
return this.withHeaders(response, headers);
|
|
148
|
+
}
|
|
149
|
+
/** The first condition that makes this response unsafe to cache, if any. */
|
|
150
|
+
rejectionReason(response, signals, vary) {
|
|
151
|
+
if (response.headers.has("Set-Cookie")) return "set-cookie";
|
|
152
|
+
if (response.status < 200 || response.status > 299) return "status";
|
|
153
|
+
if (!signals.partitionsResolved) return "partition-unresolved";
|
|
154
|
+
const inertia = signals.inertia;
|
|
155
|
+
if (inertia?.hasFlash) return "inertia-flash";
|
|
156
|
+
if (inertia?.hasOnceProps) return "inertia-once";
|
|
157
|
+
if (inertia?.isPartial && inertia.varyHeaders.length === 0) return "inertia-partial-unkeyed";
|
|
158
|
+
const gateway = this.options.gateway;
|
|
159
|
+
const unkeyed = gateway ? unkeyedVary(vary, gateway.keyBy) : [];
|
|
160
|
+
if (unkeyed.length > 0) {
|
|
161
|
+
this.logger.warn(`[stratal:response-cache] Response varies on ${unkeyed.join(", ")}, which is not in the cache key, so it is not cached. Add those names to ResponseCacheModule.forRoot({ gateway: { keyBy: [...] } }) — for an Inertia app, \`INERTIA_VARY_HEADERS\` from @stratal/inertia.`, { vary: unkeyed });
|
|
162
|
+
return "vary-unkeyed";
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* What a PRIVATE cache — a visitor's browser — may do with the response.
|
|
168
|
+
*
|
|
169
|
+
* A browser copy is the one this feature cannot reach: `Cache-Tag` and
|
|
170
|
+
* `ctx.cache.purge()` retire the shared copy, and nothing retires that one.
|
|
171
|
+
* So `browserTtl` is what a route says it is willing to have held without
|
|
172
|
+
* being askable, and it defaults to `ttl` — the same lifetime the shared
|
|
173
|
+
* cache was already given.
|
|
174
|
+
*
|
|
175
|
+
* `stale-while-revalidate` is emitted here as well as on the shared header,
|
|
176
|
+
* and it is what makes a short `browserTtl` cheap rather than punishing: the
|
|
177
|
+
* browser paints from the copy it has instead of waiting on the network, and
|
|
178
|
+
* fetches the replacement behind that. A route that must be retractable sets
|
|
179
|
+
* `browserTtl: 0` with a stale window, and converges on the next navigation
|
|
180
|
+
* rather than holding a withdrawn response for the whole `ttl`.
|
|
181
|
+
*
|
|
182
|
+
* `must-revalidate` only where there is no stale window to protect: it
|
|
183
|
+
* forbids serving stale at all, which is the opposite of what `swr` asks
|
|
184
|
+
* for, so emitting both would leave the pair contradicting each other.
|
|
185
|
+
*/
|
|
186
|
+
browserCacheControl(resolved) {
|
|
187
|
+
const parts = ["public", `max-age=${resolved.browserTtl}`];
|
|
188
|
+
if (resolved.swr !== void 0) parts.push(`stale-while-revalidate=${resolved.swr}`);
|
|
189
|
+
else if (resolved.browserTtl === 0) parts.push("must-revalidate");
|
|
190
|
+
return parts.join(", ");
|
|
191
|
+
}
|
|
192
|
+
/** The lifetime, for the cache that can be purged. */
|
|
193
|
+
sharedCacheControl(resolved) {
|
|
194
|
+
const parts = ["public", `max-age=${resolved.ttl}`];
|
|
195
|
+
if (resolved.swr !== void 0) parts.push(`stale-while-revalidate=${resolved.swr}`);
|
|
196
|
+
return parts.join(", ");
|
|
197
|
+
}
|
|
198
|
+
/** Union the declared Vary names with whatever the response already set. */
|
|
199
|
+
mergeVary(response, declared) {
|
|
200
|
+
const existing = (response.headers.get("Vary") ?? "").split(",").map((v) => v.trim()).filter(Boolean);
|
|
201
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...declared])];
|
|
202
|
+
return merged.length > 0 ? merged.join(", ") : null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Overlay headers onto the response, mutating in place when the header
|
|
206
|
+
* list is writable — see `setResponseHeaders` for why that's the common
|
|
207
|
+
* case and safe. `Cache-Tag` and `CDN-Cache-Control` are explicitly deleted
|
|
208
|
+
* (not just omitted) when degrading to `no-store`, so neither a stale tag
|
|
209
|
+
* nor a lifetime from a differently-decided response lingers. The second
|
|
210
|
+
* matters most: Cloudflare reads `CDN-Cache-Control` in preference to
|
|
211
|
+
* `Cache-Control`, so one left behind would cache a response this just
|
|
212
|
+
* refused to cache.
|
|
213
|
+
*/
|
|
214
|
+
withHeaders(response, headers) {
|
|
215
|
+
const toSet = { ...headers };
|
|
216
|
+
if (headers["Cache-Control"] === NO_STORE) {
|
|
217
|
+
toSet["Cache-Tag"] = null;
|
|
218
|
+
toSet["CDN-Cache-Control"] = null;
|
|
219
|
+
}
|
|
220
|
+
return setResponseHeaders(response, toSet);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
CacheabilityService = __decorate([
|
|
224
|
+
Singleton(RESPONSE_CACHE_TOKENS.CacheabilityService),
|
|
225
|
+
__decorateParam(0, inject(LOGGER_TOKENS.LoggerService)),
|
|
226
|
+
__decorateParam(1, inject(RESPONSE_CACHE_TOKENS.Options))
|
|
227
|
+
], CacheabilityService);
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/response-cache/services/gateway-primer.service.ts
|
|
230
|
+
let GatewayPrimerService = class GatewayPrimerService {
|
|
231
|
+
options;
|
|
232
|
+
logger;
|
|
233
|
+
constructor(options, logger) {
|
|
234
|
+
this.options = options;
|
|
235
|
+
this.logger = logger;
|
|
236
|
+
}
|
|
237
|
+
get hasPrimers() {
|
|
238
|
+
return (this.options.primers?.length ?? 0) > 0;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Run every primer in registration order.
|
|
242
|
+
*
|
|
243
|
+
* @returns `true` when the whole chain ran to completion, `false` when a
|
|
244
|
+
* primer short-circuited.
|
|
245
|
+
*
|
|
246
|
+
* `Middleware.handle` may return a `Response` to short-circuit its chain — an
|
|
247
|
+
* auth middleware rejecting a request, for example. In the gateway that
|
|
248
|
+
* `Response` is **not** the answer to send: primers run only to populate the
|
|
249
|
+
* request container before partition resolution, and the real middleware
|
|
250
|
+
* chain still runs inside the app, where the same middleware will produce the
|
|
251
|
+
* same rejection through the application's own pipeline (exception handler,
|
|
252
|
+
* logging, Inertia error shaping). Answering from the gateway would make it a
|
|
253
|
+
* second, partially-run response pipeline whose output is assembled by a
|
|
254
|
+
* *subset* of the app's middleware.
|
|
255
|
+
*
|
|
256
|
+
* So the `Response` is dropped — but not silently. A short-circuit means the
|
|
257
|
+
* container was never fully primed, so partition resolution cannot be
|
|
258
|
+
* trusted; the caller must fail closed and run the request inline instead of
|
|
259
|
+
* caching it.
|
|
260
|
+
*/
|
|
261
|
+
async prime(ctx) {
|
|
262
|
+
const primers = this.options.primers ?? [];
|
|
263
|
+
const container = ctx.getContainer();
|
|
264
|
+
for (const Primer of primers) {
|
|
265
|
+
const result = await container.resolve(Primer).handle(ctx, async () => {});
|
|
266
|
+
if (result instanceof Response) {
|
|
267
|
+
this.logger.debug("[stratal:response-cache] Primer short-circuited; not caching", {
|
|
268
|
+
primer: Primer.name,
|
|
269
|
+
status: result.status
|
|
270
|
+
});
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
GatewayPrimerService = __decorate([
|
|
278
|
+
Singleton(RESPONSE_CACHE_TOKENS.GatewayPrimerService),
|
|
279
|
+
__decorateParam(0, inject(RESPONSE_CACHE_TOKENS.Options)),
|
|
280
|
+
__decorateParam(1, inject(LOGGER_TOKENS.LoggerService))
|
|
281
|
+
], GatewayPrimerService);
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/response-cache/services/gateway-route-table.ts
|
|
284
|
+
let GatewayRouteTable = class GatewayRouteTable {
|
|
285
|
+
entries = /* @__PURE__ */ new Map();
|
|
286
|
+
/**
|
|
287
|
+
* The consumer's `export const <name> = cachedEntrypoint(stratal)`, from
|
|
288
|
+
* `ResponseCacheModule.forRoot({ gateway: { entrypoint } })`.
|
|
289
|
+
*
|
|
290
|
+
* `undefined` means no gateway is configured, which is also the condition
|
|
291
|
+
* under which `bindRouteCache` refuses a non-empty `partitionBy` — so an
|
|
292
|
+
* unconfigured table is always an empty one.
|
|
293
|
+
*/
|
|
294
|
+
entrypoint;
|
|
295
|
+
/**
|
|
296
|
+
* `gateway.keyBy` — the request headers whose values join the cache key.
|
|
297
|
+
*
|
|
298
|
+
* Carried here rather than resolved per request because the dispatch
|
|
299
|
+
* middleware already memoises this table, so reading it costs a property
|
|
300
|
+
* access on the hot path instead of a container lookup.
|
|
301
|
+
*/
|
|
302
|
+
keyBy = [];
|
|
303
|
+
/** Set by `RouteRegistrationService.configure()`, before any request. */
|
|
304
|
+
configure(entrypoint, keyBy = []) {
|
|
305
|
+
this.entrypoint = entrypoint;
|
|
306
|
+
this.keyBy = keyBy;
|
|
307
|
+
}
|
|
308
|
+
/** True when no route in this app declares a partition. */
|
|
309
|
+
get isEmpty() {
|
|
310
|
+
return this.entries.size === 0;
|
|
311
|
+
}
|
|
312
|
+
/** Number of recorded routes. Test/diagnostic affordance. */
|
|
313
|
+
get size() {
|
|
314
|
+
return this.entries.size;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Record one registered route.
|
|
318
|
+
*
|
|
319
|
+
* `method` is the Hono-registered method; `path` is the pattern the route
|
|
320
|
+
* was registered under (already expanded for versioning and locale
|
|
321
|
+
* variants, so each variant gets its own entry).
|
|
322
|
+
*/
|
|
323
|
+
record(method, path, partitionBy) {
|
|
324
|
+
if (partitionBy.length === 0) return;
|
|
325
|
+
this.entries.set(`${method.toUpperCase()} ${path}`, { partitionBy });
|
|
326
|
+
}
|
|
327
|
+
/** The entry for a matched route pattern, or `undefined` to run inline. */
|
|
328
|
+
lookup(method, path) {
|
|
329
|
+
return this.entries.get(`${method.toUpperCase()} ${path}`);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
GatewayRouteTable = __decorate([Singleton(RESPONSE_CACHE_TOKENS.GatewayRouteTable)], GatewayRouteTable);
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/response-cache/services/partition-resolver.service.ts
|
|
335
|
+
let PartitionResolverService = class PartitionResolverService {
|
|
336
|
+
options;
|
|
337
|
+
logger;
|
|
338
|
+
constructor(options, logger) {
|
|
339
|
+
this.options = options;
|
|
340
|
+
this.logger = logger;
|
|
341
|
+
}
|
|
342
|
+
/** Throw at boot if a route names a partition with no registered resolver. */
|
|
343
|
+
assertKnown(names, where) {
|
|
344
|
+
const known = Object.keys(this.options.partitions ?? {});
|
|
345
|
+
for (const name of names) if (!known.includes(name)) throw new ResponseCacheConfigError(`${where}: unknown partition "${name}". Register it via ResponseCacheModule.forRoot({ partitions: { ${name}: (ctx) => ... } }). Known partitions: ${known.length > 0 ? known.join(", ") : "(none)"}.`);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Partition names seen so far, split by whether they have ever resolved.
|
|
349
|
+
*
|
|
350
|
+
* A resolver that fails is ordinary traffic — an anonymous visitor to a
|
|
351
|
+
* per-user route — so the per-request signal stays at `debug`. But a
|
|
352
|
+
* partition that has *never* resolved is a configuration mistake, not
|
|
353
|
+
* traffic: the usual cause is `partitions: { user: (ctx) => ctx.user().id }`
|
|
354
|
+
* configured without `primers: AUTH_GATEWAY_PRIMERS`, so `ctx.user()` throws
|
|
355
|
+
* on every request and the route is silently never cached. Warned once per
|
|
356
|
+
* name, so that diagnosis is available without making normal misses noisy.
|
|
357
|
+
*/
|
|
358
|
+
warnedUnresolved = /* @__PURE__ */ new Set();
|
|
359
|
+
everResolved = /* @__PURE__ */ new Set();
|
|
360
|
+
noteUnresolved(name) {
|
|
361
|
+
if (this.everResolved.has(name) || this.warnedUnresolved.has(name)) return;
|
|
362
|
+
this.warnedUnresolved.add(name);
|
|
363
|
+
this.logger.warn(`[stratal:response-cache] Partition "${name}" has never resolved; routes using it are not being cached. If its resolver calls \`ctx.user()\` or reads the request container, the gateway needs \`primers\` (e.g. \`AUTH_GATEWAY_PRIMERS\` from @stratal/framework) to populate that context before resolution runs.`, { partition: name });
|
|
364
|
+
}
|
|
365
|
+
async resolve(ctx, names) {
|
|
366
|
+
if (names.length === 0) return {
|
|
367
|
+
props: {},
|
|
368
|
+
resolved: true
|
|
369
|
+
};
|
|
370
|
+
const partitions = this.options.partitions ?? {};
|
|
371
|
+
const props = {};
|
|
372
|
+
for (const name of names) {
|
|
373
|
+
const resolver = partitions[name];
|
|
374
|
+
if (!resolver) return {
|
|
375
|
+
props: {},
|
|
376
|
+
resolved: false
|
|
377
|
+
};
|
|
378
|
+
let value;
|
|
379
|
+
try {
|
|
380
|
+
value = await resolver(ctx);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
this.logger.debug("[stratal:response-cache] Partition resolver threw; not caching", {
|
|
383
|
+
partition: name,
|
|
384
|
+
error
|
|
385
|
+
});
|
|
386
|
+
this.noteUnresolved(name);
|
|
387
|
+
return {
|
|
388
|
+
props: {},
|
|
389
|
+
resolved: false
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (value === null || value === void 0) {
|
|
393
|
+
this.logger.debug("[stratal:response-cache] Partition unresolved; not caching", { partition: name });
|
|
394
|
+
this.noteUnresolved(name);
|
|
395
|
+
return {
|
|
396
|
+
props: {},
|
|
397
|
+
resolved: false
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
this.everResolved.add(name);
|
|
401
|
+
props[name] = value;
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
props,
|
|
405
|
+
resolved: true
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
PartitionResolverService = __decorate([
|
|
410
|
+
Singleton(RESPONSE_CACHE_TOKENS.PartitionResolverService),
|
|
411
|
+
__decorateParam(0, inject(RESPONSE_CACHE_TOKENS.Options)),
|
|
412
|
+
__decorateParam(1, inject(LOGGER_TOKENS.LoggerService))
|
|
413
|
+
], PartitionResolverService);
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/response-cache/services/response-cache.service.ts
|
|
416
|
+
let ResponseCacheService = class ResponseCacheService {
|
|
417
|
+
logger;
|
|
418
|
+
constructor(logger) {
|
|
419
|
+
this.logger = logger;
|
|
420
|
+
}
|
|
421
|
+
/** Render a `@PurgesCache` declaration against this request's scopes. */
|
|
422
|
+
buildPurgeSpec(options, scopes) {
|
|
423
|
+
if (options.purgeEverything) return { purgeEverything: true };
|
|
424
|
+
const spec = {};
|
|
425
|
+
if (options.tags?.length) spec.tags = renderTags(options.tags, scopes);
|
|
426
|
+
if (options.pathPrefixes?.length) spec.pathPrefixes = [...options.pathPrefixes];
|
|
427
|
+
return spec;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Issue a purge and wait for it.
|
|
431
|
+
*
|
|
432
|
+
* Deliberately not deferred through `waitUntil`: a client that re-reads
|
|
433
|
+
* immediately after its own write must not be served the pre-write response.
|
|
434
|
+
*
|
|
435
|
+
* @throws {CachePurgeError} The mutation has already committed at this point,
|
|
436
|
+
* so failing loudly is the lesser evil against a cache that is silently
|
|
437
|
+
* inconsistent with the database.
|
|
438
|
+
*/
|
|
439
|
+
async purge(spec, cache) {
|
|
440
|
+
try {
|
|
441
|
+
const result = await cache.purge(spec);
|
|
442
|
+
if (result && !result.success) {
|
|
443
|
+
this.logger.error("[stratal:response-cache] Purge reported failure", { spec });
|
|
444
|
+
throw new CachePurgeError(`the runtime reported failure for ${JSON.stringify(spec)}`);
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (error instanceof CachePurgeError) throw error;
|
|
448
|
+
this.logger.error("[stratal:response-cache] Purge threw", {
|
|
449
|
+
spec,
|
|
450
|
+
error: error instanceof Error ? error.message : String(error)
|
|
451
|
+
});
|
|
452
|
+
throw new CachePurgeError(error instanceof Error ? error.message : String(error));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
ResponseCacheService = __decorate([Singleton(RESPONSE_CACHE_TOKENS.ResponseCacheService), __decorateParam(0, inject(LOGGER_TOKENS.LoggerService))], ResponseCacheService);
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/response-cache/response-cache.module.ts
|
|
459
|
+
var _ResponseCacheModule;
|
|
460
|
+
let ResponseCacheModule = _ResponseCacheModule = class ResponseCacheModule {
|
|
461
|
+
static forRoot(options = {}) {
|
|
462
|
+
assertNoGatewayOptions(options);
|
|
463
|
+
return {
|
|
464
|
+
module: _ResponseCacheModule,
|
|
465
|
+
providers: [{
|
|
466
|
+
provide: RESPONSE_CACHE_TOKENS.Options,
|
|
467
|
+
useValue: options
|
|
468
|
+
}]
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
static forRootAsync(options) {
|
|
472
|
+
return {
|
|
473
|
+
module: _ResponseCacheModule,
|
|
474
|
+
providers: [{
|
|
475
|
+
provide: RESPONSE_CACHE_TOKENS.Options,
|
|
476
|
+
useFactory: (...deps) => {
|
|
477
|
+
const resolved = options.useFactory(...deps);
|
|
478
|
+
if (!(resolved instanceof Promise)) assertNoGatewayOptions(resolved);
|
|
479
|
+
return resolved;
|
|
480
|
+
},
|
|
481
|
+
inject: options.inject ?? []
|
|
482
|
+
}]
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
ResponseCacheModule = _ResponseCacheModule = __decorate([Module({ providers: [
|
|
487
|
+
{
|
|
488
|
+
provide: RESPONSE_CACHE_TOKENS.CacheabilityService,
|
|
489
|
+
useClass: CacheabilityService
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
provide: RESPONSE_CACHE_TOKENS.PartitionResolverService,
|
|
493
|
+
useClass: PartitionResolverService
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
provide: RESPONSE_CACHE_TOKENS.GatewayPrimerService,
|
|
497
|
+
useClass: GatewayPrimerService
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
provide: RESPONSE_CACHE_TOKENS.ResponseCacheService,
|
|
501
|
+
useClass: ResponseCacheService
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
provide: RESPONSE_CACHE_TOKENS.GatewayRouteTable,
|
|
505
|
+
useClass: GatewayRouteTable
|
|
506
|
+
}
|
|
507
|
+
] })], ResponseCacheModule);
|
|
508
|
+
//#endregion
|
|
509
|
+
export { CachePurgeError, Cacheable, GatewayRouteTable, InvalidCacheTagError, PurgesCache, RESPONSE_CACHE_TOKENS, ResponseCacheConfigError, ResponseCacheModule, assertValidGatewayEntrypoint, bindRouteCache, cachingUnavailableReason, createLoopbackPurgeTarget, getCacheable, getPurgesCache, isGatewayMode, markGatewayMode, resolveCachedEntrypoint, shouldLoopback };
|
|
510
|
+
|
|
511
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/response-cache/tag-template.ts","../../src/response-cache/services/cacheability.service.ts","../../src/response-cache/services/gateway-primer.service.ts","../../src/response-cache/services/gateway-route-table.ts","../../src/response-cache/services/partition-resolver.service.ts","../../src/response-cache/services/response-cache.service.ts","../../src/response-cache/response-cache.module.ts"],"sourcesContent":["import { InvalidCacheTagError } from './errors'\n\n/**\n * Values available to `{scope.path}` placeholders when rendering tags.\n *\n * `body` is never populated by the HTTP router — `RouteRegistrationService\n * .buildTagScopes` always sets it `undefined`, and route registration now\n * rejects any `{body.*}` tag at boot (`bindRouteCache`) precisely because\n * nothing can ever fill it in on the request path. It stays part of this\n * type — rather than being dropped — because `renderTags` is a general\n * template compiler exercised directly in its own unit tests with hand-built\n * scopes (including `body`); those tests are about the compiler accepting\n * all four scopes correctly, independent of what any one caller can supply.\n */\nexport interface TagScopes {\n param: Record<string, string>\n query: Record<string, string>\n body: unknown\n data: unknown\n /**\n * The partition values this request resolved, keyed by partition name.\n *\n * The only scope guaranteed present on every variant of a response. `data`\n * is the resolved payload, and an Inertia partial reload carries only the\n * props it asked for — so `{data.auth.user.id}` renders on the document and\n * throws on the partial, which fails that variant closed and leaves exactly\n * the deferred work uncached. A partition is in the cache key by\n * construction, so `{partition.user}` renders identically on both.\n */\n partition: Record<string, string>\n}\n\n/** Cloudflare's per-tag ceiling. Longer tags are dropped silently. */\nconst MAX_TAG_BYTES = 1024\n/** Hoisted: one encoder for the process, not one per tag per request. */\nconst TEXT_ENCODER = new TextEncoder()\n/**\n * Printable ASCII (`0x21`–`0x7E`) — which already excludes space, every C0\n * control (`\\t`, `\\r`, `\\n`, `\\v`, `\\f`), `DEL`, and all non-ASCII, so header\n * splitting via CR/LF is impossible — **minus** the two characters that carry\n * structural meaning inside the header this value is written into:\n *\n * - `,` (`0x2C`) delimits the `Cache-Tag` list. `CacheabilityService.apply`\n * joins tags with `','`, so a rendered tag containing a comma is not one tag\n * — it becomes two, neither of which is the tag the author named, and every\n * later purge for the intended tag misses forever. On the `@PurgesCache`\n * path it is worse: rendered tags are handed straight to `ctx.cache.purge()`,\n * so a comma arriving in request input (`?t=a,b`) purges tags the author\n * never wrote — an arbitrary cache-flush primitive driven by the request.\n * - `\"` (`0x22`) opens a quoted-string in an RFC 9110 §5.6.1 comma-delimited\n * list. An unbalanced quote makes a strict parser stop treating the\n * following commas as delimiters, smuggling list structure the same way.\n *\n * `\\` is deliberately still allowed: it only has meaning *inside* a\n * quoted-string, and `\"` can no longer appear to open one.\n */\nconst VALID_TAG = /^[\\x21\\x23-\\x2B\\x2D-\\x7E]+$/\nconst PLACEHOLDER = /\\{([A-Za-z]+)\\.([^}]+)\\}/g\nconst SCOPES = ['param', 'query', 'body', 'data', 'partition'] as const\n\n/**\n * Render `Cache-Tag` templates against the current request's scopes.\n *\n * A single `.*` suffix expands an array into one tag per element. Missing\n * values throw rather than rendering a broken tag: Cloudflare drops invalid\n * tags without any signal, so a silently-wrong tag means a purge that misses\n * forever.\n */\nexport function renderTags(templates: string[], scopes: TagScopes): string[] {\n const out: string[] = []\n // A `Set` keeps membership checks O(1) — an `Array.includes` scan here would\n // make a fan-out template (`{body.ids.*}` over hundreds of ids) quadratic.\n // The array still carries insertion order for the returned result.\n const seen = new Set<string>()\n\n for (const template of templates) {\n for (const tag of renderOne(template, scopes)) {\n assertValidTag(tag)\n if (!seen.has(tag)) {\n seen.add(tag)\n out.push(tag)\n }\n }\n }\n\n return out\n}\n\n/** Throw unless `tag` is something Cloudflare will actually store. */\nexport function assertValidTag(tag: string): void {\n if (tag.length === 0) {\n throw new InvalidCacheTagError(tag, 'tag is empty')\n }\n if (TEXT_ENCODER.encode(tag).length > MAX_TAG_BYTES) {\n throw new InvalidCacheTagError(tag, `exceeds ${MAX_TAG_BYTES} bytes`)\n }\n if (!VALID_TAG.test(tag)) {\n throw new InvalidCacheTagError(\n tag,\n 'must be printable ASCII with no space, comma, or double quote — a comma would split ' +\n 'this into two `Cache-Tag` entries, neither of which is the tag that was declared',\n )\n }\n}\n\n/** Render one template, expanding at most one `.*` fan-out. */\nfunction renderOne(template: string, scopes: TagScopes): string[] {\n const matches = [...template.matchAll(PLACEHOLDER)]\n const fanOuts = matches.filter((m) => m[2].endsWith('.*'))\n\n if (fanOuts.length > 1) {\n throw new InvalidCacheTagError(template, 'only one `.*` fan-out is supported per tag')\n }\n\n if (fanOuts.length === 0) {\n return [substitute(matches, scopes, undefined, template)]\n }\n\n const [fan] = fanOuts\n const path = fan[2].slice(0, -2)\n const value = lookup(fan[1], path, scopes, template)\n\n if (!Array.isArray(value)) {\n throw new InvalidCacheTagError(template, `\\`${fan[1]}.${path}\\` is not an array`)\n }\n\n return value.map((item) =>\n substitute(matches, scopes, { placeholder: fan[0], item }, template),\n )\n}\n\n/** Replace every placeholder with its resolved value, left to right. */\nfunction substitute(\n matches: RegExpMatchArray[],\n scopes: TagScopes,\n fan: { placeholder: string; item: unknown } | undefined,\n template: string,\n): string {\n const parts: string[] = []\n let cursor = 0\n\n for (const match of matches) {\n const [placeholder, scope, path] = match\n const index = match.index ?? 0\n\n parts.push(template.slice(cursor, index))\n\n const value =\n fan && placeholder === fan.placeholder ? fan.item : lookup(scope, path, scopes, template)\n parts.push(stringify(value, template))\n\n cursor = index + placeholder.length\n }\n\n parts.push(template.slice(cursor))\n\n return parts.join('')\n}\n\n/** Walk `scope.path`, throwing on an unknown scope or a missing value. */\nfunction lookup(scope: string, path: string, scopes: TagScopes, template: string): unknown {\n if (!(SCOPES as readonly string[]).includes(scope)) {\n throw new InvalidCacheTagError(\n template,\n `unknown scope \"${scope}\" (expected one of ${SCOPES.join(', ')})`,\n )\n }\n\n let current: unknown = scopes[scope as keyof TagScopes]\n\n for (const segment of path.split('.')) {\n if (current === null || current === undefined || typeof current !== 'object') {\n throw new InvalidCacheTagError(template, `\\`${scope}.${path}\\` is not present on this request`)\n }\n current = (current as Record<string, unknown>)[segment]\n }\n\n if (current === null || current === undefined) {\n throw new InvalidCacheTagError(template, `\\`${scope}.${path}\\` is not present on this request`)\n }\n\n return current\n}\n\n/** Coerce an interpolated value to its tag text. */\nfunction stringify(value: unknown, template: string): string {\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n throw new InvalidCacheTagError(template, `interpolated value has unsupported type \"${typeof value}\"`)\n}\n","import { inject } from '../../di'\nimport { Singleton } from '../../di/decorators'\nimport { LOGGER_TOKENS, type LoggerService } from '../../logger'\nimport { unkeyedVary } from '../representation'\nimport { RESPONSE_CACHE_TOKENS } from '../response-cache.tokens'\nimport { setResponseHeaders } from '../response-headers'\nimport { renderTags, type TagScopes } from '../tag-template'\nimport type { ResolvedCacheable, ResponseCacheModuleOptions } from '../types'\n\n/** Inertia-specific conditions that make a page unsafe to cache. */\nexport interface InertiaCacheSignals {\n hasFlash: boolean\n isPartial: boolean\n hasOnceProps: boolean\n /**\n * Request headers the adapter reports this response is a function of.\n * Unioned into `Vary`, so a partial reload and the full page at the same URL\n * are stored as separate variants rather than colliding.\n *\n * Required rather than optional: an empty list is what makes a partial reload\n * fail closed, and an adapter that simply forgot the field would otherwise be\n * indistinguishable from one declaring the response unkeyed.\n */\n varyHeaders: readonly string[]\n}\n\nexport interface CacheabilitySignals {\n /** False when any declared partition resolver returned null/undefined. */\n partitionsResolved: boolean\n inertia?: InertiaCacheSignals\n}\n\n/** What fail-closed emits. Cloudflare treats this as BYPASS. */\nconst NO_STORE = 'private, no-store'\n\n\n/**\n * Decides whether a `@Cacheable` response may actually be cached, and stamps\n * the headers that say so.\n *\n * Everything that is not provably safe becomes `private, no-store`. That\n * matters more than it looks: Workers Caching applies RFC 9111 heuristic\n * freshness, so a response with no `Cache-Control` at all is still cached —\n * `200` for two hours. Silence is not a safe default here.\n */\n@Singleton(RESPONSE_CACHE_TOKENS.CacheabilityService)\nexport class CacheabilityService {\n constructor(\n @inject(LOGGER_TOKENS.LoggerService) private readonly logger: LoggerService,\n @inject(RESPONSE_CACHE_TOKENS.Options) private readonly options: ResponseCacheModuleOptions,\n ) {}\n\n apply(response: Response, resolved: ResolvedCacheable, scopes: TagScopes, signals: CacheabilitySignals): Response {\n // Computed before the decision, because what a response varies on is one of\n // the things that decides it.\n const vary = this.mergeVary(response, [...resolved.vary, ...(signals.inertia?.varyHeaders ?? [])])\n\n const reason = this.rejectionReason(response, signals, vary)\n\n if (reason) {\n this.logger.warn('[stratal:response-cache] Response not cached', { reason })\n return this.withHeaders(response, { 'Cache-Control': NO_STORE })\n }\n\n // Two audiences, two headers. `CDN-Cache-Control` is the one Cloudflare\n // reads in preference to `Cache-Control`, and the one browsers ignore\n // entirely, so the lifetime lands where it can still be purged and nowhere\n // else. Keeping them separate is also what lets `swr` survive: expressing\n // the same split through `s-maxage` would end the stale window, since\n // Cloudflare stops honouring `stale-while-revalidate` once it is present.\n const headers: Record<string, string> = {\n 'Cache-Control': this.browserCacheControl(resolved),\n 'CDN-Cache-Control': this.sharedCacheControl(resolved),\n }\n\n // Malformed tag templates (e.g., missing scope value) throw InvalidCacheTagError.\n // Rather than crashing the request, fail closed — a bad tag is an author bug\n // distinct from ordinary rejections (which are runtime data), and the risk\n // the throw guarded against was a tag silently vanishing while the response\n // cached anyway. Here, the response doesn't cache at all.\n let tags: string[]\n try {\n tags = renderTags(resolved.tags, scopes)\n } catch (error) {\n this.logger.error('[stratal:response-cache] Invalid cache tag; not caching', { error })\n return this.withHeaders(response, { 'Cache-Control': NO_STORE })\n }\n\n if (tags.length > 0) headers['Cache-Tag'] = tags.join(',')\n\n if (vary) headers.Vary = vary\n\n return this.withHeaders(response, headers)\n }\n\n /** The first condition that makes this response unsafe to cache, if any. */\n private rejectionReason(response: Response, signals: CacheabilitySignals, vary: string | null): string | null {\n if (response.headers.has('Set-Cookie')) return 'set-cookie'\n if (response.status < 200 || response.status > 299) return 'status'\n if (!signals.partitionsResolved) return 'partition-unresolved'\n\n const inertia = signals.inertia\n if (inertia?.hasFlash) return 'inertia-flash'\n if (inertia?.hasOnceProps) return 'inertia-once'\n\n // A partial reload IS cacheable — it is a deterministic function of the URL,\n // the partition, and the headers naming which props were asked for. What is\n // not cacheable is one whose adapter did not report those headers: keying it\n // on the URL alone would make it interchangeable with the full page and with\n // every other prop set at that URL.\n if (inertia?.isPartial && inertia.varyHeaders.length === 0) return 'inertia-partial-unkeyed'\n\n // Anything this response varies on that is not also in the cache key is a\n // way for two different bodies to share one entry. `Vary` is supposed to\n // prevent that on its own; measured against Workers Caching on a URL with a\n // query string, it does not — a variant stored for one header value is\n // handed to a request sending another. So the key has to carry it, and a\n // response naming a header the key does not is refused rather than stored\n // wrong. `gateway.keyBy` is what puts a header into the key.\n //\n // Only under a gateway, and that boundary is evidence, not caution: the\n // failure was measured on the loopback topology, and the loopback is also\n // the only place a representation CAN reach the key, since `ctx.props` is\n // set by the forwarding call. Refusing without a gateway would withdraw\n // caching from apps on a path whose behaviour has not been measured, and\n // offer them nothing to fix it with.\n const gateway = this.options.gateway\n const unkeyed = gateway ? unkeyedVary(vary, gateway.keyBy) : []\n if (unkeyed.length > 0) {\n this.logger.warn(\n `[stratal:response-cache] Response varies on ${unkeyed.join(', ')}, which is not in the ` +\n 'cache key, so it is not cached. Add those names to ' +\n 'ResponseCacheModule.forRoot({ gateway: { keyBy: [...] } }) — for an Inertia app, ' +\n '`INERTIA_VARY_HEADERS` from @stratal/inertia.',\n { vary: unkeyed },\n )\n return 'vary-unkeyed'\n }\n\n return null\n }\n\n /**\n * What a PRIVATE cache — a visitor's browser — may do with the response.\n *\n * A browser copy is the one this feature cannot reach: `Cache-Tag` and\n * `ctx.cache.purge()` retire the shared copy, and nothing retires that one.\n * So `browserTtl` is what a route says it is willing to have held without\n * being askable, and it defaults to `ttl` — the same lifetime the shared\n * cache was already given.\n *\n * `stale-while-revalidate` is emitted here as well as on the shared header,\n * and it is what makes a short `browserTtl` cheap rather than punishing: the\n * browser paints from the copy it has instead of waiting on the network, and\n * fetches the replacement behind that. A route that must be retractable sets\n * `browserTtl: 0` with a stale window, and converges on the next navigation\n * rather than holding a withdrawn response for the whole `ttl`.\n *\n * `must-revalidate` only where there is no stale window to protect: it\n * forbids serving stale at all, which is the opposite of what `swr` asks\n * for, so emitting both would leave the pair contradicting each other.\n */\n private browserCacheControl(resolved: ResolvedCacheable): string {\n const parts = ['public', `max-age=${resolved.browserTtl}`]\n\n if (resolved.swr !== undefined) parts.push(`stale-while-revalidate=${resolved.swr}`)\n else if (resolved.browserTtl === 0) parts.push('must-revalidate')\n\n return parts.join(', ')\n }\n\n /** The lifetime, for the cache that can be purged. */\n private sharedCacheControl(resolved: ResolvedCacheable): string {\n const parts = ['public', `max-age=${resolved.ttl}`]\n if (resolved.swr !== undefined) parts.push(`stale-while-revalidate=${resolved.swr}`)\n return parts.join(', ')\n }\n\n /** Union the declared Vary names with whatever the response already set. */\n private mergeVary(response: Response, declared: string[]): string | null {\n const existing = (response.headers.get('Vary') ?? '')\n .split(',')\n .map((v) => v.trim())\n .filter(Boolean)\n\n const merged = [...new Set([...existing, ...declared])]\n return merged.length > 0 ? merged.join(', ') : null\n }\n\n /**\n * Overlay headers onto the response, mutating in place when the header\n * list is writable — see `setResponseHeaders` for why that's the common\n * case and safe. `Cache-Tag` and `CDN-Cache-Control` are explicitly deleted\n * (not just omitted) when degrading to `no-store`, so neither a stale tag\n * nor a lifetime from a differently-decided response lingers. The second\n * matters most: Cloudflare reads `CDN-Cache-Control` in preference to\n * `Cache-Control`, so one left behind would cache a response this just\n * refused to cache.\n */\n private withHeaders(response: Response, headers: Record<string, string>): Response {\n const toSet: Record<string, string | null> = { ...headers }\n if (headers['Cache-Control'] === NO_STORE) {\n toSet['Cache-Tag'] = null\n toSet['CDN-Cache-Control'] = null\n }\n\n return setResponseHeaders(response, toSet)\n }\n}\n","import type { Constructor } from '../../types'\nimport { inject } from '../../di'\nimport { Singleton } from '../../di/decorators'\nimport { LOGGER_TOKENS, type LoggerService } from '../../logger'\nimport type { Middleware } from '../../router/middleware.interface'\nimport type { RouterContext } from '../../router/router-context'\nimport { RESPONSE_CACHE_TOKENS } from '../response-cache.tokens'\nimport type { ResponseCacheModuleOptions } from '../types'\n\n/**\n * Runs registered middleware in the gateway before partition resolution, so a\n * resolver sees the same `RouterContext` it would inside the app.\n *\n * `ctx.user()` is a macro over the request-scoped `AuthContext`, which\n * `SessionVerificationMiddleware` populates imperatively inside the app's\n * middleware chain. Without priming, that chain has not run and `ctx.user()`\n * throws. `AuthModule` does **not** register anything here: `@stratal/\n * framework` exports `AUTH_GATEWAY_PRIMERS`, which a consumer passes\n * explicitly via `ResponseCacheModule.forRoot({ primers })`.\n *\n * Called by `createGatewayDispatchMiddleware`, and only for `@Cacheable`\n * routes that declare `partitionBy` — a public cacheable route and every\n * non-cacheable route skip the chain entirely.\n */\n@Singleton(RESPONSE_CACHE_TOKENS.GatewayPrimerService)\nexport class GatewayPrimerService {\n constructor(\n @inject(RESPONSE_CACHE_TOKENS.Options) private readonly options: ResponseCacheModuleOptions,\n @inject(LOGGER_TOKENS.LoggerService) private readonly logger: LoggerService,\n ) {}\n\n get hasPrimers(): boolean {\n return (this.options.primers?.length ?? 0) > 0\n }\n\n /**\n * Run every primer in registration order.\n *\n * @returns `true` when the whole chain ran to completion, `false` when a\n * primer short-circuited.\n *\n * `Middleware.handle` may return a `Response` to short-circuit its chain — an\n * auth middleware rejecting a request, for example. In the gateway that\n * `Response` is **not** the answer to send: primers run only to populate the\n * request container before partition resolution, and the real middleware\n * chain still runs inside the app, where the same middleware will produce the\n * same rejection through the application's own pipeline (exception handler,\n * logging, Inertia error shaping). Answering from the gateway would make it a\n * second, partially-run response pipeline whose output is assembled by a\n * *subset* of the app's middleware.\n *\n * So the `Response` is dropped — but not silently. A short-circuit means the\n * container was never fully primed, so partition resolution cannot be\n * trusted; the caller must fail closed and run the request inline instead of\n * caching it.\n */\n async prime(ctx: RouterContext): Promise<boolean> {\n const primers = this.options.primers ?? []\n const container = ctx.getContainer()\n\n for (const Primer of primers) {\n const middleware = container.resolve<Middleware>(Primer as Constructor<Middleware>)\n // Primers exist for their side effects on the request container, so the\n // chain terminates here rather than continuing to a handler.\n // eslint-disable-next-line @typescript-eslint/no-empty-function -- terminating next for side-effect middleware\n const result = await middleware.handle(ctx, async () => {})\n\n if (result instanceof Response) {\n this.logger.debug('[stratal:response-cache] Primer short-circuited; not caching', {\n primer: (Primer as Constructor<Middleware>).name,\n status: result.status,\n })\n return false\n }\n }\n\n return true\n }\n}\n","import { Singleton } from '../../di/decorators'\nimport { RESPONSE_CACHE_TOKENS } from '../response-cache.tokens'\nimport type { CacheableOptions } from '../types'\n\n/**\n * What the dispatch middleware needs to know about one route.\n *\n * Typed as a `CacheableOptions` (every field optional) rather than a bespoke\n * shape so it can be handed straight to `shouldLoopback`, which already\n * encodes \"only GET/HEAD ever loop back\".\n */\nexport interface GatewayRouteEntry extends CacheableOptions {\n partitionBy: string[]\n}\n\n/**\n * `${METHOD} ${routePath}` → the partitions that route declared.\n *\n * Populated once, at route registration, and only for routes whose\n * **effective** `partitionBy` (after module defaults) is non-empty. Every\n * other route is absent, and absence means \"run inline\" — the safe default,\n * and the one that keeps an app which uses no partitions paying nothing but a\n * single `isEmpty` check per request.\n *\n * The key is the route *pattern* Hono matched (`/posts/:id`), not the request\n * path, so one entry serves every request to that route.\n */\n@Singleton(RESPONSE_CACHE_TOKENS.GatewayRouteTable)\nexport class GatewayRouteTable {\n private readonly entries = new Map<string, GatewayRouteEntry>()\n\n /**\n * The consumer's `export const <name> = cachedEntrypoint(stratal)`, from\n * `ResponseCacheModule.forRoot({ gateway: { entrypoint } })`.\n *\n * `undefined` means no gateway is configured, which is also the condition\n * under which `bindRouteCache` refuses a non-empty `partitionBy` — so an\n * unconfigured table is always an empty one.\n */\n entrypoint: string | undefined\n\n /**\n * `gateway.keyBy` — the request headers whose values join the cache key.\n *\n * Carried here rather than resolved per request because the dispatch\n * middleware already memoises this table, so reading it costs a property\n * access on the hot path instead of a container lookup.\n */\n keyBy: readonly string[] = []\n\n /** Set by `RouteRegistrationService.configure()`, before any request. */\n configure(entrypoint: string | undefined, keyBy: readonly string[] = []): void {\n this.entrypoint = entrypoint\n this.keyBy = keyBy\n }\n\n /** True when no route in this app declares a partition. */\n get isEmpty(): boolean {\n return this.entries.size === 0\n }\n\n /** Number of recorded routes. Test/diagnostic affordance. */\n get size(): number {\n return this.entries.size\n }\n\n /**\n * Record one registered route.\n *\n * `method` is the Hono-registered method; `path` is the pattern the route\n * was registered under (already expanded for versioning and locale\n * variants, so each variant gets its own entry).\n */\n record(method: string, path: string, partitionBy: string[]): void {\n if (partitionBy.length === 0) return\n this.entries.set(`${method.toUpperCase()} ${path}`, { partitionBy })\n }\n\n /** The entry for a matched route pattern, or `undefined` to run inline. */\n lookup(method: string, path: string): GatewayRouteEntry | undefined {\n return this.entries.get(`${method.toUpperCase()} ${path}`)\n }\n}\n","import { inject } from '../../di'\nimport { Singleton } from '../../di/decorators'\nimport { LOGGER_TOKENS, type LoggerService } from '../../logger'\nimport type { RouterContext } from '../../router/router-context'\nimport { ResponseCacheConfigError } from '../errors'\nimport { RESPONSE_CACHE_TOKENS } from '../response-cache.tokens'\nimport type { ResponseCacheModuleOptions } from '../types'\n\nexport interface PartitionResult {\n /** Values to place in `ctx.props`. Empty when nothing resolved. */\n props: Record<string, string>\n /** False when any declared partition could not be resolved — do not cache. */\n resolved: boolean\n}\n\n/**\n * Resolves the named partitions a route declared into `ctx.props` values.\n *\n * `ctx.props` is part of the Workers Caching cache key and, per Cloudflare,\n * \"cannot be bypassed\" — so this is what keeps one caller's cached response\n * from reaching another.\n */\n@Singleton(RESPONSE_CACHE_TOKENS.PartitionResolverService)\nexport class PartitionResolverService {\n constructor(\n @inject(RESPONSE_CACHE_TOKENS.Options) private readonly options: ResponseCacheModuleOptions,\n @inject(LOGGER_TOKENS.LoggerService) private readonly logger: LoggerService,\n ) {}\n\n /** Throw at boot if a route names a partition with no registered resolver. */\n assertKnown(names: string[], where: string): void {\n const known = Object.keys(this.options.partitions ?? {})\n\n for (const name of names) {\n if (!known.includes(name)) {\n throw new ResponseCacheConfigError(\n `${where}: unknown partition \"${name}\". Register it via ` +\n `ResponseCacheModule.forRoot({ partitions: { ${name}: (ctx) => ... } }). ` +\n `Known partitions: ${known.length > 0 ? known.join(', ') : '(none)'}.`,\n )\n }\n }\n }\n\n /**\n * Partition names seen so far, split by whether they have ever resolved.\n *\n * A resolver that fails is ordinary traffic — an anonymous visitor to a\n * per-user route — so the per-request signal stays at `debug`. But a\n * partition that has *never* resolved is a configuration mistake, not\n * traffic: the usual cause is `partitions: { user: (ctx) => ctx.user().id }`\n * configured without `primers: AUTH_GATEWAY_PRIMERS`, so `ctx.user()` throws\n * on every request and the route is silently never cached. Warned once per\n * name, so that diagnosis is available without making normal misses noisy.\n */\n private readonly warnedUnresolved = new Set<string>()\n private readonly everResolved = new Set<string>()\n\n private noteUnresolved(name: string): void {\n if (this.everResolved.has(name) || this.warnedUnresolved.has(name)) return\n\n this.warnedUnresolved.add(name)\n this.logger.warn(\n `[stratal:response-cache] Partition \"${name}\" has never resolved; routes using it are ` +\n 'not being cached. If its resolver calls `ctx.user()` or reads the request container, ' +\n 'the gateway needs `primers` (e.g. `AUTH_GATEWAY_PRIMERS` from @stratal/framework) to ' +\n 'populate that context before resolution runs.',\n { partition: name },\n )\n }\n\n async resolve(ctx: RouterContext, names: string[]): Promise<PartitionResult> {\n if (names.length === 0) return { props: {}, resolved: true }\n\n const partitions = this.options.partitions ?? {}\n const props: Record<string, string> = {}\n\n for (const name of names) {\n const resolver = partitions[name]\n if (!resolver) return { props: {}, resolved: false }\n\n let value: string | null | undefined\n try {\n value = await resolver(ctx)\n } catch (error) {\n // An anonymous request to a per-user route makes ctx.user() throw.\n // That is ordinary traffic, not a fault — it just is not cacheable.\n this.logger.debug('[stratal:response-cache] Partition resolver threw; not caching', {\n partition: name,\n error,\n })\n this.noteUnresolved(name)\n return { props: {}, resolved: false }\n }\n\n if (value === null || value === undefined) {\n this.logger.debug('[stratal:response-cache] Partition unresolved; not caching', { partition: name })\n this.noteUnresolved(name)\n return { props: {}, resolved: false }\n }\n\n this.everResolved.add(name)\n props[name] = value\n }\n\n return { props, resolved: true }\n }\n}\n","import { inject } from '../../di'\nimport { Singleton } from '../../di/decorators'\nimport { LOGGER_TOKENS, type LoggerService } from '../../logger'\nimport { CachePurgeError } from '../errors'\nimport { RESPONSE_CACHE_TOKENS } from '../response-cache.tokens'\nimport { renderTags, type TagScopes } from '../tag-template'\nimport type { PurgesCacheOptions } from '../types'\n\n// Declared on `StratalExecutionContext` so the foundational module does not\n// have to depend on this feature to describe `ctx.cache`. Re-exported here (and\n// from `stratal/response-cache`) so consumers import them alongside the API\n// that uses them.\nimport type { PurgeSpec, WorkersCache } from '../../execution-context'\n\nexport type { PurgeSpec, WorkersCache }\n\n@Singleton(RESPONSE_CACHE_TOKENS.ResponseCacheService)\nexport class ResponseCacheService {\n constructor(\n @inject(LOGGER_TOKENS.LoggerService) private readonly logger: LoggerService,\n ) {}\n\n /** Render a `@PurgesCache` declaration against this request's scopes. */\n buildPurgeSpec(options: PurgesCacheOptions, scopes: TagScopes): PurgeSpec {\n if (options.purgeEverything) return { purgeEverything: true }\n\n const spec: PurgeSpec = {}\n if (options.tags?.length) spec.tags = renderTags(options.tags, scopes)\n if (options.pathPrefixes?.length) spec.pathPrefixes = [...options.pathPrefixes]\n\n return spec\n }\n\n /**\n * Issue a purge and wait for it.\n *\n * Deliberately not deferred through `waitUntil`: a client that re-reads\n * immediately after its own write must not be served the pre-write response.\n *\n * @throws {CachePurgeError} The mutation has already committed at this point,\n * so failing loudly is the lesser evil against a cache that is silently\n * inconsistent with the database.\n */\n async purge(spec: PurgeSpec, cache: WorkersCache): Promise<void> {\n try {\n const result = await cache.purge(spec)\n\n if (result && ! result.success) {\n this.logger.error('[stratal:response-cache] Purge reported failure', { spec })\n throw new CachePurgeError(`the runtime reported failure for ${JSON.stringify(spec)}`)\n }\n } catch (error) {\n if (error instanceof CachePurgeError) throw error\n\n this.logger.error('[stratal:response-cache] Purge threw', {\n spec,\n error: error instanceof Error ? error.message : String(error),\n })\n throw new CachePurgeError(error instanceof Error ? error.message : String(error))\n }\n }\n}\n","import { Module } from '../module'\nimport type { AsyncModuleOptions, DynamicModule } from '../module/types'\nimport { assertNoGatewayOptions } from './boot-check'\nimport { CacheabilityService, GatewayPrimerService, GatewayRouteTable, PartitionResolverService, ResponseCacheService } from './services'\nimport { RESPONSE_CACHE_TOKENS } from './response-cache.tokens'\nimport type { ResponseCacheModuleOptions } from './types'\n\n/**\n * Response caching on Cloudflare Workers Caching — opt-in, NOT registered\n * automatically by `Application`.\n *\n * Requires `cache.enabled = true` in the Wrangler config, Wrangler >= 4.69.0,\n * and `compatibility_date >= 2026-07-06`. Without that binding, a route\n * decorated with `@PurgesCache` throws `CachePurgeError` **at request time**\n * — after its mutation has already committed — rather than silently\n * skipping the purge and leaving the cache stale. Fail-loud is deliberate\n * here, but it does mean a `@PurgesCache` route deployed to an environment\n * that forgot `cache.enabled` turns every successful mutation into a `500`;\n * confirm the binding is present in every environment that uses it.\n *\n * `forRootAsync`'s `useFactory` **must be synchronous**, or resolve\n * synchronously from the caller's perspective: route registration reads\n * `defaults` before any async work this module kicks off could settle, so\n * an async factory's `Promise` is read as the options object itself,\n * silently discarding `defaults` (this is detected and throws at boot,\n * rather than silently caching without the intended defaults).\n *\n * Per-caller caching needs the gateway: `partitionBy`, `partitions`, and\n * `primers` all throw `ResponseCacheConfigError` at boot unless\n * `gateway: { entrypoint }` names a cached entrypoint built with\n * `cachedEntrypoint(stratal)` from `stratal/workers`. A route that declares a\n * partition it cannot honour must fail loudly rather than cache per-user data\n * publicly.\n *\n * ```typescript\n * @Module({\n * imports: [ResponseCacheModule.forRoot({\n * defaults: { ttl: 300, swr: 60, vary: ['Accept-Language'] },\n * })],\n * })\n * export class AppModule {}\n * ```\n *\n * With per-caller partitioning:\n *\n * ```typescript\n * // app.module.ts\n * ResponseCacheModule.forRoot({\n * gateway: { entrypoint: 'Cached' },\n * partitions: { user: (ctx) => ctx.user().id },\n * })\n *\n * // src/index.ts\n * const stratal = new Stratal({ module: AppModule })\n * export default stratal\n * export const Cached = cachedEntrypoint(stratal)\n * ```\n */\n@Module({\n providers: [\n { provide: RESPONSE_CACHE_TOKENS.CacheabilityService, useClass: CacheabilityService },\n { provide: RESPONSE_CACHE_TOKENS.PartitionResolverService, useClass: PartitionResolverService },\n { provide: RESPONSE_CACHE_TOKENS.GatewayPrimerService, useClass: GatewayPrimerService },\n { provide: RESPONSE_CACHE_TOKENS.ResponseCacheService, useClass: ResponseCacheService },\n { provide: RESPONSE_CACHE_TOKENS.GatewayRouteTable, useClass: GatewayRouteTable },\n ],\n})\nexport class ResponseCacheModule {\n static forRoot(options: ResponseCacheModuleOptions = {}): DynamicModule {\n assertNoGatewayOptions(options)\n\n return {\n module: ResponseCacheModule,\n providers: [{ provide: RESPONSE_CACHE_TOKENS.Options, useValue: options }],\n }\n }\n\n static forRootAsync(options: AsyncModuleOptions<ResponseCacheModuleOptions>): DynamicModule {\n return {\n module: ResponseCacheModule,\n providers: [\n {\n provide: RESPONSE_CACHE_TOKENS.Options,\n // Same rejection as `forRoot`, applied to whatever the factory\n // produced. A `Promise` is passed through untouched: an async\n // factory is its own boot error, raised in\n // `RouteRegistrationService.responseCacheDefaults()` with a message\n // about the real problem — inspecting a Promise for `partitions`\n // here would only report a missing option instead.\n useFactory: (...deps: unknown[]) => {\n const resolved = options.useFactory(...deps)\n if (!(resolved instanceof Promise)) assertNoGatewayOptions(resolved)\n return resolved\n },\n inject: options.inject ?? [],\n },\n ],\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAiCA,MAAM,gBAAgB;;AAEtB,MAAM,eAAe,IAAI,YAAY;;;;;;;;;;;;;;;;;;;;;AAqBrC,MAAM,YAAY;AAClB,MAAM,cAAc;AACpB,MAAM,SAAS;CAAC;CAAS;CAAS;CAAQ;CAAQ;AAAW;;;;;;;;;AAU7D,SAAgB,WAAW,WAAqB,QAA6B;CAC3E,MAAM,MAAgB,CAAC;CAIvB,MAAM,uBAAO,IAAI,IAAY;CAE7B,KAAK,MAAM,YAAY,WACrB,KAAK,MAAM,OAAO,UAAU,UAAU,MAAM,GAAG;EAC7C,eAAe,GAAG;EAClB,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG;GAClB,KAAK,IAAI,GAAG;GACZ,IAAI,KAAK,GAAG;EACd;CACF;CAGF,OAAO;AACT;;AAGA,SAAgB,eAAe,KAAmB;CAChD,IAAI,IAAI,WAAW,GACjB,MAAM,IAAI,qBAAqB,KAAK,cAAc;CAEpD,IAAI,aAAa,OAAO,GAAG,CAAC,CAAC,SAAS,eACpC,MAAM,IAAI,qBAAqB,KAAK,WAAW,cAAc,OAAO;CAEtE,IAAI,CAAC,UAAU,KAAK,GAAG,GACrB,MAAM,IAAI,qBACR,KACA,sKAEF;AAEJ;;AAGA,SAAS,UAAU,UAAkB,QAA6B;CAChE,MAAM,UAAU,CAAC,GAAG,SAAS,SAAS,WAAW,CAAC;CAClD,MAAM,UAAU,QAAQ,QAAQ,MAAM,EAAE,EAAE,CAAC,SAAS,IAAI,CAAC;CAEzD,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,qBAAqB,UAAU,4CAA4C;CAGvF,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC,WAAW,SAAS,QAAQ,KAAA,GAAW,QAAQ,CAAC;CAG1D,MAAM,CAAC,OAAO;CACd,MAAM,OAAO,IAAI,EAAE,CAAC,MAAM,GAAG,EAAE;CAC/B,MAAM,QAAQ,OAAO,IAAI,IAAI,MAAM,QAAQ,QAAQ;CAEnD,IAAI,CAAC,MAAM,QAAQ,KAAK,GACtB,MAAM,IAAI,qBAAqB,UAAU,KAAK,IAAI,GAAG,GAAG,KAAK,mBAAmB;CAGlF,OAAO,MAAM,KAAK,SAChB,WAAW,SAAS,QAAQ;EAAE,aAAa,IAAI;EAAI;CAAK,GAAG,QAAQ,CACrE;AACF;;AAGA,SAAS,WACP,SACA,QACA,KACA,UACQ;CACR,MAAM,QAAkB,CAAC;CACzB,IAAI,SAAS;CAEb,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,CAAC,aAAa,OAAO,QAAQ;EACnC,MAAM,QAAQ,MAAM,SAAS;EAE7B,MAAM,KAAK,SAAS,MAAM,QAAQ,KAAK,CAAC;EAExC,MAAM,QACJ,OAAO,gBAAgB,IAAI,cAAc,IAAI,OAAO,OAAO,OAAO,MAAM,QAAQ,QAAQ;EAC1F,MAAM,KAAK,UAAU,OAAO,QAAQ,CAAC;EAErC,SAAS,QAAQ,YAAY;CAC/B;CAEA,MAAM,KAAK,SAAS,MAAM,MAAM,CAAC;CAEjC,OAAO,MAAM,KAAK,EAAE;AACtB;;AAGA,SAAS,OAAO,OAAe,MAAc,QAAmB,UAA2B;CACzF,IAAI,CAAE,OAA6B,SAAS,KAAK,GAC/C,MAAM,IAAI,qBACR,UACA,kBAAkB,MAAM,qBAAqB,OAAO,KAAK,IAAI,EAAE,EACjE;CAGF,IAAI,UAAmB,OAAO;CAE9B,KAAK,MAAM,WAAW,KAAK,MAAM,GAAG,GAAG;EACrC,IAAI,YAAY,QAAQ,YAAY,KAAA,KAAa,OAAO,YAAY,UAClE,MAAM,IAAI,qBAAqB,UAAU,KAAK,MAAM,GAAG,KAAK,kCAAkC;EAEhG,UAAW,QAAoC;CACjD;CAEA,IAAI,YAAY,QAAQ,YAAY,KAAA,GAClC,MAAM,IAAI,qBAAqB,UAAU,KAAK,MAAM,GAAG,KAAK,kCAAkC;CAGhG,OAAO;AACT;;AAGA,SAAS,UAAU,OAAgB,UAA0B;CAC3D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW,OAAO,OAAO,KAAK;CAChF,MAAM,IAAI,qBAAqB,UAAU,4CAA4C,OAAO,MAAM,EAAE;AACtG;;;;AC5JA,MAAM,WAAW;AAaV,IAAM,sBAAN,MAAM,oBAAoB;CAEyB;CACE;CAF1D,YACE,QACA,SACA;EAFsD,KAAA,SAAA;EACE,KAAA,UAAA;CACvD;CAEH,MAAM,UAAoB,UAA6B,QAAmB,SAAwC;EAGhH,MAAM,OAAO,KAAK,UAAU,UAAU,CAAC,GAAG,SAAS,MAAM,GAAI,QAAQ,SAAS,eAAe,CAAC,CAAE,CAAC;EAEjG,MAAM,SAAS,KAAK,gBAAgB,UAAU,SAAS,IAAI;EAE3D,IAAI,QAAQ;GACV,KAAK,OAAO,KAAK,gDAAgD,EAAE,OAAO,CAAC;GAC3E,OAAO,KAAK,YAAY,UAAU,EAAE,iBAAiB,SAAS,CAAC;EACjE;EAQA,MAAM,UAAkC;GACtC,iBAAiB,KAAK,oBAAoB,QAAQ;GAClD,qBAAqB,KAAK,mBAAmB,QAAQ;EACvD;EAOA,IAAI;EACJ,IAAI;GACF,OAAO,WAAW,SAAS,MAAM,MAAM;EACzC,SAAS,OAAO;GACd,KAAK,OAAO,MAAM,2DAA2D,EAAE,MAAM,CAAC;GACtF,OAAO,KAAK,YAAY,UAAU,EAAE,iBAAiB,SAAS,CAAC;EACjE;EAEA,IAAI,KAAK,SAAS,GAAG,QAAQ,eAAe,KAAK,KAAK,GAAG;EAEzD,IAAI,MAAM,QAAQ,OAAO;EAEzB,OAAO,KAAK,YAAY,UAAU,OAAO;CAC3C;;CAGA,gBAAwB,UAAoB,SAA8B,MAAoC;EAC5G,IAAI,SAAS,QAAQ,IAAI,YAAY,GAAG,OAAO;EAC/C,IAAI,SAAS,SAAS,OAAO,SAAS,SAAS,KAAK,OAAO;EAC3D,IAAI,CAAC,QAAQ,oBAAoB,OAAO;EAExC,MAAM,UAAU,QAAQ;EACxB,IAAI,SAAS,UAAU,OAAO;EAC9B,IAAI,SAAS,cAAc,OAAO;EAOlC,IAAI,SAAS,aAAa,QAAQ,YAAY,WAAW,GAAG,OAAO;EAgBnE,MAAM,UAAU,KAAK,QAAQ;EAC7B,MAAM,UAAU,UAAU,YAAY,MAAM,QAAQ,KAAK,IAAI,CAAC;EAC9D,IAAI,QAAQ,SAAS,GAAG;GACtB,KAAK,OAAO,KACV,+CAA+C,QAAQ,KAAK,IAAI,EAAE,4MAIlE,EAAE,MAAM,QAAQ,CAClB;GACA,OAAO;EACT;EAEA,OAAO;CACT;;;;;;;;;;;;;;;;;;;;;CAsBA,oBAA4B,UAAqC;EAC/D,MAAM,QAAQ,CAAC,UAAU,WAAW,SAAS,YAAY;EAEzD,IAAI,SAAS,QAAQ,KAAA,GAAW,MAAM,KAAK,0BAA0B,SAAS,KAAK;OAC9E,IAAI,SAAS,eAAe,GAAG,MAAM,KAAK,iBAAiB;EAEhE,OAAO,MAAM,KAAK,IAAI;CACxB;;CAGA,mBAA2B,UAAqC;EAC9D,MAAM,QAAQ,CAAC,UAAU,WAAW,SAAS,KAAK;EAClD,IAAI,SAAS,QAAQ,KAAA,GAAW,MAAM,KAAK,0BAA0B,SAAS,KAAK;EACnF,OAAO,MAAM,KAAK,IAAI;CACxB;;CAGA,UAAkB,UAAoB,UAAmC;EACvE,MAAM,YAAY,SAAS,QAAQ,IAAI,MAAM,KAAK,GAAA,CAC/C,MAAM,GAAG,CAAC,CACV,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CACpB,OAAO,OAAO;EAEjB,MAAM,SAAS,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,UAAU,GAAG,QAAQ,CAAC,CAAC;EACtD,OAAO,OAAO,SAAS,IAAI,OAAO,KAAK,IAAI,IAAI;CACjD;;;;;;;;;;;CAYA,YAAoB,UAAoB,SAA2C;EACjF,MAAM,QAAuC,EAAE,GAAG,QAAQ;EAC1D,IAAI,QAAQ,qBAAqB,UAAU;GACzC,MAAM,eAAe;GACrB,MAAM,uBAAuB;EAC/B;EAEA,OAAO,mBAAmB,UAAU,KAAK;CAC3C;AACF;;CAnKC,UAAU,sBAAsB,mBAAmB;CAG/C,gBAAA,GAAA,OAAO,cAAc,aAAa,CAAA;CAClC,gBAAA,GAAA,OAAO,sBAAsB,OAAO,CAAA;;;;ACxBlC,IAAM,uBAAN,MAAM,qBAAqB;CAE0B;CACF;CAFxD,YACE,SACA,QACA;EAFwD,KAAA,UAAA;EACF,KAAA,SAAA;CACrD;CAEH,IAAI,aAAsB;EACxB,QAAQ,KAAK,QAAQ,SAAS,UAAU,KAAK;CAC/C;;;;;;;;;;;;;;;;;;;;;;CAuBA,MAAM,MAAM,KAAsC;EAChD,MAAM,UAAU,KAAK,QAAQ,WAAW,CAAC;EACzC,MAAM,YAAY,IAAI,aAAa;EAEnC,KAAK,MAAM,UAAU,SAAS;GAK5B,MAAM,SAAS,MAJI,UAAU,QAAoB,MAInB,CAAC,CAAC,OAAO,KAAK,YAAY,CAAC,CAAC;GAE1D,IAAI,kBAAkB,UAAU;IAC9B,KAAK,OAAO,MAAM,gEAAgE;KAChF,QAAS,OAAmC;KAC5C,QAAQ,OAAO;IACjB,CAAC;IACD,OAAO;GACT;EACF;EAEA,OAAO;CACT;AACF;;CAtDC,UAAU,sBAAsB,oBAAoB;CAGhD,gBAAA,GAAA,OAAO,sBAAsB,OAAO,CAAA;CACpC,gBAAA,GAAA,OAAO,cAAc,aAAa,CAAA;;;;ACAhC,IAAM,oBAAN,MAAM,kBAAkB;CAC7B,0BAA2B,IAAI,IAA+B;;;;;;;;;CAU9D;;;;;;;;CASA,QAA2B,CAAC;;CAG5B,UAAU,YAAgC,QAA2B,CAAC,GAAS;EAC7E,KAAK,aAAa;EAClB,KAAK,QAAQ;CACf;;CAGA,IAAI,UAAmB;EACrB,OAAO,KAAK,QAAQ,SAAS;CAC/B;;CAGA,IAAI,OAAe;EACjB,OAAO,KAAK,QAAQ;CACtB;;;;;;;;CASA,OAAO,QAAgB,MAAc,aAA6B;EAChE,IAAI,YAAY,WAAW,GAAG;EAC9B,KAAK,QAAQ,IAAI,GAAG,OAAO,YAAY,EAAE,GAAG,QAAQ,EAAE,YAAY,CAAC;CACrE;;CAGA,OAAO,QAAgB,MAA6C;EAClE,OAAO,KAAK,QAAQ,IAAI,GAAG,OAAO,YAAY,EAAE,GAAG,MAAM;CAC3D;AACF;AAvDC,oBAAA,WAAA,CAAA,UAAU,sBAAsB,iBAAiB,CAAA,GAAA,iBAAA;;;ACJ3C,IAAM,2BAAN,MAAM,yBAAyB;CAEsB;CACF;CAFxD,YACE,SACA,QACA;EAFwD,KAAA,UAAA;EACF,KAAA,SAAA;CACrD;;CAGH,YAAY,OAAiB,OAAqB;EAChD,MAAM,QAAQ,OAAO,KAAK,KAAK,QAAQ,cAAc,CAAC,CAAC;EAEvD,KAAK,MAAM,QAAQ,OACjB,IAAI,CAAC,MAAM,SAAS,IAAI,GACtB,MAAM,IAAI,yBACR,GAAG,MAAM,uBAAuB,KAAK,iEACY,KAAK,yCAC/B,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,SAAS,EACxE;CAGN;;;;;;;;;;;;CAaA,mCAAoC,IAAI,IAAY;CACpD,+BAAgC,IAAI,IAAY;CAEhD,eAAuB,MAAoB;EACzC,IAAI,KAAK,aAAa,IAAI,IAAI,KAAK,KAAK,iBAAiB,IAAI,IAAI,GAAG;EAEpE,KAAK,iBAAiB,IAAI,IAAI;EAC9B,KAAK,OAAO,KACV,uCAAuC,KAAK,0QAI5C,EAAE,WAAW,KAAK,CACpB;CACF;CAEA,MAAM,QAAQ,KAAoB,OAA2C;EAC3E,IAAI,MAAM,WAAW,GAAG,OAAO;GAAE,OAAO,CAAC;GAAG,UAAU;EAAK;EAE3D,MAAM,aAAa,KAAK,QAAQ,cAAc,CAAC;EAC/C,MAAM,QAAgC,CAAC;EAEvC,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,WAAW,WAAW;GAC5B,IAAI,CAAC,UAAU,OAAO;IAAE,OAAO,CAAC;IAAG,UAAU;GAAM;GAEnD,IAAI;GACJ,IAAI;IACF,QAAQ,MAAM,SAAS,GAAG;GAC5B,SAAS,OAAO;IAGd,KAAK,OAAO,MAAM,kEAAkE;KAClF,WAAW;KACX;IACF,CAAC;IACD,KAAK,eAAe,IAAI;IACxB,OAAO;KAAE,OAAO,CAAC;KAAG,UAAU;IAAM;GACtC;GAEA,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW;IACzC,KAAK,OAAO,MAAM,8DAA8D,EAAE,WAAW,KAAK,CAAC;IACnG,KAAK,eAAe,IAAI;IACxB,OAAO;KAAE,OAAO,CAAC;KAAG,UAAU;IAAM;GACtC;GAEA,KAAK,aAAa,IAAI,IAAI;GAC1B,MAAM,QAAQ;EAChB;EAEA,OAAO;GAAE;GAAO,UAAU;EAAK;CACjC;AACF;;CArFC,UAAU,sBAAsB,wBAAwB;CAGpD,gBAAA,GAAA,OAAO,sBAAsB,OAAO,CAAA;CACpC,gBAAA,GAAA,OAAO,cAAc,aAAa,CAAA;;;;ACThC,IAAM,uBAAN,MAAM,qBAAqB;CAEwB;CADxD,YACE,QACA;EADsD,KAAA,SAAA;CACrD;;CAGH,eAAe,SAA6B,QAA8B;EACxE,IAAI,QAAQ,iBAAiB,OAAO,EAAE,iBAAiB,KAAK;EAE5D,MAAM,OAAkB,CAAC;EACzB,IAAI,QAAQ,MAAM,QAAQ,KAAK,OAAO,WAAW,QAAQ,MAAM,MAAM;EACrE,IAAI,QAAQ,cAAc,QAAQ,KAAK,eAAe,CAAC,GAAG,QAAQ,YAAY;EAE9E,OAAO;CACT;;;;;;;;;;;CAYA,MAAM,MAAM,MAAiB,OAAoC;EAC/D,IAAI;GACF,MAAM,SAAS,MAAM,MAAM,MAAM,IAAI;GAErC,IAAI,UAAU,CAAE,OAAO,SAAS;IAC9B,KAAK,OAAO,MAAM,mDAAmD,EAAE,KAAK,CAAC;IAC7E,MAAM,IAAI,gBAAgB,oCAAoC,KAAK,UAAU,IAAI,GAAG;GACtF;EACF,SAAS,OAAO;GACd,IAAI,iBAAiB,iBAAiB,MAAM;GAE5C,KAAK,OAAO,MAAM,wCAAwC;IACxD;IACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D,CAAC;GACD,MAAM,IAAI,gBAAgB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;EAClF;CACF;AACF;AA7CC,uBAAA,WAAA,CAAA,UAAU,sBAAsB,oBAAoB,GAAA,gBAAA,GAGhD,OAAO,cAAc,aAAa,CAAA,CAAA,GAAA,oBAAA;;;;ACgDhC,IAAM,sBAAA,uBAAN,MAAM,oBAAoB;CAC/B,OAAO,QAAQ,UAAsC,CAAC,GAAkB;EACtE,uBAAuB,OAAO;EAE9B,OAAO;GACL,QAAA;GACA,WAAW,CAAC;IAAE,SAAS,sBAAsB;IAAS,UAAU;GAAQ,CAAC;EAC3E;CACF;CAEA,OAAO,aAAa,SAAwE;EAC1F,OAAO;GACL,QAAA;GACA,WAAW,CACT;IACE,SAAS,sBAAsB;IAO/B,aAAa,GAAG,SAAoB;KAClC,MAAM,WAAW,QAAQ,WAAW,GAAG,IAAI;KAC3C,IAAI,EAAE,oBAAoB,UAAU,uBAAuB,QAAQ;KACnE,OAAO;IACT;IACA,QAAQ,QAAQ,UAAU,CAAC;GAC7B,CACF;EACF;CACF;AACF;AAzCC,sBAAA,uBAAA,WAAA,CAAA,OAAO,EACN,WAAW;CACT;EAAE,SAAS,sBAAsB;EAAqB,UAAU;CAAoB;CACpF;EAAE,SAAS,sBAAsB;EAA0B,UAAU;CAAyB;CAC9F;EAAE,SAAS,sBAAsB;EAAsB,UAAU;CAAqB;CACtF;EAAE,SAAS,sBAAsB;EAAsB,UAAU;CAAqB;CACtF;EAAE,SAAS,sBAAsB;EAAmB,UAAU;CAAkB;AAClF,EACF,CAAC,CAAA,GAAA,mBAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { a as ApplicationError } from "./container-storage-DfF7SXdV.mjs";
|
|
2
|
+
//#region src/response-cache/errors/response-cache.error.ts
|
|
3
|
+
/** A rendered `Cache-Tag` value Cloudflare would silently drop. */
|
|
4
|
+
var InvalidCacheTagError = class extends ApplicationError {
|
|
5
|
+
constructor(tag, reason) {
|
|
6
|
+
super(`[stratal:response-cache] Invalid Cache-Tag "${tag}": ${reason}`);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
/** A purge failed after its mutation already committed. */
|
|
10
|
+
var CachePurgeError = class extends ApplicationError {
|
|
11
|
+
constructor(detail, cause) {
|
|
12
|
+
super(`[stratal:response-cache] Cache purge failed: ${detail}`, cause);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** Misconfiguration detected at boot. */
|
|
16
|
+
var ResponseCacheConfigError = class extends ApplicationError {
|
|
17
|
+
constructor(detail) {
|
|
18
|
+
super(`[stratal:response-cache] ${detail}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { InvalidCacheTagError as n, ResponseCacheConfigError as r, CachePurgeError as t };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=response-cache.error-B4D0zy9W.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-cache.error-B4D0zy9W.mjs","names":[],"sources":["../src/response-cache/errors/response-cache.error.ts"],"sourcesContent":["import { ApplicationError } from '../../errors'\n\n/** A rendered `Cache-Tag` value Cloudflare would silently drop. */\nexport class InvalidCacheTagError extends ApplicationError {\n constructor(tag: string, reason: string) {\n super(`[stratal:response-cache] Invalid Cache-Tag \"${tag}\": ${reason}`)\n }\n}\n\n/** A purge failed after its mutation already committed. */\nexport class CachePurgeError extends ApplicationError {\n constructor(detail: string, cause?: unknown) {\n super(`[stratal:response-cache] Cache purge failed: ${detail}`, cause)\n }\n}\n\n/** Misconfiguration detected at boot. */\nexport class ResponseCacheConfigError extends ApplicationError {\n constructor(detail: string) {\n super(`[stratal:response-cache] ${detail}`)\n }\n}\n"],"mappings":";;;AAGA,IAAa,uBAAb,cAA0C,iBAAiB;CACzD,YAAY,KAAa,QAAgB;EACvC,MAAM,+CAA+C,IAAI,KAAK,QAAQ;CACxE;AACF;;AAGA,IAAa,kBAAb,cAAqC,iBAAiB;CACpD,YAAY,QAAgB,OAAiB;EAC3C,MAAM,gDAAgD,UAAU,KAAK;CACvE;AACF;;AAGA,IAAa,2BAAb,cAA8C,iBAAiB;CAC7D,YAAY,QAAgB;EAC1B,MAAM,4BAA4B,QAAQ;CAC5C;AACF"}
|