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,158 @@
|
|
|
1
|
+
//#region src/response-cache/cached-entrypoint.ts
|
|
2
|
+
/** Methods Workers Caching will ever store. Everything else always runs inline. */
|
|
3
|
+
const CACHEABLE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
4
|
+
/**
|
|
5
|
+
* Whether the gateway should forward this request into the cached entrypoint.
|
|
6
|
+
*
|
|
7
|
+
* Anything that returns `false` runs inline in the gateway and never reaches
|
|
8
|
+
* the cached entrypoint at all — which is what keeps RFC 9111 heuristic
|
|
9
|
+
* freshness (a `200` with no `Cache-Control` cached for two hours) away from
|
|
10
|
+
* un-annotated routes.
|
|
11
|
+
*/
|
|
12
|
+
function shouldLoopback(method, cacheable) {
|
|
13
|
+
if (!cacheable) return false;
|
|
14
|
+
return CACHEABLE_METHODS.has(method.toUpperCase());
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/response-cache/representation.ts
|
|
18
|
+
/**
|
|
19
|
+
* Folding a response's *representation* into the cache key.
|
|
20
|
+
*
|
|
21
|
+
* A route may answer one URL with more than one body — an HTML document and a
|
|
22
|
+
* JSON payload for the same page, say — chosen from a request header. HTTP's
|
|
23
|
+
* answer is `Vary`, and Workers Caching documents it as honoured, so a
|
|
24
|
+
* response that declares `Vary: X-Inertia` should be stored once per distinct
|
|
25
|
+
* value of that header.
|
|
26
|
+
*
|
|
27
|
+
* Measured, it is not reliable: on a URL carrying a query string, a variant
|
|
28
|
+
* stored for one value of the header is served to a request sending another,
|
|
29
|
+
* so a browser navigation receives the JSON meant for a client-side visit.
|
|
30
|
+
* `Vary` is a matching pass applied to an entry that has already been located
|
|
31
|
+
* by key; `ctx.props` IS part of the key, and Cloudflare documents it as
|
|
32
|
+
* impossible to bypass — the same property that makes it the mechanism for
|
|
33
|
+
* per-caller partitioning.
|
|
34
|
+
*
|
|
35
|
+
* So the representation goes in the key. Two representations become two
|
|
36
|
+
* entries, and nothing downstream has to match correctly for the right one to
|
|
37
|
+
* come back. `Vary` is still emitted, because it is what tells the *browser*
|
|
38
|
+
* cache the same thing and the Inertia protocol requires it — it is simply no
|
|
39
|
+
* longer load-bearing here.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* The `ctx.props` key the representation is carried under.
|
|
43
|
+
*
|
|
44
|
+
* `$`-prefixed so it cannot collide with a partition name: partitions are
|
|
45
|
+
* referenced from `partitionBy: ['user']` and are written as identifiers, and
|
|
46
|
+
* `assertPartitionNames` rejects the prefix outright rather than letting a
|
|
47
|
+
* collision silently overwrite one or the other.
|
|
48
|
+
*/
|
|
49
|
+
const REPRESENTATION_PROP = "$representation";
|
|
50
|
+
/**
|
|
51
|
+
* Headers the platform varies on by itself, which therefore need no key of
|
|
52
|
+
* ours.
|
|
53
|
+
*
|
|
54
|
+
* Compression is negotiated and stored by Cloudflare independently of the
|
|
55
|
+
* response body the Worker returns, so `Vary: Accept-Encoding` — which almost
|
|
56
|
+
* every framework and proxy emits — describes something already handled. Left
|
|
57
|
+
* out of this list it would make nearly every response uncacheable.
|
|
58
|
+
*/
|
|
59
|
+
const PLATFORM_HANDLED = /* @__PURE__ */ new Set(["accept-encoding"]);
|
|
60
|
+
/**
|
|
61
|
+
* The request's values for `keyBy`, as one deterministic string.
|
|
62
|
+
*
|
|
63
|
+
* Names are lowercased and sorted so that reordering `keyBy` does not
|
|
64
|
+
* re-partition a live cache, and each value is carried as `null` when the
|
|
65
|
+
* header is absent — a header that is missing and one sent empty are different
|
|
66
|
+
* requests and must not collapse onto one entry.
|
|
67
|
+
*
|
|
68
|
+
* Returns `undefined` when nothing is declared, so the common case adds no
|
|
69
|
+
* prop at all and leaves the key exactly as it was.
|
|
70
|
+
*/
|
|
71
|
+
function representationOf(headers, keyBy) {
|
|
72
|
+
if (!keyBy || keyBy.length === 0) return void 0;
|
|
73
|
+
const names = [...new Set(keyBy.map((name) => name.toLowerCase()))].sort();
|
|
74
|
+
return JSON.stringify(names.map((name) => [name, headers.get(name)]));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The names a response says it varies on that are NOT in the cache key.
|
|
78
|
+
*
|
|
79
|
+
* Each one is a way for two different bodies to share one entry, so a response
|
|
80
|
+
* naming any of them is refused rather than stored — see `CacheabilityService`.
|
|
81
|
+
* `*` counts as unkeyed: Cloudflare treats `Vary: *` as uncacheable anyway, and
|
|
82
|
+
* it cannot be satisfied from request headers by definition.
|
|
83
|
+
*/
|
|
84
|
+
function unkeyedVary(varyHeader, keyBy) {
|
|
85
|
+
if (!varyHeader) return [];
|
|
86
|
+
const keyed = new Set((keyBy ?? []).map((name) => name.toLowerCase()));
|
|
87
|
+
return varyHeader.split(",").map((name) => name.trim().toLowerCase()).filter(Boolean).filter((name) => !keyed.has(name) && !PLATFORM_HANDLED.has(name));
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/response-cache/response-headers.ts
|
|
91
|
+
/**
|
|
92
|
+
* Set (or delete, via `null`) one or more headers on a `Response`, mutating
|
|
93
|
+
* in place whenever the header list allows it.
|
|
94
|
+
*
|
|
95
|
+
* A `Response`'s header list has a "guard" — per the Fetch spec, plain
|
|
96
|
+
* `Response`s (anything built with `new Response(...)`, which is what every
|
|
97
|
+
* Stratal controller returns) get guard `"response"`, which is mutable.
|
|
98
|
+
* `.set()`/`.delete()` only throw for guard `"immutable"`, which shows up for
|
|
99
|
+
* responses obtained from a `fetch()` subrequest or otherwise already handed
|
|
100
|
+
* off — not for a response this Worker constructed itself. That's the
|
|
101
|
+
* overwhelming majority of what flows through this function, so mutating in
|
|
102
|
+
* place avoids cloning the response (and re-wrapping its body stream) on
|
|
103
|
+
* every request just to add a `Cache-Control` header.
|
|
104
|
+
*
|
|
105
|
+
* Falls back to reconstructing the `Response` only when a `.set()`/`.delete()`
|
|
106
|
+
* call actually throws. Skips touching the response entirely outside the
|
|
107
|
+
* `[200, 599]` status range: the `Response` constructor throws a `RangeError`
|
|
108
|
+
* for a status outside that range (notably `101`), and — for a `101`
|
|
109
|
+
* WebSocket-upgrade response specifically — reconstructing would drop
|
|
110
|
+
* Cloudflare's `webSocket`/`cf` init fields the original carried, which
|
|
111
|
+
* `{ status, statusText, headers }` alone can't reproduce.
|
|
112
|
+
*/
|
|
113
|
+
function setResponseHeaders(response, headers) {
|
|
114
|
+
if (response.status < 200 || response.status > 599) return response;
|
|
115
|
+
try {
|
|
116
|
+
for (const [name, value] of Object.entries(headers)) if (value === null) response.headers.delete(name);
|
|
117
|
+
else response.headers.set(name, value);
|
|
118
|
+
return response;
|
|
119
|
+
} catch {
|
|
120
|
+
const merged = new Headers(response.headers);
|
|
121
|
+
for (const [name, value] of Object.entries(headers)) if (value === null) merged.delete(name);
|
|
122
|
+
else merged.set(name, value);
|
|
123
|
+
return new Response(response.body, {
|
|
124
|
+
status: response.status,
|
|
125
|
+
statusText: response.statusText,
|
|
126
|
+
headers: merged
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** `public` as a whole directive, not the substring inside `no-cache="public"` or a field name. */
|
|
131
|
+
const PUBLIC_DIRECTIVE = /(?:^|,)\s*public\s*(?:,|$)/;
|
|
132
|
+
/**
|
|
133
|
+
* Whether a SHARED cache is permitted to store this response and serve it to a
|
|
134
|
+
* different caller.
|
|
135
|
+
*
|
|
136
|
+
* `public` and `s-maxage` are the two directives that say so outright (RFC 9111
|
|
137
|
+
* §5.2.2). Everything else is treated as not shared-cacheable: `private` and
|
|
138
|
+
* `no-store` say so explicitly, and a response with no `Cache-Control` at all
|
|
139
|
+
* never reaches a caller in a Stratal app without one — `createNoStoreFallback\
|
|
140
|
+
Middleware` stamps it first.
|
|
141
|
+
*
|
|
142
|
+
* `CDN-Cache-Control` is read the same way and counts on its own. It is the
|
|
143
|
+
* header a CDN honours in preference to `Cache-Control`, so a response can be
|
|
144
|
+
* stored and replayed to other callers on the strength of it alone — reading
|
|
145
|
+
* only `Cache-Control` would call such a response private and leave whatever
|
|
146
|
+
* this guards, per-caller headers included, to be served to everyone.
|
|
147
|
+
*/
|
|
148
|
+
function isSharedCacheable(response) {
|
|
149
|
+
const says = (header) => {
|
|
150
|
+
const directives = response.headers.get(header)?.toLowerCase() ?? "";
|
|
151
|
+
return PUBLIC_DIRECTIVE.test(directives) || directives.includes("s-maxage");
|
|
152
|
+
};
|
|
153
|
+
return says("Cache-Control") || says("CDN-Cache-Control");
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
export { unkeyedVary as a, representationOf as i, setResponseHeaders as n, shouldLoopback as o, REPRESENTATION_PROP as r, isSharedCacheable as t };
|
|
157
|
+
|
|
158
|
+
//# sourceMappingURL=response-headers-BqA57S_X.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-headers-BqA57S_X.mjs","names":[],"sources":["../src/response-cache/cached-entrypoint.ts","../src/response-cache/representation.ts","../src/response-cache/response-headers.ts"],"sourcesContent":["import type { CacheableOptions } from './types'\n\n/** Methods Workers Caching will ever store. Everything else always runs inline. */\nconst CACHEABLE_METHODS = new Set(['GET', 'HEAD'])\n\n/**\n * Whether the gateway should forward this request into the cached entrypoint.\n *\n * Anything that returns `false` runs inline in the gateway and never reaches\n * the cached entrypoint at all — which is what keeps RFC 9111 heuristic\n * freshness (a `200` with no `Cache-Control` cached for two hours) away from\n * un-annotated routes.\n */\nexport function shouldLoopback(method: string, cacheable: CacheableOptions | undefined): boolean {\n if (!cacheable) return false\n return CACHEABLE_METHODS.has(method.toUpperCase())\n}\n","/**\n * Folding a response's *representation* into the cache key.\n *\n * A route may answer one URL with more than one body — an HTML document and a\n * JSON payload for the same page, say — chosen from a request header. HTTP's\n * answer is `Vary`, and Workers Caching documents it as honoured, so a\n * response that declares `Vary: X-Inertia` should be stored once per distinct\n * value of that header.\n *\n * Measured, it is not reliable: on a URL carrying a query string, a variant\n * stored for one value of the header is served to a request sending another,\n * so a browser navigation receives the JSON meant for a client-side visit.\n * `Vary` is a matching pass applied to an entry that has already been located\n * by key; `ctx.props` IS part of the key, and Cloudflare documents it as\n * impossible to bypass — the same property that makes it the mechanism for\n * per-caller partitioning.\n *\n * So the representation goes in the key. Two representations become two\n * entries, and nothing downstream has to match correctly for the right one to\n * come back. `Vary` is still emitted, because it is what tells the *browser*\n * cache the same thing and the Inertia protocol requires it — it is simply no\n * longer load-bearing here.\n */\n\n/**\n * The `ctx.props` key the representation is carried under.\n *\n * `$`-prefixed so it cannot collide with a partition name: partitions are\n * referenced from `partitionBy: ['user']` and are written as identifiers, and\n * `assertPartitionNames` rejects the prefix outright rather than letting a\n * collision silently overwrite one or the other.\n */\nexport const REPRESENTATION_PROP = '$representation'\n\n/**\n * Headers the platform varies on by itself, which therefore need no key of\n * ours.\n *\n * Compression is negotiated and stored by Cloudflare independently of the\n * response body the Worker returns, so `Vary: Accept-Encoding` — which almost\n * every framework and proxy emits — describes something already handled. Left\n * out of this list it would make nearly every response uncacheable.\n */\nconst PLATFORM_HANDLED = new Set(['accept-encoding'])\n\n/**\n * The request's values for `keyBy`, as one deterministic string.\n *\n * Names are lowercased and sorted so that reordering `keyBy` does not\n * re-partition a live cache, and each value is carried as `null` when the\n * header is absent — a header that is missing and one sent empty are different\n * requests and must not collapse onto one entry.\n *\n * Returns `undefined` when nothing is declared, so the common case adds no\n * prop at all and leaves the key exactly as it was.\n */\nexport function representationOf(\n headers: Headers,\n keyBy: readonly string[] | undefined,\n): string | undefined {\n if (!keyBy || keyBy.length === 0) return undefined\n\n const names = [...new Set(keyBy.map((name) => name.toLowerCase()))].sort()\n\n return JSON.stringify(names.map((name) => [name, headers.get(name)]))\n}\n\n/**\n * The names a response says it varies on that are NOT in the cache key.\n *\n * Each one is a way for two different bodies to share one entry, so a response\n * naming any of them is refused rather than stored — see `CacheabilityService`.\n * `*` counts as unkeyed: Cloudflare treats `Vary: *` as uncacheable anyway, and\n * it cannot be satisfied from request headers by definition.\n */\nexport function unkeyedVary(\n varyHeader: string | null,\n keyBy: readonly string[] | undefined,\n): string[] {\n if (!varyHeader) return []\n\n const keyed = new Set((keyBy ?? []).map((name) => name.toLowerCase()))\n\n return varyHeader\n .split(',')\n .map((name) => name.trim().toLowerCase())\n .filter(Boolean)\n .filter((name) => !keyed.has(name) && !PLATFORM_HANDLED.has(name))\n}\n","/**\n * Set (or delete, via `null`) one or more headers on a `Response`, mutating\n * in place whenever the header list allows it.\n *\n * A `Response`'s header list has a \"guard\" — per the Fetch spec, plain\n * `Response`s (anything built with `new Response(...)`, which is what every\n * Stratal controller returns) get guard `\"response\"`, which is mutable.\n * `.set()`/`.delete()` only throw for guard `\"immutable\"`, which shows up for\n * responses obtained from a `fetch()` subrequest or otherwise already handed\n * off — not for a response this Worker constructed itself. That's the\n * overwhelming majority of what flows through this function, so mutating in\n * place avoids cloning the response (and re-wrapping its body stream) on\n * every request just to add a `Cache-Control` header.\n *\n * Falls back to reconstructing the `Response` only when a `.set()`/`.delete()`\n * call actually throws. Skips touching the response entirely outside the\n * `[200, 599]` status range: the `Response` constructor throws a `RangeError`\n * for a status outside that range (notably `101`), and — for a `101`\n * WebSocket-upgrade response specifically — reconstructing would drop\n * Cloudflare's `webSocket`/`cf` init fields the original carried, which\n * `{ status, statusText, headers }` alone can't reproduce.\n */\nexport function setResponseHeaders(response: Response, headers: Record<string, string | null>): Response {\n if (response.status < 200 || response.status > 599) return response\n\n try {\n for (const [name, value] of Object.entries(headers)) {\n if (value === null) response.headers.delete(name)\n else response.headers.set(name, value)\n }\n return response\n } catch {\n const merged = new Headers(response.headers)\n for (const [name, value] of Object.entries(headers)) {\n if (value === null) merged.delete(name)\n else merged.set(name, value)\n }\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: merged,\n })\n }\n}\n\n/** `public` as a whole directive, not the substring inside `no-cache=\"public\"` or a field name. */\nconst PUBLIC_DIRECTIVE = /(?:^|,)\\s*public\\s*(?:,|$)/\n\n/**\n * Whether a SHARED cache is permitted to store this response and serve it to a\n * different caller.\n *\n * `public` and `s-maxage` are the two directives that say so outright (RFC 9111\n * §5.2.2). Everything else is treated as not shared-cacheable: `private` and\n * `no-store` say so explicitly, and a response with no `Cache-Control` at all\n * never reaches a caller in a Stratal app without one — `createNoStoreFallback\\\nMiddleware` stamps it first.\n *\n * `CDN-Cache-Control` is read the same way and counts on its own. It is the\n * header a CDN honours in preference to `Cache-Control`, so a response can be\n * stored and replayed to other callers on the strength of it alone — reading\n * only `Cache-Control` would call such a response private and leave whatever\n * this guards, per-caller headers included, to be served to everyone.\n */\nexport function isSharedCacheable(response: Response): boolean {\n const says = (header: string): boolean => {\n const directives = response.headers.get(header)?.toLowerCase() ?? ''\n return PUBLIC_DIRECTIVE.test(directives) || directives.includes('s-maxage')\n }\n\n return says('Cache-Control') || says('CDN-Cache-Control')\n}\n"],"mappings":";;AAGA,MAAM,oCAAoB,IAAI,IAAI,CAAC,OAAO,MAAM,CAAC;;;;;;;;;AAUjD,SAAgB,eAAe,QAAgB,WAAkD;CAC/F,IAAI,CAAC,WAAW,OAAO;CACvB,OAAO,kBAAkB,IAAI,OAAO,YAAY,CAAC;AACnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACgBA,MAAa,sBAAsB;;;;;;;;;;AAWnC,MAAM,mCAAmB,IAAI,IAAI,CAAC,iBAAiB,CAAC;;;;;;;;;;;;AAapD,SAAgB,iBACd,SACA,OACoB;CACpB,IAAI,CAAC,SAAS,MAAM,WAAW,GAAG,OAAO,KAAA;CAEzC,MAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,MAAM,KAAK,SAAS,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK;CAEzE,OAAO,KAAK,UAAU,MAAM,KAAK,SAAS,CAAC,MAAM,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC;AACtE;;;;;;;;;AAUA,SAAgB,YACd,YACA,OACU;CACV,IAAI,CAAC,YAAY,OAAO,CAAC;CAEzB,MAAM,QAAQ,IAAI,KAAK,SAAS,CAAC,EAAA,CAAG,KAAK,SAAS,KAAK,YAAY,CAAC,CAAC;CAErE,OAAO,WACJ,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CACxC,OAAO,OAAO,CAAC,CACf,QAAQ,SAAS,CAAC,MAAM,IAAI,IAAI,KAAK,CAAC,iBAAiB,IAAI,IAAI,CAAC;AACrE;;;;;;;;;;;;;;;;;;;;;;;;;AClEA,SAAgB,mBAAmB,UAAoB,SAAkD;CACvG,IAAI,SAAS,SAAS,OAAO,SAAS,SAAS,KAAK,OAAO;CAE3D,IAAI;EACF,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAChD,IAAI,UAAU,MAAM,SAAS,QAAQ,OAAO,IAAI;OAC3C,SAAS,QAAQ,IAAI,MAAM,KAAK;EAEvC,OAAO;CACT,QAAQ;EACN,MAAM,SAAS,IAAI,QAAQ,SAAS,OAAO;EAC3C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAChD,IAAI,UAAU,MAAM,OAAO,OAAO,IAAI;OACjC,OAAO,IAAI,MAAM,KAAK;EAE7B,OAAO,IAAI,SAAS,SAAS,MAAM;GACjC,QAAQ,SAAS;GACjB,YAAY,SAAS;GACrB,SAAS;EACX,CAAC;CACH;AACF;;AAGA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;AAkBzB,SAAgB,kBAAkB,UAA6B;CAC7D,MAAM,QAAQ,WAA4B;EACxC,MAAM,aAAa,SAAS,QAAQ,IAAI,MAAM,CAAC,EAAE,YAAY,KAAK;EAClE,OAAO,iBAAiB,KAAK,UAAU,KAAK,WAAW,SAAS,UAAU;CAC5E;CAEA,OAAO,KAAK,eAAe,KAAK,KAAK,mBAAmB;AAC1D"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { _ as defineMetadata, v as getMetadata } from "./di-DQ3xbjgf.mjs";
|
|
2
|
+
import { u as ROUTE_METADATA_KEYS } from "./exception-context-CdnGTQNF.mjs";
|
|
3
|
+
import { r as ResponseCacheConfigError } from "./response-cache.error-B4D0zy9W.mjs";
|
|
4
|
+
//#region src/response-cache/gateway-binding.ts
|
|
5
|
+
/**
|
|
6
|
+
* Look up the configured cached entrypoint on `ctx.exports`, failing loudly if
|
|
7
|
+
* it isn't there.
|
|
8
|
+
*
|
|
9
|
+
* A typo in `gateway: { entrypoint: 'Cachd' }`, a missing `export const
|
|
10
|
+
* Cached = ...`, or a missing `enable_ctx_exports` compatibility flag would
|
|
11
|
+
* otherwise all present identically: every partitioned route quietly runs
|
|
12
|
+
* inline in the gateway forever, never caching, with no signal anywhere. The
|
|
13
|
+
* whole point of `partitionBy` refusing to boot without a gateway is that a
|
|
14
|
+
* declared partition is either honoured or it is an error — so this is an
|
|
15
|
+
* error too.
|
|
16
|
+
*
|
|
17
|
+
* @throws {ResponseCacheConfigError} `ctx.exports` is unavailable, or carries
|
|
18
|
+
* no export under `name`.
|
|
19
|
+
*/
|
|
20
|
+
function resolveCachedEntrypoint(ctx, name) {
|
|
21
|
+
let exports;
|
|
22
|
+
try {
|
|
23
|
+
exports = ctx?.exports;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new ResponseCacheConfigError(`reading \`ctx.exports\` failed (${error instanceof Error ? error.message : String(error)}), so the cached entrypoint "${name}" cannot be reached. Add "enable_ctx_exports" to \`compatibility_flags\` in your Wrangler config.`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof exports !== "object" || exports === null) throw new ResponseCacheConfigError(`\`ctx.exports\` is unavailable on this entrypoint, so the cached entrypoint "${name}" configured via \`ResponseCacheModule.forRoot({ gateway: { entrypoint } })\` cannot be reached. Add "enable_ctx_exports" to \`compatibility_flags\` in your Wrangler config, and set \`compatibility_date\` to 2026-07-06 or later.`);
|
|
28
|
+
const binding = exports[name];
|
|
29
|
+
if (binding === void 0 || binding === null) {
|
|
30
|
+
const available = Object.keys(exports);
|
|
31
|
+
throw new ResponseCacheConfigError(`the cached entrypoint "${name}" is not exported by this Worker. \`ResponseCacheModule.forRoot({ gateway: { entrypoint } })\` must name a top-level export — \`export const ${name} = cachedEntrypoint(stratal)\` — that is also declared in your Wrangler \`exports\` block with \`cache: { enabled: true }\`. Exports visible on \`ctx.exports\`: ${available.length > 0 ? available.join(", ") : "(none)"}.`);
|
|
32
|
+
}
|
|
33
|
+
return binding;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A `WorkersCache` that purges through the cached entrypoint over RPC.
|
|
37
|
+
*
|
|
38
|
+
* Handed to `ResponseCacheService.purge()` in place of the gateway's own
|
|
39
|
+
* `ctx.cache` whenever a gateway is configured and this request is running as
|
|
40
|
+
* the gateway. See `cachedEntrypoint` for why that redirection is required
|
|
41
|
+
* rather than merely tidy.
|
|
42
|
+
*/
|
|
43
|
+
function createLoopbackPurgeTarget(ctx, name) {
|
|
44
|
+
return { purge: async (spec) => resolveCachedEntrypoint(ctx, name).purge(spec) };
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/response-cache/response-cache.tokens.ts
|
|
48
|
+
const RESPONSE_CACHE_TOKENS = {
|
|
49
|
+
Options: Symbol.for("stratal:response-cache:options"),
|
|
50
|
+
ResponseCacheService: Symbol.for("stratal:response-cache:service"),
|
|
51
|
+
CacheabilityService: Symbol.for("stratal:response-cache:cacheability"),
|
|
52
|
+
PartitionResolverService: Symbol.for("stratal:response-cache:partition-resolver"),
|
|
53
|
+
GatewayPrimerService: Symbol.for("stratal:response-cache:gateway-primer"),
|
|
54
|
+
GatewayPrimers: Symbol.for("stratal:response-cache:gateway-primers"),
|
|
55
|
+
GatewayRouteTable: Symbol.for("stratal:response-cache:gateway-route-table")
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/response-cache/decorators/cacheable.decorator.ts
|
|
59
|
+
const KEY$1 = ROUTE_METADATA_KEYS.CACHEABLE;
|
|
60
|
+
/**
|
|
61
|
+
* Mark a route's response as cacheable by Cloudflare Workers Caching.
|
|
62
|
+
*
|
|
63
|
+
* Only `GET` and `HEAD` are ever cached. Omitted fields fall back to the
|
|
64
|
+
* module's `defaults` block; an explicit value always wins.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* @Get('/blog/:slug')
|
|
69
|
+
* @Cacheable({ ttl: 300, swr: 60, tags: ['post:{param.slug}'] })
|
|
70
|
+
* async show(ctx: RouterContext) { ... }
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* Module defaults fill in whatever the route leaves out:
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* ResponseCacheModule.forRoot({ defaults: { ttl: 300, swr: 60 } })
|
|
78
|
+
*
|
|
79
|
+
* @Get('/pricing')
|
|
80
|
+
* @Cacheable() // ttl 300, swr 60
|
|
81
|
+
* async pricing(ctx: RouterContext) { ... }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* **A guarded route needs a `partitionBy`.** A guarded route's response differs
|
|
85
|
+
* per caller, so caching it under one shared entry would serve one user's
|
|
86
|
+
* response to another. `@Cacheable` on a route carrying `@UseGuards` is a boot
|
|
87
|
+
* error unless it declares a non-empty `partitionBy` (or inherits one from
|
|
88
|
+
* `defaults.partitionBy`) — and per-caller keying additionally requires
|
|
89
|
+
* `ResponseCacheModule.forRoot({ gateway: { entrypoint } })`, without which
|
|
90
|
+
* `partitionBy` itself is rejected at boot.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* @Get('/dashboard')
|
|
95
|
+
* @UseGuards(AuthGuard)
|
|
96
|
+
* @Cacheable({ ttl: 60, partitionBy: ['user'] })
|
|
97
|
+
* async dashboard(ctx: RouterContext) { ... }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
function Cacheable(options = {}) {
|
|
101
|
+
return (target, propertyKey) => {
|
|
102
|
+
defineMetadata(KEY$1, options, target, propertyKey);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Read the `@Cacheable` options for a method. Returns `undefined` when absent. */
|
|
106
|
+
function getCacheable(target, methodName) {
|
|
107
|
+
return getMetadata(KEY$1, target, methodName);
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/response-cache/decorators/purges-cache.decorator.ts
|
|
111
|
+
const KEY = ROUTE_METADATA_KEYS.PURGES_CACHE;
|
|
112
|
+
/**
|
|
113
|
+
* Invalidate cached responses after this route mutates data.
|
|
114
|
+
*
|
|
115
|
+
* Runs after the handler returns a 2xx or 3xx. `pathPrefixes` matches the
|
|
116
|
+
* request path only, so `['/blog']` also clears `/blog?page=2`.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* @Post('/posts/:slug/publish')
|
|
121
|
+
* @PurgesCache({ tags: ['post:{param.slug}', 'category:{data.post.categoryId}'] })
|
|
122
|
+
* async publish(ctx: RouterContext) { ... }
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
function PurgesCache(options) {
|
|
126
|
+
const hasTargets = (options.tags?.length ?? 0) > 0 || (options.pathPrefixes?.length ?? 0) > 0;
|
|
127
|
+
if (options.purgeEverything && hasTargets) throw new ResponseCacheConfigError("@PurgesCache: `purgeEverything` is exclusive — remove `tags` and `pathPrefixes`.");
|
|
128
|
+
if (!options.purgeEverything && !hasTargets) throw new ResponseCacheConfigError("@PurgesCache: specify at least one of `tags`, `pathPrefixes`, or `purgeEverything`.");
|
|
129
|
+
return (target, propertyKey) => {
|
|
130
|
+
defineMetadata(KEY, options, target, propertyKey);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Read the `@PurgesCache` options for a method. Returns `undefined` when absent. */
|
|
134
|
+
function getPurgesCache(target, methodName) {
|
|
135
|
+
return getMetadata(KEY, target, methodName);
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/response-cache/boot-check.ts
|
|
139
|
+
/**
|
|
140
|
+
* Why this entrypoint cannot cache, or `undefined` when it can.
|
|
141
|
+
*
|
|
142
|
+
* A Worker cannot read its own Wrangler configuration, but the presence of
|
|
143
|
+
* `ctx.cache` tells us whether Workers Caching is enabled for this entrypoint.
|
|
144
|
+
*
|
|
145
|
+
* Reported rather than thrown, and the distinction is the whole point. The
|
|
146
|
+
* hazard is a route stamping `public, max-age=…` while nothing stores the
|
|
147
|
+
* response: a claim the app makes and no layer honours. Refusing to make that
|
|
148
|
+
* claim removes the hazard — the response falls through to
|
|
149
|
+
* `createNoStoreFallbackMiddleware` and is stamped `private, no-store`, which
|
|
150
|
+
* is both true and visible in the response itself. Failing the request
|
|
151
|
+
* removes it too, but takes down every route in the isolate to do it,
|
|
152
|
+
* including the ones that never asked to be cached.
|
|
153
|
+
*
|
|
154
|
+
* The answer belongs to an entrypoint rather than to the app, because a
|
|
155
|
+
* gateway topology runs two of them — one with `cache.enabled` false by
|
|
156
|
+
* design, one with it true — inside a single isolate.
|
|
157
|
+
*/
|
|
158
|
+
function cachingUnavailableReason(cacheableRouteCount, cache) {
|
|
159
|
+
if (cacheableRouteCount === 0) return void 0;
|
|
160
|
+
if (cache) return void 0;
|
|
161
|
+
return `${cacheableRouteCount} route(s) declare @Cacheable, but Workers Caching is not available on this entrypoint, so their responses are served uncached and stamped \`private, no-store\`. Set \`"cache": { "enabled": true }\` in your Wrangler config, use Wrangler >= 4.69.0, and set \`compatibility_date\` to 2026-07-06 or later.`;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The one export name a gateway can never forward to: its own.
|
|
165
|
+
*
|
|
166
|
+
* `ctx.exports.default` is the default export — the `Stratal` instance whose
|
|
167
|
+
* `fetch` marks the context as the gateway and dispatches. Forwarding there
|
|
168
|
+
* re-enters the gateway, which marks the *new* context and forwards again,
|
|
169
|
+
* recursing until the runtime's subrequest limit kills the request. Rejecting
|
|
170
|
+
* it at boot is what makes "the cached entrypoint never re-dispatches, so no
|
|
171
|
+
* loop is possible" true rather than merely true-if-configured-correctly.
|
|
172
|
+
*/
|
|
173
|
+
const SELF_ENTRYPOINT = "default";
|
|
174
|
+
/**
|
|
175
|
+
* Validate the configured entrypoint name itself.
|
|
176
|
+
*
|
|
177
|
+
* Split from the reachability check in `resolveCachedEntrypoint` because this
|
|
178
|
+
* part needs no runtime: a name that is empty, or names the gateway itself, is
|
|
179
|
+
* wrong on inspection and should never reach a first request.
|
|
180
|
+
*/
|
|
181
|
+
function assertValidGatewayEntrypoint(options) {
|
|
182
|
+
const gateway = options.gateway;
|
|
183
|
+
if (!gateway) return;
|
|
184
|
+
const entrypoint = gateway.entrypoint;
|
|
185
|
+
if (typeof entrypoint !== "string" || entrypoint.trim() === "") throw new ResponseCacheConfigError("ResponseCacheModule: `gateway.entrypoint` must be a non-empty export name, e.g. `gateway: { entrypoint: 'Cached' }` matching `export const Cached = cachedEntrypoint(stratal)`.");
|
|
186
|
+
if (entrypoint === SELF_ENTRYPOINT) throw new ResponseCacheConfigError("ResponseCacheModule: `gateway.entrypoint` cannot be \"default\" — that is the gateway itself. Forwarding to it would re-enter the gateway, which would forward again, recursing until the Worker hits its subrequest limit. Export a separate cached entrypoint (`export const Cached = cachedEntrypoint(stratal)`) and name that instead, with `cache: { enabled: false }` on `default` and `true` on the named export.");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Keep partition names out of the `$`-prefixed space the framework reserves.
|
|
190
|
+
*
|
|
191
|
+
* Partitions and the representation both become `ctx.props` entries, and props
|
|
192
|
+
* is a flat object — a partition named `$representation` would overwrite the
|
|
193
|
+
* representation or be overwritten by it, either way silently collapsing two
|
|
194
|
+
* cache keys into one. The prefix is rejected wholesale rather than just the
|
|
195
|
+
* one name in use today, so a later reserved key cannot break an app that had
|
|
196
|
+
* already shipped a partition called it.
|
|
197
|
+
*/
|
|
198
|
+
function assertPartitionNames(options) {
|
|
199
|
+
const reserved = Object.keys(options.partitions ?? {}).filter((name) => name.startsWith("$"));
|
|
200
|
+
if (reserved.length > 0) throw new ResponseCacheConfigError(`ResponseCacheModule: partition name(s) ${reserved.join(", ")} start with "$", which is reserved for values the framework puts in \`ctx.props\` itself. Rename them.`);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Reject the two module options only the gateway entrypoint can act on, when
|
|
204
|
+
* no gateway entrypoint is configured.
|
|
205
|
+
*
|
|
206
|
+
* `partitions` is looked up by `partitionBy`, which `bindRouteCache` rejects
|
|
207
|
+
* under the same condition; `primers` is read only by `GatewayPrimerService`,
|
|
208
|
+
* which the dispatch middleware is the sole caller of. Without
|
|
209
|
+
* `gateway.entrypoint` neither is ever consulted, and accepting them silently
|
|
210
|
+
* is the "option a consumer sets that does nothing" failure this feature is
|
|
211
|
+
* otherwise careful to avoid — an author would reasonably read a configured
|
|
212
|
+
* `partitions` block as proof that per-user keying is in effect. Fail at boot
|
|
213
|
+
* instead, in the same breath as `partitionBy`.
|
|
214
|
+
*
|
|
215
|
+
* Validates the entrypoint name first, so `gateway: { entrypoint: '' }` is
|
|
216
|
+
* reported as the malformed name it is rather than falling through to a
|
|
217
|
+
* "you need a gateway" message about the wrong thing.
|
|
218
|
+
*/
|
|
219
|
+
function assertNoGatewayOptions(options) {
|
|
220
|
+
assertValidGatewayEntrypoint(options);
|
|
221
|
+
assertPartitionNames(options);
|
|
222
|
+
if (options.gateway !== void 0) return;
|
|
223
|
+
if (Object.keys(options.partitions ?? {}).length > 0) throw new ResponseCacheConfigError("ResponseCacheModule: `partitions` requires `gateway: { entrypoint }`. Partition resolvers only run in the gateway, which forwards partitioned reads to a cached entrypoint over `ctx.exports` — without one, the resolvers configured here would never be called and every `partitionBy` route would cache publicly. Add `gateway: { entrypoint: 'Cached' }` and `export const Cached = cachedEntrypoint(stratal)`, or remove `partitions` (and any `partitionBy`).");
|
|
224
|
+
if ((options.primers?.length ?? 0) > 0) throw new ResponseCacheConfigError("ResponseCacheModule: `primers` requires `gateway: { entrypoint }`. Primers exist to populate the request container before partition resolution runs in the gateway — without a cached entrypoint nothing would ever run them, including `AUTH_GATEWAY_PRIMERS`. Add `gateway: { entrypoint }`, or remove `primers`.");
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/response-cache/resolve-cacheable.ts
|
|
228
|
+
/**
|
|
229
|
+
* Fold module defaults into a route's `@Cacheable` options.
|
|
230
|
+
*
|
|
231
|
+
* `partitionBy` replaces rather than merges: a route must be able to declare
|
|
232
|
+
* itself public (`[]`) or re-partition without inheriting a stale default.
|
|
233
|
+
* Reading the route alone must be enough to know how it is keyed.
|
|
234
|
+
*/
|
|
235
|
+
function resolveCacheable(route, defaults, context) {
|
|
236
|
+
const where = `${context.controller}.${context.method}`;
|
|
237
|
+
const ttl = route.ttl ?? defaults.ttl;
|
|
238
|
+
if (ttl === void 0) throw new ResponseCacheConfigError(`${where}: @Cacheable needs a \`ttl\`, either on the route or via ResponseCacheModule.forRoot({ defaults: { ttl } }).`);
|
|
239
|
+
if (ttl <= 0 || !Number.isFinite(ttl)) throw new ResponseCacheConfigError(`${where}: @Cacheable \`ttl\` must be a positive number of seconds.`);
|
|
240
|
+
const browserTtl = route.browserTtl ?? defaults.browserTtl ?? ttl;
|
|
241
|
+
if (browserTtl < 0 || !Number.isFinite(browserTtl)) throw new ResponseCacheConfigError(`${where}: @Cacheable \`browserTtl\` must be a non-negative finite number of seconds.`);
|
|
242
|
+
const swr = route.swr ?? defaults.swr;
|
|
243
|
+
if (swr !== void 0 && (swr < 0 || !Number.isFinite(swr))) throw new ResponseCacheConfigError(`${where}: @Cacheable \`swr\` must be a non-negative finite number of seconds.`);
|
|
244
|
+
const partitionBy = route.partitionBy ?? defaults.partitionBy ?? [];
|
|
245
|
+
if (context.guarded && partitionBy.length === 0) throw new ResponseCacheConfigError(context.gatewayConfigured ? `${where}: @Cacheable on a guarded route needs a non-empty \`partitionBy\`. A guarded route's response differs per caller, so caching it under one shared entry would serve one user's response to another. Declare the partitions it varies by (\`@Cacheable({ partitionBy: ['user'] })\`), or set a module-level \`defaults.partitionBy\`. \`partitionBy: []\` is not accepted here even explicitly.` : `${where}: @Cacheable cannot be used on a guarded route without a gateway entrypoint. A guarded route's response differs per caller, so caching it would serve one user's response to another. Per-caller cache keying needs \`ResponseCacheModule.forRoot({ gateway: { entrypoint } })\` plus a \`partitionBy\` — configure both, or remove @Cacheable from this route.`);
|
|
246
|
+
return {
|
|
247
|
+
ttl,
|
|
248
|
+
browserTtl,
|
|
249
|
+
swr,
|
|
250
|
+
tags: route.tags ?? [],
|
|
251
|
+
partitionBy,
|
|
252
|
+
vary: [.../* @__PURE__ */ new Set([...defaults.vary ?? [], ...route.vary ?? []])]
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/response-cache/services/route-cache-binding.ts
|
|
257
|
+
/** Matches a `{body.*}` cache tag placeholder (any path under the `body` scope). */
|
|
258
|
+
const BODY_SCOPE_TAG = /\{body\.[^}]+\}/;
|
|
259
|
+
/**
|
|
260
|
+
* Reject any tag that reads the `{body.*}` scope: the HTTP router never
|
|
261
|
+
* populates `TagScopes.body` (see `RouteRegistrationService.buildTagScopes`)
|
|
262
|
+
* — the parsed request body isn't cheaply available at the point tags are
|
|
263
|
+
* rendered, and re-parsing it on every request just to support a rarely used
|
|
264
|
+
* tag scope was deliberately rejected. A `{body.*}` tag can therefore never
|
|
265
|
+
* resolve.
|
|
266
|
+
*
|
|
267
|
+
* Left unchecked, this doesn't fail quietly: on a `@PurgesCache` route,
|
|
268
|
+
* `buildPurgeSpec(...)` is evaluated as an *argument* to `purge(...)`, so
|
|
269
|
+
* `renderTags` throwing `InvalidCacheTagError` happens before `purge`'s own
|
|
270
|
+
* try/catch exists — and `applyCacheDecision` has no catch of its own either,
|
|
271
|
+
* so the request 500s *after* its mutation already committed. Catching it
|
|
272
|
+
* here, at registration, turns a request-time failure the author only
|
|
273
|
+
* discovers in production into a boot-time error they see before deploying.
|
|
274
|
+
*/
|
|
275
|
+
function assertNoBodyScopeTags(tags, decorator, context) {
|
|
276
|
+
const offending = tags?.find((tag) => BODY_SCOPE_TAG.test(tag));
|
|
277
|
+
if (!offending) return;
|
|
278
|
+
throw new ResponseCacheConfigError(`${context.controller}.${context.method}: ${decorator} tag "${offending}" uses the \`{body.*}\` scope, which can never resolve — the parsed request body is not available when cache tags are rendered. Use \`{param.*}\`, \`{query.*}\`, or \`{data.*}\` instead.`);
|
|
279
|
+
}
|
|
280
|
+
/** Matches a `{param.X}` cache tag placeholder, capturing the param name `X`. */
|
|
281
|
+
const PARAM_SCOPE_TAG = /\{param\.([^}.]+)/g;
|
|
282
|
+
/**
|
|
283
|
+
* Reject a `{param.X}` tag whose `X` is not one of this route's own path
|
|
284
|
+
* parameters.
|
|
285
|
+
*
|
|
286
|
+
* Unlike `{query.*}`/`{data.*}` — which depend on what a given *request*
|
|
287
|
+
* happens to send, and so can only fail per-request — a route's path pattern
|
|
288
|
+
* is fixed the moment it is registered. Whether `{param.id}` can ever resolve
|
|
289
|
+
* is therefore knowable right now: if the route's path has no `:id` segment,
|
|
290
|
+
* it never will, on any request. Left unchecked, this fails the exact same
|
|
291
|
+
* way as `{body.*}` — `renderTags` throws mid-`purge(...)` call, after the
|
|
292
|
+
* mutation already committed. Catching it here turns that into a boot-time
|
|
293
|
+
* error instead.
|
|
294
|
+
*
|
|
295
|
+
* No-ops when `context.routeParams` is `undefined` — the caller didn't supply
|
|
296
|
+
* the route's path, so there is nothing to validate against.
|
|
297
|
+
*/
|
|
298
|
+
function assertParamTagsResolvable(tags, decorator, context) {
|
|
299
|
+
if (!tags?.length || context.routeParams === void 0) return;
|
|
300
|
+
const routeParams = context.routeParams;
|
|
301
|
+
for (const tag of tags) for (const match of tag.matchAll(PARAM_SCOPE_TAG)) {
|
|
302
|
+
const param = match[1];
|
|
303
|
+
if (routeParams.includes(param)) continue;
|
|
304
|
+
const available = routeParams.length > 0 ? routeParams.join(", ") : "(none)";
|
|
305
|
+
throw new ResponseCacheConfigError(`${context.controller}.${context.method}: ${decorator} tag "${tag}" references \`{param.${param}}\`, but this route's path has no ":${param}" segment, so it can never resolve — available params: ${available}. Use one of those, or a \`{query.*}\`/\`{data.*}\` tag instead.`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** Matches a `{partition.X}` cache tag placeholder, capturing the partition name `X`. */
|
|
309
|
+
const PARTITION_SCOPE_TAG = /\{partition\.([^}.]+)/g;
|
|
310
|
+
/**
|
|
311
|
+
* Reject a `{partition.X}` tag whose `X` is not one of this route's own
|
|
312
|
+
* declared partitions.
|
|
313
|
+
*
|
|
314
|
+
* Knowable at boot for the same reason `{param.X}` is: `partitionBy` is fixed
|
|
315
|
+
* when the route registers. A partition the route does not declare is not in
|
|
316
|
+
* the cache key, so a tag naming it would describe an entry by something that
|
|
317
|
+
* did not key it — and on `@PurgesCache` it throws mid-purge, after the
|
|
318
|
+
* mutation committed.
|
|
319
|
+
*
|
|
320
|
+
* Only checked on `@Cacheable`, which is where `partitionBy` is declared. A
|
|
321
|
+
* `@PurgesCache` route runs inline in the gateway, which resolves no
|
|
322
|
+
* partitions of its own, so `{partition.*}` can never render there.
|
|
323
|
+
*/
|
|
324
|
+
function assertPartitionTagsResolvable(tags, partitionBy, decorator, context) {
|
|
325
|
+
if (!tags?.length) return;
|
|
326
|
+
for (const tag of tags) for (const match of tag.matchAll(PARTITION_SCOPE_TAG)) {
|
|
327
|
+
const partition = match[1];
|
|
328
|
+
if (decorator === "@Cacheable" && partitionBy.includes(partition)) continue;
|
|
329
|
+
const available = partitionBy.length > 0 ? partitionBy.join(", ") : "(none)";
|
|
330
|
+
throw new ResponseCacheConfigError(decorator === "@PurgesCache" ? `${context.controller}.${context.method}: @PurgesCache tag "${tag}" references \`{partition.${partition}}\`, but a mutation runs inline in the gateway, which resolves no partitions — so it can never resolve. Purge by a \`{param.*}\`/\`{query.*}\`/\`{data.*}\` tag the cached route also carries.` : `${context.controller}.${context.method}: @Cacheable tag "${tag}" references \`{partition.${partition}}\`, but this route does not declare that partition, so it is not in the cache key and can never resolve — declared: ${available}. Add it to \`partitionBy\`, or use one of those.`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Reject a `pathPrefixes` entry that looks like it expects interpolation.
|
|
335
|
+
*
|
|
336
|
+
* `ResponseCacheService.buildPurgeSpec` copies `pathPrefixes` through
|
|
337
|
+
* verbatim — only `tags` go through `renderTags`. So
|
|
338
|
+
* `pathPrefixes: ['/blog/{param.slug}']` is sent to `ctx.cache.purge()` as
|
|
339
|
+
* that literal string, which matches no request path: the purge "succeeds",
|
|
340
|
+
* nothing is invalidated, and the author is never told. Same class of silent
|
|
341
|
+
* miss as a `{body.*}` tag, so it gets the same boot-time treatment.
|
|
342
|
+
*/
|
|
343
|
+
function assertNoTemplatedPathPrefixes(pathPrefixes, context) {
|
|
344
|
+
const offending = pathPrefixes?.find((prefix) => prefix.includes("{") || prefix.includes("}"));
|
|
345
|
+
if (offending === void 0) return;
|
|
346
|
+
throw new ResponseCacheConfigError(`${context.controller}.${context.method}: @PurgesCache \`pathPrefixes\` entry "${offending}" contains \`{\`/\`}\`, but path prefixes are not interpolated — only \`tags\` are. It would be purged as that literal string and match nothing. Use a static prefix (\`/blog\`), or move the dynamic part into a \`{param.*}\`/\`{query.*}\`/\`{data.*}\` cache tag.`);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Fold a route's decorators and the module defaults into the config the request
|
|
350
|
+
* path uses. Runs once per route at registration, never per request.
|
|
351
|
+
*
|
|
352
|
+
* Returns `undefined` when the route declares neither decorator, so the hot path
|
|
353
|
+
* can skip all cache work with a single truthiness check.
|
|
354
|
+
*
|
|
355
|
+
* @throws {ResponseCacheConfigError} On a non-empty `partitionBy` when no
|
|
356
|
+
* `gateway.entrypoint` is configured. Placing a partition in the cache key
|
|
357
|
+
* requires the gateway to forward the read to a cached entrypoint over
|
|
358
|
+
* `ctx.exports`. Accepting the option and ignoring it would give every
|
|
359
|
+
* visitor a single shared entry for a route the author explicitly marked
|
|
360
|
+
* per-user.
|
|
361
|
+
* @throws {ResponseCacheConfigError} On a `{body.*}` tag in either decorator's
|
|
362
|
+
* `tags` — see `assertNoBodyScopeTags`.
|
|
363
|
+
* @throws {ResponseCacheConfigError} On a `{param.X}` tag whose `X` is not a
|
|
364
|
+
* `:param` in this route's own path (when `context.routeParams` is
|
|
365
|
+
* supplied) — see `assertParamTagsResolvable`.
|
|
366
|
+
* @throws {ResponseCacheConfigError} On a `{`/`}` in `@PurgesCache`'s
|
|
367
|
+
* `pathPrefixes` — see `assertNoTemplatedPathPrefixes`.
|
|
368
|
+
*/
|
|
369
|
+
function bindRouteCache(cacheable, purges, defaults, context) {
|
|
370
|
+
if (!cacheable && !purges) return void 0;
|
|
371
|
+
assertNoBodyScopeTags(cacheable?.tags, "@Cacheable", context);
|
|
372
|
+
assertNoBodyScopeTags(purges?.tags, "@PurgesCache", context);
|
|
373
|
+
assertParamTagsResolvable(cacheable?.tags, "@Cacheable", context);
|
|
374
|
+
assertParamTagsResolvable(purges?.tags, "@PurgesCache", context);
|
|
375
|
+
assertPartitionTagsResolvable(purges?.tags, [], "@PurgesCache", context);
|
|
376
|
+
assertNoTemplatedPathPrefixes(purges?.pathPrefixes, context);
|
|
377
|
+
const binding = {};
|
|
378
|
+
if (purges) binding.purges = purges;
|
|
379
|
+
if (cacheable) {
|
|
380
|
+
const declared = cacheable.partitionBy ?? defaults.partitionBy ?? [];
|
|
381
|
+
assertPartitionTagsResolvable(cacheable.tags, declared, "@Cacheable", context);
|
|
382
|
+
if (declared.length > 0 && !context.gatewayConfigured) throw new ResponseCacheConfigError(`${context.controller}.${context.method}: \`partitionBy\` requires a gateway entrypoint. Placing a partition in the cache key means forwarding the read to a cached entrypoint over \`ctx.exports\`, so the option cannot be honored without one — and a route marked per-caller that quietly shared a single cache entry would serve one visitor's response to everyone. Configure \`ResponseCacheModule.forRoot({ gateway: { entrypoint: 'Cached' }, partitions: { … } })\` and \`export const Cached = cachedEntrypoint(stratal)\`, or remove \`partitionBy\`.`);
|
|
383
|
+
binding.cacheable = resolveCacheable(cacheable, defaults, context);
|
|
384
|
+
}
|
|
385
|
+
return binding;
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
export { PurgesCache as a, getCacheable as c, resolveCachedEntrypoint as d, cachingUnavailableReason as i, RESPONSE_CACHE_TOKENS as l, assertNoGatewayOptions as n, getPurgesCache as o, assertValidGatewayEntrypoint as r, Cacheable as s, bindRouteCache as t, createLoopbackPurgeTarget as u };
|
|
389
|
+
|
|
390
|
+
//# sourceMappingURL=route-cache-binding-CrLh8tB6.mjs.map
|