@timber-js/app 0.2.0-alpha.192 → 0.2.0-alpha.194
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/agent-skill.md +2 -0
- package/dist/_chunks/{actions-BjbbNRFN.js → actions-CWYtq6ii.js} +3 -3
- package/dist/_chunks/actions-CWYtq6ii.js.map +1 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -1
- package/dist/_chunks/{build-output-helper-C3DrfzZR.js → build-output-helper-9dqCv1pZ.js} +3 -2
- package/dist/_chunks/build-output-helper-9dqCv1pZ.js.map +1 -0
- package/dist/_chunks/{cache-api-DllJ-Lyw.js → cache-api-CQeYzA5g.js} +6 -8
- package/dist/_chunks/cache-api-CQeYzA5g.js.map +1 -0
- package/dist/_chunks/chains-h7EO-u3n.js +412 -0
- package/dist/_chunks/chains-h7EO-u3n.js.map +1 -0
- package/dist/_chunks/{cli-check-DJZDc22E.js → cli-check-BVthpfLS.js} +43 -6
- package/dist/_chunks/cli-check-BVthpfLS.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-D4_AZVUb.js → cli-schema-sync-ZwM9u_ob.js} +6 -5
- package/dist/_chunks/{cli-schema-sync-D4_AZVUb.js.map → cli-schema-sync-ZwM9u_ob.js.map} +1 -1
- package/dist/_chunks/{cloudflare-CGP6BZKO.js → cloudflare-BKJC3SC_.js} +28 -22
- package/dist/_chunks/{cloudflare-CGP6BZKO.js.map → cloudflare-BKJC3SC_.js.map} +1 -1
- package/dist/_chunks/codec-registry-oOUxugz3.js.map +1 -1
- package/dist/_chunks/{convention-lint-CpteIpTm.js → convention-lint-DO10_pVl.js} +93 -7
- package/dist/_chunks/convention-lint-DO10_pVl.js.map +1 -0
- package/dist/_chunks/dist-DtrJyBbw.js +386 -0
- package/dist/_chunks/dist-DtrJyBbw.js.map +1 -0
- package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -1
- package/dist/_chunks/{file-cache-DmX7OqZP.js → file-cache-Dw6BJPG7.js} +5 -1
- package/dist/_chunks/{file-cache-DmX7OqZP.js.map → file-cache-Dw6BJPG7.js.map} +1 -1
- package/dist/_chunks/{format-CfwjgPz9.js → format-BKclgVbk.js} +3 -3
- package/dist/_chunks/{format-CfwjgPz9.js.map → format-BKclgVbk.js.map} +1 -1
- package/dist/_chunks/graph-cache-CP4GEmf9.js +304 -0
- package/dist/_chunks/graph-cache-CP4GEmf9.js.map +1 -0
- package/dist/_chunks/href-validation-CMc5JRls.js.map +1 -1
- package/dist/_chunks/live-graph-Bx4HodF1.js +150 -0
- package/dist/_chunks/live-graph-Bx4HodF1.js.map +1 -0
- package/dist/_chunks/{logger-CH7IcMmg.js → logger-pumCm3Il.js} +25 -7
- package/dist/_chunks/{logger-CH7IcMmg.js.map → logger-pumCm3Il.js.map} +1 -1
- package/dist/_chunks/{mdx-file-C005ay-P.js → mdx-file-CXyHGUpS.js} +3 -3
- package/dist/_chunks/mdx-file-CXyHGUpS.js.map +1 -0
- package/dist/_chunks/{plugin-context-rCinWLiE.js → plugin-context-8idfR3UZ.js} +2 -2
- package/dist/_chunks/plugin-context-8idfR3UZ.js.map +1 -0
- package/dist/_chunks/poison-rules-DoEhbqaY.js +110 -0
- package/dist/_chunks/poison-rules-DoEhbqaY.js.map +1 -0
- package/dist/_chunks/poison-scan-BAxfTT5L.js +101 -0
- package/dist/_chunks/poison-scan-BAxfTT5L.js.map +1 -0
- package/dist/_chunks/resolve-schema-CBR6Lm4i.js.map +1 -1
- package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -1
- package/dist/_chunks/{rsc-media-type-DRqE_lD_.js → rsc-media-type-DDc7duTD.js} +17 -2
- package/dist/_chunks/{rsc-media-type-DRqE_lD_.js.map → rsc-media-type-DDc7duTD.js.map} +1 -1
- package/dist/_chunks/scan-BoP17W3w.js +140 -0
- package/dist/_chunks/scan-BoP17W3w.js.map +1 -0
- package/dist/_chunks/{scanner-CAietmj4.js → scanner-BRIOmHE2.js} +43 -4
- package/dist/_chunks/scanner-BRIOmHE2.js.map +1 -0
- package/dist/_chunks/schema-bridge-C83xa9lT.js.map +1 -1
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
- package/dist/_chunks/{segment-keys-BawYuNFO.js → segment-keys-BhqoHiLc.js} +14 -2
- package/dist/_chunks/{segment-keys-BawYuNFO.js.map → segment-keys-BhqoHiLc.js.map} +1 -1
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -1
- package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -1
- package/dist/_chunks/use-query-states-I3JMng6J.js.map +1 -1
- package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -1
- package/dist/_chunks/{walkers-BsVLmD1S.js → walkers-uCu3WW6_.js} +9 -6
- package/dist/_chunks/{walkers-BsVLmD1S.js.map → walkers-uCu3WW6_.js.map} +1 -1
- package/dist/adapters/build-output-helper.d.ts +1 -1
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +2 -1
- package/dist/adapters/cloudflare-dev.js.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +7 -8
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro-presets.d.ts +37 -0
- package/dist/adapters/nitro-presets.d.ts.map +1 -0
- package/dist/adapters/nitro-preview.d.ts +32 -0
- package/dist/adapters/nitro-preview.d.ts.map +1 -0
- package/dist/adapters/nitro.d.ts +28 -51
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +224 -171
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/analyze/chains.d.ts +124 -0
- package/dist/analyze/chains.d.ts.map +1 -0
- package/dist/analyze/classify.d.ts +150 -0
- package/dist/analyze/classify.d.ts.map +1 -0
- package/dist/analyze/crawl-entry.d.ts +17 -0
- package/dist/analyze/crawl-entry.d.ts.map +1 -0
- package/dist/analyze/crawl-entry.js +313 -0
- package/dist/analyze/crawl-entry.js.map +1 -0
- package/dist/analyze/crawl.d.ts +134 -0
- package/dist/analyze/crawl.d.ts.map +1 -0
- package/dist/analyze/graph-cache.d.ts +104 -0
- package/dist/analyze/graph-cache.d.ts.map +1 -0
- package/dist/analyze/graph-command.d.ts +43 -0
- package/dist/analyze/graph-command.d.ts.map +1 -0
- package/dist/analyze/graph-command.js +472 -0
- package/dist/analyze/graph-command.js.map +1 -0
- package/dist/analyze/live-graph.d.ts +81 -0
- package/dist/analyze/live-graph.d.ts.map +1 -0
- package/dist/analyze/poison-rules.d.ts +72 -0
- package/dist/analyze/poison-rules.d.ts.map +1 -0
- package/dist/analyze/poison-scan.d.ts +48 -0
- package/dist/analyze/poison-scan.d.ts.map +1 -0
- package/dist/analyze/scan.d.ts +73 -0
- package/dist/analyze/scan.d.ts.map +1 -0
- package/dist/cache/cache-api.d.ts +2 -2
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +8 -8
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cache/redis-handler.d.ts +1 -1
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -1
- package/dist/cache/stores/cloudflare-kv.js.map +1 -1
- package/dist/cache/stores/memory.d.ts +1 -1
- package/dist/cache/stores/memory.d.ts.map +1 -1
- package/dist/cache/stores/memory.js.map +1 -1
- package/dist/cache/stores/redis.d.ts +1 -1
- package/dist/cache/stores/redis.d.ts.map +1 -1
- package/dist/cache/stores/redis.js.map +1 -1
- package/dist/cache/stores/vercel.d.ts +1 -1
- package/dist/cache/stores/vercel.d.ts.map +1 -1
- package/dist/cache/stores/vercel.js.map +1 -1
- package/dist/cache/tag-aware-handler.d.ts +2 -2
- package/dist/cache/tag-aware-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.d.ts +1 -1
- package/dist/cdn/cloudflare-purge.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.js.map +1 -1
- package/dist/cdn/fastly-purge.d.ts +1 -1
- package/dist/cdn/fastly-purge.d.ts.map +1 -1
- package/dist/cdn/fastly-purge.js.map +1 -1
- package/dist/cdn/index.d.ts +4 -4
- package/dist/cdn/index.d.ts.map +1 -1
- package/dist/cdn/workers-cache-purge.d.ts +5 -1
- package/dist/cdn/workers-cache-purge.d.ts.map +1 -1
- package/dist/cdn/workers-cache-purge.js.map +1 -1
- package/dist/cli-check.d.ts +10 -0
- package/dist/cli-check.d.ts.map +1 -1
- package/dist/cli.d.ts +12 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +30 -6
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts +2 -2
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts +2 -2
- package/dist/client/browser-entry/router-init.d.ts +2 -2
- package/dist/client/browser-entry/rsc-stream.d.ts +1 -1
- package/dist/client/form.d.ts +2 -2
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/history.d.ts +2 -2
- package/dist/client/index.d.ts +18 -18
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +19 -19
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +11 -15
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +3 -3
- package/dist/client/navigation-commit.d.ts +5 -5
- package/dist/client/navigation-root.d.ts +1 -1
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +3 -3
- package/dist/client/router-pipeline.d.ts +7 -7
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/router-types.d.ts +6 -6
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts +1 -1
- package/dist/client/rsc-fetch.d.ts +4 -4
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +2 -2
- package/dist/client/slot-provider.d.ts +1 -1
- package/dist/client/ssr-data.d.ts +2 -2
- package/dist/client/state.d.ts +4 -4
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/client/use-segment-params.d.ts +3 -3
- package/dist/codec.d.ts +2 -2
- package/dist/codec.js.map +1 -1
- package/dist/config-validation.d.ts +1 -1
- package/dist/cookies/define-cookie.d.ts +3 -3
- package/dist/cookies/index.d.ts +3 -3
- package/dist/cookies/index.js +3 -4
- package/dist/cookies/index.js.map +1 -1
- package/dist/cookies/json-cookie.d.ts +1 -1
- package/dist/dev-tools/404-page.d.ts +2 -2
- package/dist/dev-tools/error-page.d.ts +2 -2
- package/dist/dev-tools/index.d.ts +12 -12
- package/dist/dev-tools/instrumentation.d.ts +1 -1
- package/dist/dev-tools/logs.d.ts +1 -1
- package/dist/dev-tools/overlay.d.ts +2 -2
- package/dist/dev-tools/terminal.d.ts +1 -1
- package/dist/fonts/ast.d.ts +4 -4
- package/dist/fonts/bundle.d.ts +3 -3
- package/dist/fonts/css.d.ts +1 -1
- package/dist/fonts/dev-middleware.d.ts +1 -1
- package/dist/fonts/google.d.ts +1 -1
- package/dist/fonts/local.d.ts +1 -1
- package/dist/fonts/pipeline.d.ts +3 -3
- package/dist/fonts/transform.d.ts +2 -2
- package/dist/fonts/types.d.ts +1 -1
- package/dist/fonts/virtual-modules.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +785 -282
- package/dist/index.js.map +1 -1
- package/dist/params/codec-registry.d.ts +1 -1
- package/dist/params/define-schema.d.ts +2 -2
- package/dist/params/index.d.ts +4 -4
- package/dist/params/index.js.map +1 -1
- package/dist/params/resolve-schema.d.ts +1 -1
- package/dist/plugin-context.d.ts +9 -9
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts +2 -2
- package/dist/plugins/build-manifest.d.ts +2 -2
- package/dist/plugins/build-report.d.ts +5 -5
- package/dist/plugins/cache.d.ts +1 -1
- package/dist/plugins/callsite-jsx.d.ts +2 -2
- package/dist/plugins/chunks.d.ts +1 -1
- package/dist/plugins/chunks.d.ts.map +1 -1
- package/dist/plugins/cloudflare-wasm.d.ts +1 -1
- package/dist/plugins/content.d.ts +1 -1
- package/dist/plugins/content.d.ts.map +1 -1
- package/dist/plugins/dev-discovery.d.ts +123 -0
- package/dist/plugins/dev-discovery.d.ts.map +1 -0
- package/dist/plugins/dev-server.d.ts +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +1 -1
- package/dist/plugins/fonts.d.ts +3 -3
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/graph-endpoint.d.ts +112 -0
- package/dist/plugins/graph-endpoint.d.ts.map +1 -0
- package/dist/plugins/mdx.d.ts +1 -1
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/prebuilt-capture.d.ts +2 -2
- package/dist/plugins/prebuilt-options-analysis.d.ts +1 -1
- package/dist/plugins/prebuilt.d.ts +1 -1
- package/dist/plugins/prerender-sugar.d.ts +11 -14
- package/dist/plugins/prerender-sugar.d.ts.map +1 -1
- package/dist/plugins/request-dep/analysis.d.ts +1 -1
- package/dist/plugins/request-dep/index.d.ts +5 -5
- package/dist/plugins/request-dep/origin.d.ts +1 -1
- package/dist/plugins/routing.d.ts +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +5 -5
- package/dist/routing/codegen-shared.d.ts +1 -1
- package/dist/routing/codegen-write.d.ts +1 -1
- package/dist/routing/codegen.d.ts +1 -1
- package/dist/routing/collision-probe.d.ts +1 -1
- package/dist/routing/collision-spaces.d.ts +1 -1
- package/dist/routing/convention-lint.d.ts +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.d.ts +13 -13
- package/dist/routing/index.js +3 -3
- package/dist/routing/interception.d.ts +1 -1
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/link-codegen.d.ts +1 -1
- package/dist/routing/manifest-codegen.d.ts +1 -1
- package/dist/routing/scanner.d.ts +14 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/schema-validation.d.ts +1 -1
- package/dist/routing/segment-classify.d.ts +1 -1
- package/dist/routing/segment-codegen.d.ts +1 -1
- package/dist/routing/segment-keys.d.ts +1 -1
- package/dist/routing/slot-placement.d.ts +1 -1
- package/dist/routing/walkers.d.ts +1 -1
- package/dist/rsc-runtime/transforms.d.ts +13 -0
- package/dist/rsc-runtime/transforms.d.ts.map +1 -0
- package/dist/schema-bridge.d.ts +1 -1
- package/dist/search-params/define.d.ts +3 -3
- package/dist/search-params/index.d.ts +3 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +1 -1
- package/dist/server/access-gate.d.ts +1 -1
- package/dist/server/action-client.d.ts +1 -1
- package/dist/server/action-handler.d.ts +5 -5
- package/dist/server/actions.d.ts +2 -2
- package/dist/server/als-registry.d.ts +8 -8
- package/dist/server/asset-headers.d.ts +1 -1
- package/dist/server/chain-url-parts.d.ts +3 -3
- package/dist/server/children-interception.d.ts +2 -2
- package/dist/server/cookie-context.d.ts +1 -1
- package/dist/server/cookie-parsing.d.ts +2 -2
- package/dist/server/default-logger.d.ts +1 -1
- package/dist/server/deny-boundary.d.ts +2 -2
- package/dist/server/deny-renderer.d.ts +5 -5
- package/dist/server/early-hints.d.ts +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +2 -4
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts +5 -5
- package/dist/server/flight-injection-state.d.ts +1 -1
- package/dist/server/form-flash.d.ts +1 -1
- package/dist/server/html-injector-core.d.ts +2 -2
- package/dist/server/html-injectors.d.ts +1 -1
- package/dist/server/index.d.ts +18 -18
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +33 -32
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts +1 -1
- package/dist/server/internal.d.ts +50 -50
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +1023 -1025
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +2 -2
- package/dist/server/metadata-platform.d.ts +2 -2
- package/dist/server/metadata-render.d.ts +2 -2
- package/dist/server/metadata-routes.d.ts +3 -3
- package/dist/server/metadata-social.d.ts +2 -2
- package/dist/server/metadata.d.ts +2 -2
- package/dist/server/middleware-runner.d.ts +1 -1
- package/dist/server/node-stream-transforms.d.ts +1 -1
- package/dist/server/param-coercion.d.ts +4 -4
- package/dist/server/pipeline-helpers.d.ts +3 -3
- package/dist/server/pipeline-interception.d.ts +1 -1
- package/dist/server/pipeline-metadata.d.ts +1 -1
- package/dist/server/pipeline-outcome.d.ts +2 -2
- package/dist/server/pipeline-phases.d.ts +3 -3
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +9 -9
- package/dist/server/prebuilt/cache-key.d.ts +1 -1
- package/dist/server/prebuilt/capture-state.d.ts +1 -1
- package/dist/server/prebuilt/key-discipline.d.ts +1 -1
- package/dist/server/prebuilt/manifest-tag-index.d.ts +1 -1
- package/dist/server/prebuilt/overlay.d.ts +2 -2
- package/dist/server/prebuilt/payload-source.d.ts +1 -1
- package/dist/server/prebuilt/slots.d.ts +1 -1
- package/dist/server/prebuilt/synthetic-store.d.ts +2 -2
- package/dist/server/prebuilt-builder.d.ts +2 -2
- package/dist/server/primitives.d.ts +2 -2
- package/dist/server/publish-params.d.ts +1 -1
- package/dist/server/request-context.d.ts +3 -3
- package/dist/server/route-element-builder.d.ts +16 -6
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts +1 -1
- package/dist/server/route-matcher.d.ts +4 -4
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +2 -2
- package/dist/server/rsc-entry/api-handler.d.ts +1 -1
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts +3 -3
- package/dist/server/rsc-entry/error-renderer.d.ts +4 -4
- package/dist/server/rsc-entry/helpers.d.ts +6 -6
- package/dist/server/rsc-entry/index.d.ts +7 -6
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +4 -4
- package/dist/server/rsc-entry/revalidate-renderer.d.ts +4 -4
- package/dist/server/rsc-entry/rsc-payload.d.ts +4 -4
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +3 -3
- package/dist/server/rsc-entry/ssr-bridge.d.ts +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +7 -7
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +5 -5
- package/dist/server/sitemap-generator.d.ts +2 -2
- package/dist/server/sitemap-handler.d.ts +1 -1
- package/dist/server/skippable-prefix.d.ts +3 -3
- package/dist/server/slot-interception.d.ts +2 -2
- package/dist/server/slot-resolver.d.ts +3 -7
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +2 -2
- package/dist/server/ssr-entry.d.ts +1 -1
- package/dist/server/static-generator.d.ts +1 -1
- package/dist/server/static-not-found.d.ts +1 -1
- package/dist/server/status-code-resolver.d.ts +1 -1
- package/dist/server/tracing.d.ts +3 -3
- package/dist/server/tree-builder.d.ts +3 -3
- package/dist/server/tree-match.d.ts +1 -1
- package/dist/server/types.d.ts +2 -2
- package/dist/server/utils/mdx-file.d.ts +2 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -1
- package/dist/server/workers-cache-bridge.d.ts +1 -1
- package/dist/shared/als-slots.d.ts +1 -1
- package/dist/shared/payload-root.d.ts +2 -2
- package/dist/shared/rsc-media-type.d.ts +12 -0
- package/dist/shared/rsc-media-type.d.ts.map +1 -1
- package/dist/shared/slot-params.d.ts +1 -1
- package/dist/shims/font-google.d.ts +1 -1
- package/dist/shims/font-google.js.map +1 -1
- package/dist/shims/font-local.d.ts +1 -1
- package/dist/shims/font-local.js.map +1 -1
- package/dist/shims/navigation-client.d.ts +4 -4
- package/dist/shims/navigation.d.ts +2 -2
- package/docs/api/36-cli.mdx +23 -3
- package/docs/learn/11-error-handling.mdx +11 -0
- package/docs/more/01-advanced-routing.mdx +1 -0
- package/package.json +17 -16
- package/src/adapters/build-output-helper.ts +4 -4
- package/src/adapters/cloudflare-dev.ts +7 -1
- package/src/adapters/cloudflare-kv-cache.ts +11 -8
- package/src/adapters/cloudflare.ts +21 -15
- package/src/adapters/compress-module.ts +2 -2
- package/src/adapters/nitro-presets.ts +139 -0
- package/src/adapters/nitro-preview.ts +340 -0
- package/src/adapters/nitro.ts +100 -465
- package/src/adapters/shared.ts +1 -1
- package/src/analyze/chains.ts +411 -0
- package/src/analyze/classify.ts +258 -0
- package/src/analyze/crawl-entry.ts +68 -0
- package/src/analyze/crawl.ts +500 -0
- package/src/analyze/graph-cache.ts +332 -0
- package/src/analyze/graph-command.ts +762 -0
- package/src/analyze/live-graph.ts +232 -0
- package/src/analyze/poison-rules.ts +165 -0
- package/src/analyze/poison-scan.ts +162 -0
- package/src/analyze/scan.ts +149 -0
- package/src/cache/cache-api.ts +7 -7
- package/src/cache/index.ts +9 -9
- package/src/cache/redis-handler.ts +2 -2
- package/src/cache/stores/cloudflare-kv.ts +1 -1
- package/src/cache/stores/memory.ts +2 -2
- package/src/cache/stores/redis.ts +1 -1
- package/src/cache/stores/vercel.ts +1 -1
- package/src/cache/tag-aware-handler.ts +2 -2
- package/src/cache/timber-cache.ts +8 -8
- package/src/cdn/cloudflare-purge.ts +1 -1
- package/src/cdn/fastly-purge.ts +1 -1
- package/src/cdn/index.ts +10 -4
- package/src/cdn/workers-cache-purge.ts +6 -2
- package/src/cli-check.ts +53 -4
- package/src/cli-schema-sync.ts +3 -3
- package/src/cli.ts +50 -11
- package/src/client/browser-dev.ts +1 -1
- package/src/client/browser-entry/action-dispatch.ts +5 -5
- package/src/client/browser-entry/hmr.ts +1 -1
- package/src/client/browser-entry/hydrate.ts +9 -9
- package/src/client/browser-entry/index.ts +10 -10
- package/src/client/browser-entry/post-hydration.ts +5 -5
- package/src/client/browser-entry/router-init.ts +9 -9
- package/src/client/browser-entry/rsc-stream.ts +3 -3
- package/src/client/error-boundary.tsx +4 -4
- package/src/client/form.tsx +7 -2
- package/src/client/history.ts +2 -2
- package/src/client/index.ts +21 -18
- package/src/client/internal.ts +19 -19
- package/src/client/link.tsx +9 -9
- package/src/client/navigation-api.ts +1 -1
- package/src/client/navigation-commit.ts +5 -5
- package/src/client/navigation-context.ts +1 -1
- package/src/client/navigation-root.tsx +1 -1
- package/src/client/nuqs-adapter.tsx +2 -2
- package/src/client/params-context.ts +4 -4
- package/src/client/router-effects.ts +3 -3
- package/src/client/router-lifecycle.ts +2 -2
- package/src/client/router-pipeline.ts +10 -10
- package/src/client/router-ref.ts +2 -2
- package/src/client/router-skew.ts +2 -2
- package/src/client/router-types.ts +6 -6
- package/src/client/router.ts +13 -13
- package/src/client/rsc-fetch.ts +11 -16
- package/src/client/segment-cache.ts +2 -2
- package/src/client/segment-outlet.tsx +1 -1
- package/src/client/shallow-url.ts +1 -1
- package/src/client/slot-outlet.tsx +1 -1
- package/src/client/slot-provider.tsx +1 -1
- package/src/client/ssr-data.ts +3 -3
- package/src/client/stale-client.ts +1 -1
- package/src/client/state.ts +4 -4
- package/src/client/top-loader.tsx +1 -1
- package/src/client/unload-guard.ts +1 -1
- package/src/client/use-cookie.ts +2 -2
- package/src/client/use-pathname.ts +2 -2
- package/src/client/use-pending-navigation.ts +1 -1
- package/src/client/use-query-states.ts +3 -3
- package/src/client/use-router.ts +2 -2
- package/src/client/use-search-params.ts +3 -3
- package/src/client/use-segment-params.ts +6 -6
- package/src/client/use-selected-layout-segment.ts +2 -2
- package/src/codec.ts +2 -2
- package/src/config-validation.ts +1 -1
- package/src/cookies/define-cookie.ts +6 -6
- package/src/cookies/index.ts +3 -3
- package/src/cookies/json-cookie.ts +1 -1
- package/src/dev-tools/404-page.ts +4 -4
- package/src/dev-tools/dev-page-shell.ts +1 -1
- package/src/dev-tools/error-page.ts +4 -4
- package/src/dev-tools/index.ts +12 -12
- package/src/dev-tools/instrumentation.ts +1 -1
- package/src/dev-tools/logs.ts +1 -1
- package/src/dev-tools/overlay.ts +3 -3
- package/src/dev-tools/terminal.ts +2 -2
- package/src/fonts/ast.ts +5 -5
- package/src/fonts/bundle.ts +5 -5
- package/src/fonts/css.ts +1 -1
- package/src/fonts/dev-middleware.ts +2 -2
- package/src/fonts/google.ts +2 -2
- package/src/fonts/local.ts +4 -4
- package/src/fonts/pipeline.ts +8 -8
- package/src/fonts/transform.ts +5 -5
- package/src/fonts/types.ts +1 -1
- package/src/fonts/virtual-modules.ts +1 -1
- package/src/index.ts +46 -37
- package/src/params/codec-registry.ts +1 -1
- package/src/params/define-schema.ts +2 -2
- package/src/params/index.ts +4 -4
- package/src/params/resolve-schema.ts +2 -2
- package/src/plugin-context.ts +10 -10
- package/src/plugins/adapter-build.ts +5 -5
- package/src/plugins/build-manifest.ts +4 -4
- package/src/plugins/build-report.ts +6 -6
- package/src/plugins/cache.ts +3 -3
- package/src/plugins/callsite-ast.ts +1 -1
- package/src/plugins/callsite-jsx.ts +2 -2
- package/src/plugins/chunks.ts +1 -1
- package/src/plugins/cloudflare-wasm.ts +1 -1
- package/src/plugins/content.ts +5 -1
- package/src/plugins/dev-discovery.ts +218 -0
- package/src/plugins/dev-server.ts +97 -12
- package/src/plugins/entries.ts +1 -1
- package/src/plugins/fonts.ts +17 -10
- package/src/plugins/graph-endpoint.ts +532 -0
- package/src/plugins/mdx.ts +4 -3
- package/src/plugins/prebuilt-capture.ts +3 -3
- package/src/plugins/prebuilt-options-analysis.ts +1 -1
- package/src/plugins/prebuilt.ts +7 -7
- package/src/plugins/prerender-sugar.ts +11 -19
- package/src/plugins/request-dep/analysis.ts +1 -1
- package/src/plugins/request-dep/index.ts +8 -8
- package/src/plugins/request-dep/origin.ts +1 -1
- package/src/plugins/routing.ts +12 -7
- package/src/plugins/server-bundle.ts +9 -2
- package/src/plugins/shims.ts +16 -31
- package/src/plugins/static-build.ts +10 -10
- package/src/routing/codegen-shared.ts +1 -1
- package/src/routing/codegen-write.ts +3 -3
- package/src/routing/codegen.ts +14 -5
- package/src/routing/collision-probe.ts +3 -3
- package/src/routing/collision-spaces.ts +3 -3
- package/src/routing/convention-lint.ts +136 -6
- package/src/routing/export-detect.ts +1 -1
- package/src/routing/index.ts +13 -13
- package/src/routing/interception.ts +41 -4
- package/src/routing/link-codegen.ts +3 -3
- package/src/routing/manifest-codegen.ts +2 -2
- package/src/routing/scanner.ts +33 -9
- package/src/routing/schema-validation.ts +4 -4
- package/src/routing/segment-classify.ts +2 -2
- package/src/routing/segment-codegen.ts +1 -1
- package/src/routing/segment-keys.ts +1 -1
- package/src/routing/slot-placement.ts +1 -1
- package/src/routing/walkers.ts +1 -1
- package/src/rsc-runtime/transforms.ts +13 -0
- package/src/rsc-runtime/vendor-types.d.ts +12 -0
- package/src/schema-bridge.ts +1 -1
- package/src/search-params/define.ts +8 -8
- package/src/search-params/index.ts +3 -3
- package/src/search-params/wrappers.ts +3 -3
- package/src/server/access-gate.tsx +6 -6
- package/src/server/action-client.ts +5 -5
- package/src/server/action-handler.ts +16 -16
- package/src/server/actions.ts +5 -5
- package/src/server/als-registry.ts +8 -8
- package/src/server/asset-headers.ts +2 -2
- package/src/server/chain-url-parts.ts +5 -5
- package/src/server/children-interception.ts +4 -4
- package/src/server/compress.ts +1 -1
- package/src/server/cookie-context.ts +4 -4
- package/src/server/cookie-parsing.ts +2 -2
- package/src/server/default-logger.ts +3 -3
- package/src/server/deny-boundary.ts +10 -10
- package/src/server/deny-renderer.ts +38 -25
- package/src/server/early-hints-sender.ts +2 -2
- package/src/server/early-hints.ts +2 -2
- package/src/server/error-boundary-wrapper.ts +4 -12
- package/src/server/fallback-error.ts +13 -13
- package/src/server/flight-injection-state.ts +1 -1
- package/src/server/form-flash.ts +2 -2
- package/src/server/html-injector-core.ts +4 -4
- package/src/server/html-injectors.ts +5 -5
- package/src/server/index.ts +18 -18
- package/src/server/instrumentation.ts +1 -1
- package/src/server/internal.ts +50 -50
- package/src/server/logger.ts +4 -4
- package/src/server/metadata-collector.ts +7 -7
- package/src/server/metadata-platform.ts +2 -2
- package/src/server/metadata-render.ts +4 -4
- package/src/server/metadata-routes.ts +3 -3
- package/src/server/metadata-social.ts +2 -2
- package/src/server/metadata.ts +2 -2
- package/src/server/middleware-runner.ts +1 -1
- package/src/server/node-stream-transforms.ts +4 -4
- package/src/server/param-coercion.ts +8 -8
- package/src/server/pipeline-helpers.ts +7 -7
- package/src/server/pipeline-interception.ts +2 -2
- package/src/server/pipeline-metadata.ts +1 -1
- package/src/server/pipeline-outcome.ts +8 -8
- package/src/server/pipeline-phases.ts +24 -28
- package/src/server/pipeline.ts +22 -22
- package/src/server/prebuilt/cache-key.ts +1 -1
- package/src/server/prebuilt/capture-state.ts +1 -1
- package/src/server/prebuilt/key-discipline.ts +1 -1
- package/src/server/prebuilt/manifest-tag-index.ts +1 -1
- package/src/server/prebuilt/overlay.ts +6 -6
- package/src/server/prebuilt/payload-source.ts +2 -2
- package/src/server/prebuilt/slots.ts +5 -5
- package/src/server/prebuilt/synthetic-store.ts +2 -2
- package/src/server/prebuilt-builder.ts +16 -16
- package/src/server/prebuilt-runtime.ts +12 -12
- package/src/server/primitives.ts +7 -7
- package/src/server/publish-params.ts +3 -3
- package/src/server/request-context.ts +7 -7
- package/src/server/route-element-builder.ts +108 -50
- package/src/server/route-handler.ts +4 -4
- package/src/server/route-matcher.ts +5 -5
- package/src/server/rsc-cache-key-guard.ts +3 -5
- package/src/server/rsc-entry/action-middleware-runner.ts +7 -7
- package/src/server/rsc-entry/api-handler.ts +18 -9
- package/src/server/rsc-entry/deny-fallback.ts +10 -10
- package/src/server/rsc-entry/error-renderer.ts +27 -27
- package/src/server/rsc-entry/helpers.ts +10 -10
- package/src/server/rsc-entry/index.ts +51 -47
- package/src/server/rsc-entry/render-route.ts +20 -20
- package/src/server/rsc-entry/revalidate-renderer.ts +12 -12
- package/src/server/rsc-entry/rsc-payload.ts +19 -15
- package/src/server/rsc-entry/rsc-stream.ts +8 -8
- package/src/server/rsc-entry/ssr-bridge.ts +3 -3
- package/src/server/rsc-entry/ssr-renderer.ts +23 -23
- package/src/server/rsc-entry/wrap-action-dispatch.ts +9 -9
- package/src/server/rsc-prop-warnings.ts +1 -1
- package/src/server/sensitive-fields.ts +1 -1
- package/src/server/server-timing.ts +1 -1
- package/src/server/sitemap-generator.ts +3 -3
- package/src/server/sitemap-handler.ts +3 -3
- package/src/server/skippable-prefix.ts +7 -7
- package/src/server/slot-interception.ts +5 -5
- package/src/server/slot-resolver.ts +20 -28
- package/src/server/ssr-bridge-types.ts +2 -2
- package/src/server/ssr-entry.ts +18 -18
- package/src/server/ssr-render.ts +4 -4
- package/src/server/ssr-wrappers.tsx +1 -1
- package/src/server/state-tree-diff.ts +1 -1
- package/src/server/static-generator.ts +6 -6
- package/src/server/static-not-found.ts +3 -3
- package/src/server/status-code-resolver.ts +1 -1
- package/src/server/tracing.ts +5 -5
- package/src/server/tree-builder.ts +4 -4
- package/src/server/tree-match.ts +1 -1
- package/src/server/types.ts +2 -2
- package/src/server/utils/mdx-file.ts +2 -2
- package/src/server/waituntil-bridge.ts +1 -1
- package/src/server/workers-cache-bridge.ts +2 -2
- package/src/shared/als-slots.ts +1 -1
- package/src/shared/payload-root.ts +2 -2
- package/src/shared/rsc-media-type.ts +16 -0
- package/src/shared/slot-params.ts +1 -1
- package/src/shims/font-google.ts +1 -1
- package/src/shims/font-local.ts +1 -1
- package/src/shims/navigation-client.ts +5 -5
- package/src/shims/navigation.ts +2 -2
- package/LICENSE +0 -8
- package/dist/_chunks/actions-BjbbNRFN.js.map +0 -1
- package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +0 -1
- package/dist/_chunks/cache-api-DllJ-Lyw.js.map +0 -1
- package/dist/_chunks/cli-check-DJZDc22E.js.map +0 -1
- package/dist/_chunks/convention-lint-CpteIpTm.js.map +0 -1
- package/dist/_chunks/dist-BA3u1z3W.js +0 -423
- package/dist/_chunks/dist-BA3u1z3W.js.map +0 -1
- package/dist/_chunks/mdx-file-C005ay-P.js.map +0 -1
- package/dist/_chunks/plugin-context-rCinWLiE.js.map +0 -1
- package/dist/_chunks/scanner-CAietmj4.js.map +0 -1
- package/dist/content/index.d.ts +0 -2
- package/dist/content/index.d.ts.map +0 -1
- package/dist/content/index.js +0 -0
- package/src/content/index.ts +0 -5
- package/src/shims/server-only-noop.js +0 -6
package/agent-skill.md
CHANGED
|
@@ -335,6 +335,8 @@ pnpm build # Production build
|
|
|
335
335
|
pnpm preview # Preview production build
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
+
Before editing a file, `npx timber graph <file> --json` answers which environment(s) it runs in (server / client-boundary / client-internal / shared), its import chains, and any server-only/client-only conflicts. First query per project state runs a crawl (seconds); subsequent queries hit a cache (milliseconds).
|
|
339
|
+
|
|
338
340
|
## Further reading
|
|
339
341
|
|
|
340
342
|
Full docs are in `node_modules/@timber-js/app/docs/`. Key files:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as revalidationAls } from "./als-registry-C6kcfprT.js";
|
|
2
|
-
import { J as withSpan, _ as RedirectSignal, g as DenySignal } from "./logger-
|
|
3
|
-
import { t as cache } from "./cache-api-
|
|
2
|
+
import { J as withSpan, _ as RedirectSignal, g as DenySignal } from "./logger-pumCm3Il.js";
|
|
3
|
+
import { t as cache } from "./cache-api-CQeYzA5g.js";
|
|
4
4
|
//#region src/server/form-data.ts
|
|
5
5
|
/**
|
|
6
6
|
* FormData preprocessing — schema-agnostic conversion of FormData to typed objects.
|
|
@@ -315,4 +315,4 @@ async function executeAction(actionFn, args, config = {}, spanMeta) {
|
|
|
315
315
|
//#endregion
|
|
316
316
|
export { parseFormData as a, coerce as i, revalidatePath as n, revalidateTag as r, executeAction as t };
|
|
317
317
|
|
|
318
|
-
//# sourceMappingURL=actions-
|
|
318
|
+
//# sourceMappingURL=actions-CWYtq6ii.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions-CWYtq6ii.js","names":[],"sources":["../../src/server/form-data.ts","../../src/server/actions.ts"],"sourcesContent":["/**\n * FormData preprocessing — schema-agnostic conversion of FormData to typed objects.\n *\n * FormData is all strings. Schema validation expects typed values. This module\n * bridges the gap with intelligent coercion that runs *before* schema validation.\n *\n * Inspired by zod-form-data, but schema-agnostic — works with any Standard Schema\n * library (Zod, Valibot, ArkType).\n *\n * See design/08-forms-and-actions.md §\"parseFormData() and coerce helpers\"\n */\n\n// ─── parseFormData ───────────────────────────────────────────────────────\n\n/**\n * Convert FormData into a plain object with intelligent coercion.\n *\n * Handles:\n * - **Duplicate keys → arrays**: `tags=js&tags=ts` → `{ tags: [\"js\", \"ts\"] }`\n * - **Nested dot-paths**: `user.name=Alice` → `{ user: { name: \"Alice\" } }`\n * - **Empty strings → undefined**: Enables `.optional()` semantics in schemas\n * - **Empty Files → undefined**: File inputs with no selection become `undefined`\n * - **Strips `$ACTION_*` fields**: React's internal hidden fields are excluded\n */\nexport function parseFormData(formData: FormData): Record<string, unknown> {\n const flat: Record<string, unknown> = {};\n\n for (const key of new Set(formData.keys())) {\n // Skip React internal fields\n if (key.startsWith('$ACTION_')) continue;\n\n // Skip prototype-pollution keys at the top level — assigning to\n // `flat['__proto__']` invokes the prototype setter and can splice\n // attacker-controlled data into `flat`'s prototype chain.\n // The dot-path case is filtered separately in `expandDotPaths`.\n if (DANGEROUS_KEYS.has(key)) continue;\n\n const values = formData.getAll(key);\n const processed = values.map(normalizeValue);\n\n if (processed.length === 1) {\n flat[key] = processed[0];\n } else {\n // Filter out undefined (empty Files) and empty strings from multi-value fields.\n // Multi-value fields (e.g. tags=js&tags=&tags=ts) should not include blanks.\n flat[key] = processed.filter((v) => v !== undefined && v !== '');\n }\n }\n\n // Expand dot-notation paths into nested objects\n return expandDotPaths(flat);\n}\n\n/**\n * Normalize a single FormData entry value.\n * - Empty File objects (no selection) → undefined\n * - Strings pass through as-is (empty strings stay as empty strings so\n * schema validators like `z.string().min(1, 'Required')` can produce\n * their custom error messages instead of a generic type mismatch)\n * - Everything else passes through as-is\n */\nfunction normalizeValue(value: FormDataEntryValue): unknown {\n // File input with no selection: browsers submit a File with name=\"\" and size=0\n if (value instanceof File && value.size === 0 && value.name === '') {\n return undefined;\n }\n\n return value;\n}\n\n/**\n * Expand dot-notation keys into nested objects.\n * `{ \"user.name\": \"Alice\", \"user.age\": \"30\" }` → `{ user: { name: \"Alice\", age: \"30\" } }`\n *\n * Keys without dots are left as-is. Bracket notation (e.g. `items[0]`) is NOT\n * supported — use dot notation (`items.0`) instead.\n */\nfunction expandDotPaths(flat: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n let hasDotPaths = false;\n\n // First pass: check if any keys have dots\n for (const key of Object.keys(flat)) {\n if (key.includes('.')) {\n hasDotPaths = true;\n break;\n }\n }\n\n // Fast path: no dot-notation keys, return as-is.\n // Top-level dangerous keys are already filtered in `parseFormData` above.\n if (!hasDotPaths) return flat;\n\n for (const [key, value] of Object.entries(flat)) {\n // Reject any path that contains __proto__ / constructor / prototype.\n // Without this, `__proto__.x=1` writes to Object.prototype because\n // `result['__proto__']` resolves to the prototype object itself.\n const parts = key.split('.');\n if (parts.some((p) => DANGEROUS_KEYS.has(p))) continue;\n\n if (parts.length === 1) {\n result[parts[0]] = value;\n continue;\n }\n\n let current: Record<string, unknown> = result;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (current[part] === undefined || current[part] === null) {\n current[part] = {};\n }\n // If current[part] is not an object (e.g., a string from a non-dotted key),\n // the dot-path takes precedence\n if (typeof current[part] !== 'object' || current[part] instanceof File) {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n\n current[parts[parts.length - 1]] = value;\n }\n\n return result;\n}\n\n// `__proto__` is the only key in this parser that escapes the dot-path\n// walk's `typeof !== 'object'` reset: accessing `obj['__proto__']` returns\n// `Object.prototype` (itself an object), so the walk steps into the global\n// prototype and the leaf write mutates it. `constructor` resolves to a\n// function (reset fires) and `prototype` is undefined on fresh objects\n// (reset fires), so neither escapes the walk — they parse as legit nested\n// keys and do not mutate anything global.\nconst DANGEROUS_KEYS = new Set(['__proto__']);\n\n// ─── Coercion Helpers ────────────────────────────────────────────────────\n\n/**\n * Schema-agnostic coercion primitives for common FormData patterns.\n *\n * These are plain transform functions — they compose with any schema library's\n * `transform`/`preprocess` pipeline:\n *\n * ```ts\n * // Zod\n * z.preprocess(coerce.number, z.number())\n * // Valibot\n * v.pipe(v.unknown(), v.transform(coerce.number), v.number())\n * ```\n */\nexport const coerce = {\n /**\n * Coerce an empty string to undefined for optional text fields.\n * Use with `.optional()` schemas where an empty input means \"not provided\".\n *\n * ```ts\n * // Zod\n * z.unknown().transform(coerce.text).pipe(z.string().optional())\n * // Valibot\n * v.pipe(v.unknown(), v.transform(coerce.text), v.optional(v.string()))\n * ```\n */\n text(value: unknown): string | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value === 'string') return value;\n return undefined;\n },\n\n /**\n * Coerce a string to a number.\n * - `\"42\"` → `42`\n * - `\"3.14\"` → `3.14`\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Non-numeric strings → `undefined` (schema validation will catch this)\n */\n number(value: unknown): number | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value === 'number') return value;\n if (typeof value !== 'string') return undefined;\n const num = Number(value);\n if (Number.isNaN(num)) return undefined;\n return num;\n },\n\n /**\n * Coerce a checkbox value to a boolean.\n * HTML checkboxes submit \"on\" when checked and are absent when unchecked.\n * - `\"on\"` / any truthy string → `true`\n * - `undefined` / `null` / `\"\"` → `false`\n */\n checkbox(value: unknown): boolean {\n if (value === undefined || value === null || value === '') return false;\n if (typeof value === 'boolean') return value;\n // Any non-empty string (typically \"on\") is true\n return typeof value === 'string' && value.length > 0;\n },\n\n /**\n * Parse a JSON string into an object.\n * - Valid JSON string → parsed object\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Invalid JSON → `undefined` (schema validation will catch this)\n */\n json(value: unknown): unknown {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch {\n return undefined;\n }\n },\n\n /**\n * Coerce a date string to a Date object.\n * Handles `<input type=\"date\">` (`\"2024-01-15\"`), `<input type=\"datetime-local\">`\n * (`\"2024-01-15T10:30\"`), and full ISO 8601 strings.\n * - Valid date string → `Date`\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Invalid date strings → `undefined` (schema validation will catch this)\n * - Impossible dates that `new Date()` silently normalizes (e.g. Feb 31) → `undefined`\n */\n date(value: unknown): Date | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (value instanceof Date) return value;\n if (typeof value !== 'string') return undefined;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return undefined;\n\n // Overflow detection: extract Y/M/D from the input string and verify\n // they match the parsed Date components. new Date('2024-02-31') silently\n // normalizes to March 2nd — we reject such inputs.\n const ymdMatch = value.match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n if (ymdMatch) {\n const inputYear = Number(ymdMatch[1]);\n const inputMonth = Number(ymdMatch[2]);\n const inputDay = Number(ymdMatch[3]);\n\n // Use UTC methods for date-only and Z-suffixed strings to avoid\n // timezone offset shifting the day. For datetime-local (no Z suffix),\n // the Date constructor parses in local time, so use local methods.\n const isUTC = value.length === 10 || value.endsWith('Z');\n const parsedYear = isUTC ? date.getUTCFullYear() : date.getFullYear();\n const parsedMonth = isUTC ? date.getUTCMonth() + 1 : date.getMonth() + 1;\n const parsedDay = isUTC ? date.getUTCDate() : date.getDate();\n\n if (inputYear !== parsedYear || inputMonth !== parsedMonth || inputDay !== parsedDay) {\n return undefined;\n }\n }\n\n return date;\n },\n\n /**\n * Create a File coercion function with optional size and mime type validation.\n * Returns the File if valid, `undefined` otherwise.\n *\n * ```ts\n * // Basic — just checks it's a real File\n * z.preprocess(coerce.file(), z.instanceof(File))\n *\n * // With constraints\n * z.preprocess(\n * coerce.file({ maxSize: 5 * 1024 * 1024, accept: ['image/png', 'image/jpeg'] }),\n * z.instanceof(File)\n * )\n * ```\n */\n file(options?: { maxSize?: number; accept?: string[] }): (value: unknown) => File | undefined {\n return (value: unknown): File | undefined => {\n if (value === undefined || value === null || value === '') return undefined;\n if (!(value instanceof File)) return undefined;\n\n // Empty file input (no selection): browsers submit File with name=\"\" and size=0\n if (value.size === 0 && value.name === '') return undefined;\n\n if (options?.maxSize !== undefined && value.size > options.maxSize) {\n return undefined;\n }\n\n if (options?.accept !== undefined && !options.accept.includes(value.type)) {\n return undefined;\n }\n\n return value;\n };\n },\n};\n","/**\n * Server action primitives: revalidatePath, revalidateTag, and the action handler.\n *\n * - revalidatePath(path) re-renders the route at that path and returns the RSC\n * flight payload for inline reconciliation.\n * - revalidateTag(tag) invalidates timber.cache entries by tag.\n *\n * Both are callable from anywhere on the server — actions, API routes, handlers.\n *\n * The action handler processes incoming action requests, validates CSRF,\n * enforces body limits, executes the action, and returns the response\n * (with piggybacked RSC payload if revalidatePath was called).\n *\n * See design/08-forms-and-actions.md\n */\n\nimport { cache } from '../cache/cache-api.ts';\nimport { DenySignal, RedirectSignal } from './primitives.ts';\nimport { withSpan } from './tracing.ts';\nimport { revalidationAls, type RevalidationState } from './als-registry.ts';\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/** Result of rendering a revalidation — payload root before RSC serialization. */\nexport interface RevalidationResult {\n /**\n * The payload root — tree plus the params it rendered with, exactly as an\n * ordinary route payload. The client splits it in `applyRevalidation`.\n */\n payload: unknown;\n}\n\n/** Renderer function that builds a React element tree for a given path. */\nexport type RevalidateRenderer = (path: string) => Promise<RevalidationResult>;\n\n// Re-export the type from the registry for public API consumers.\nexport type { RevalidationState } from './als-registry.ts';\n\n/** Options for creating the action handler. */\nexport interface ActionHandlerConfig {\n /** Renderer for producing RSC payloads during revalidation. */\n renderer?: RevalidateRenderer;\n}\n\n/** Result of handling a server action request. */\nexport interface ActionHandlerResult {\n /** The action's return value (serialized). */\n actionResult: unknown;\n /** Revalidation result if revalidatePath was called (element tree, not yet serialized). */\n revalidation?: RevalidationResult;\n /** Redirect location if a RedirectSignal was thrown during revalidation. */\n redirectTo?: string;\n /** Redirect status code. */\n redirectStatus?: number;\n}\n\n// ─── Revalidation State ──────────────────────────────────────────────────\n\n// Per-request revalidation state stored in AsyncLocalStorage (from als-registry.ts).\n// This ensures concurrent requests never share or overwrite each other's state\n// (the previous module-level global was vulnerable to cross-request pollution).\n\n/**\n * Run `fn` inside a revalidation ALS scope.\n * @internal — for tests that call revalidatePath/Tag directly without executeAction().\n */\nexport function _runWithRevalidationState<T>(state: RevalidationState, fn: () => T): T {\n return revalidationAls.run(state, fn);\n}\n\n/**\n * Get the current revalidation state. Throws if called outside an action context.\n * @internal\n */\nfunction getRevalidationState(): RevalidationState {\n const state = revalidationAls.getStore();\n if (!state) {\n throw new Error(\n 'revalidatePath/revalidateTag called outside of a server action context. ' +\n 'These functions can only be called during action execution.'\n );\n }\n return state;\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────\n\n/**\n * Re-render the route at `path` and include the RSC flight payload in the\n * action response. The client reconciles inline — no separate fetch needed.\n *\n * Can be called from server actions, API routes, or any server-side context.\n *\n * @param path - The path to re-render (e.g. '/dashboard', '/todos').\n */\nexport function revalidatePath(path: string): void {\n const state = getRevalidationState();\n if (!state.paths.includes(path)) {\n state.paths.push(path);\n }\n}\n\n/**\n * Invalidate all timber.cache entries tagged with `tag`.\n * Does not return a payload — the next request for an invalidated entry re-executes.\n *\n * @param tag - The cache tag to invalidate (e.g. 'products', 'user:123').\n */\nexport function revalidateTag(tag: string): void {\n const state = getRevalidationState();\n if (!state.tags.includes(tag)) {\n state.tags.push(tag);\n }\n}\n\n// ─── Action Handler ──────────────────────────────────────────────────────\n\n/**\n * Execute a server action and process revalidation.\n *\n * 1. Sets up revalidation state\n * 2. Calls the action function\n * 3. Processes revalidateTag calls (invalidates cache entries)\n * 4. Processes revalidatePath calls (re-renders and captures RSC payload)\n * 5. Returns the action result + optional RSC payload\n *\n * @param actionFn - The server action function to execute.\n * @param args - Arguments to pass to the action.\n * @param config - Handler configuration (cache handler, renderer).\n */\nexport async function executeAction(\n actionFn: (...args: unknown[]) => Promise<unknown>,\n args: unknown[],\n config: ActionHandlerConfig = {},\n spanMeta?: { actionFile?: string; actionName?: string }\n): Promise<ActionHandlerResult> {\n const state: RevalidationState = { paths: [], tags: [] };\n let actionResult: unknown;\n let redirectTo: string | undefined;\n let redirectStatus: number | undefined;\n\n // Run the action inside ALS scope so revalidatePath/Tag resolve to this\n // request's state object — concurrent requests each get their own scope.\n await revalidationAls.run(state, async () => {\n try {\n actionResult = await withSpan(\n 'timber.action',\n {\n ...(spanMeta?.actionFile ? { 'timber.action_file': spanMeta.actionFile } : {}),\n ...(spanMeta?.actionName ? { 'timber.action_name': spanMeta.actionName } : {}),\n },\n () => actionFn(...args)\n );\n } catch (error) {\n if (error instanceof RedirectSignal) {\n redirectTo = error.location;\n redirectStatus = error.status;\n } else {\n throw error;\n }\n }\n });\n\n // Process tag invalidation through cache.invalidate so it records the\n // invalidation epoch — an in-flight cached fn must not re-store data this\n // mutation just invalidated (TIM-1028). cache.invalidate resolves the\n // module-level handler singleton: setCacheHandler() is called at boot from\n // rsc-entry when timber.config.ts provides a cacheHandler; otherwise falls\n // back to in-memory LRU (TIM-599).\n if (state.tags.length > 0) {\n // cache.invalidate handles both data-cache clearing AND component-seed\n // tombstone writes (design/45 §ISR mechanics) — no separate tombstone\n // step needed here.\n // Best-effort: the action body already executed and its mutation committed.\n // Use allSettled so all tags settle before proceeding — Promise.all would\n // short-circuit on the first rejection, leaving other invalidations (including\n // component-seed tombstone writes) in-flight during revalidatePath rendering.\n const results = await Promise.allSettled(state.tags.map((tag) => cache.invalidate({ tag })));\n for (const r of results) {\n if (r.status === 'rejected') {\n console.error('[timber] revalidateTag invalidation failed:', r.reason);\n }\n }\n }\n\n // Process path revalidation — build element tree (not yet serialized)\n let revalidation: RevalidationResult | undefined;\n if (state.paths.length > 0 && config.renderer) {\n // For now, render the first revalidated path.\n // Multiple paths could be supported via multipart streaming in the future.\n const path = state.paths[0];\n try {\n revalidation = await config.renderer(path);\n } catch (renderError) {\n if (renderError instanceof RedirectSignal) {\n // Revalidation triggered a redirect (e.g., session expired,\n // middleware on the target path called `redirect()`).\n redirectTo = renderError.location;\n redirectStatus = renderError.status;\n } else if (renderError instanceof DenySignal) {\n // Target path's middleware/access denied this user.\n //\n // Dropping the revalidation here is the security boundary added\n // in TIM-996: without this branch, a DenySignal would fall into\n // the `console.error` path AND the element tree would have\n // already been built and serialized — leaking a privileged RSC\n // payload to the action submitter. By the time the signal\n // surfaces, the renderer has thrown BEFORE building the tree,\n // so we simply drop `revalidation` and ship the action result\n // alone. The action itself still succeeded.\n console.error(\n `[timber] revalidatePath dropped — target middleware denied (status ${renderError.status})`\n );\n } else {\n // Log but don't fail the action — revalidation is best-effort.\n // Includes input-validation rejections from the renderer\n // (e.g., revalidatePath('//evil.com/admin')).\n console.error('[timber] revalidatePath render failed:', renderError);\n }\n }\n }\n\n return {\n actionResult,\n revalidation,\n ...(redirectTo ? { redirectTo, redirectStatus } : {}),\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"als-registry-C6kcfprT.js","names":[],"sources":["../../src/server/als-registry.ts"],"sourcesContent":["/**\n * Centralized AsyncLocalStorage registry for server-side per-request state.\n *\n * ALL ALS instances used by the server framework live here. Individual\n * modules (request-context.ts, tracing.ts, actions.ts, etc.) import from\n * this registry and re-export public accessor functions.\n *\n * Why: ALS instances require singleton semantics — if two copies of the\n * same ALS exist (one from a relative import, one from a barrel import,\n * or the same file re-evaluated by Vite after an HMR invalidation), one\n * module writes to its copy and another reads from an empty copy.\n * Centralizing ALS creation in a single module + anchoring each instance\n * on `globalThis` under a `Symbol.for(...)` key eliminates this class of\n * bug, including the Vite-dev case where module instance split would\n * otherwise hand two copies of this file out of a single evaluation cycle.\n *\n * The `timber-shims` plugin remaps `@timber-js/app/server` to src/ in\n * RSC and SSR environments so import-specifier paths converge here, and\n * the `globalThis[SYMBOL_FOR]` anchor below closes the remaining gap: any\n * second evaluation of this module picks up the ALS the first evaluation\n * already stored rather than instantiating a fresh one. Same pattern the\n * Cloudflare bindings ALS uses in `adapters/cloudflare.ts`.\n *\n * DO NOT create ALS instances outside this file. If you need a new ALS,\n * add it here and import from `./als-registry.js` in the consuming module.\n * Inside this file, always route through `getOrCreateAls(symbol)` — a raw\n * `new AsyncLocalStorage()` reintroduces the instance-split hazard.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\". Regression test:\n * `tests/als-registry-singleton.test.ts`.\n */\n\nimport '#server-only-guard';\nimport type { CoercedParams } from '../shared/param-value.js';\nimport type { SlotParamsRecord } from '../shared/slot-params.js';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { DebugComponentEntry } from './rsc-entry/helpers.js';\n\n/**\n * Return a process-wide singleton `AsyncLocalStorage` keyed by `symbol`.\n *\n * `Symbol.for(...)` keys live in the cross-realm symbol registry, and\n * `globalThis` is shared across every module evaluation inside the same\n * Node.js process. Together they give a stable anchor that survives\n * re-evaluation of this file — the second evaluation observes the\n * `globalThis[symbol]` slot already populated by the first and hands\n * back the identical ALS instance.\n */\nfunction getOrCreateAls<T>(symbol: symbol): AsyncLocalStorage<T> {\n const g = globalThis as unknown as Record<symbol, unknown>;\n const existing = g[symbol];\n if (existing instanceof AsyncLocalStorage) {\n return existing as AsyncLocalStorage<T>;\n }\n const created = new AsyncLocalStorage<T>();\n g[symbol] = created;\n return created;\n}\n\n// ─── Request Context ──────────────────────────────────────────────────────\n// Used by: request-context.ts (getHeaders(), getCookies(), getSearchParams())\n// Design doc: design/04-authorization.md\n\n/** @internal — import via request-context.ts public API */\nexport const requestContextAls = getOrCreateAls<RequestContextStore>(\n Symbol.for('timber:request-context-als')\n);\n\nexport interface RequestContextStore {\n /** Incoming request headers (read-only view). */\n headers: Headers;\n /** Raw cookie header string, parsed lazily into a Map on first access. */\n cookieHeader: string;\n /** Lazily-parsed cookie map (mutable — reflects write-overlay from set()). */\n parsedCookies?: Map<string, string>;\n /** Original (pre-overlay) frozen headers, kept for overlay merging. */\n originalHeaders: Headers;\n /**\n * Raw URLSearchParams for the current request.\n * To get typed parsed params, import a search params definition and\n * call `.parse(searchParams())`.\n */\n searchParams: URLSearchParams;\n /**\n * Raw search string from the request URL (e.g. \"?foo=bar&baz=1\").\n * Available synchronously for use in `redirect()` with `preserveSearchParams`.\n */\n searchString: string;\n /**\n * Coerced segment params for the current request.\n * Set by the pipeline after route matching and param coercion, before\n * middleware and rendering. Pages and layouts read params via\n * `getSegmentParams()` instead of receiving them as a prop.\n *\n * See design/07-routing.md §\"params.ts — Convention File for Typed Params\"\n */\n segmentParams?: CoercedParams;\n /**\n * The matched segment path (e.g. '/(browse)/[artistSlug]/[year]').\n * Includes route groups and parallel slots — uniquely identifies the\n * segment in the route tree. Set by the pipeline alongside segmentParams.\n * Used by getSegmentParams() for dev-mode validation.\n * See design/41-global-params.md §Runtime Validation\n */\n matchedSegmentPath?: string;\n /**\n * Per-slot coerced segment params, keyed by the slot's full tree path\n * (e.g. '/(browse)/@shows/[artistSlug]/[...year]').\n *\n * Each slot walks its own segment tree and may interpret URL parts\n * differently from the main route (e.g. catch-all vs dynamic). This\n * map stores slot-specific params so getSegmentParams(segmentPath)\n * returns the correct types for slot pages.\n *\n * See design/07-routing.md §\"Parallel Slot Params\"\n */\n slotParams?: SlotParamsRecord;\n /** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */\n cookieJar: Map<string, CookieEntry>;\n /** Whether the response has flushed (headers committed). */\n flushed: boolean;\n /** Whether the current context allows cookie mutation. */\n mutableContext: boolean;\n /**\n * Set by AccessGate or PageDenyBoundary when a DenySignal is caught\n * server-side (inside the React tree, before React Flight sees it).\n * The pipeline reads this after render to set the HTTP status code.\n * See TIM-666.\n */\n denyStatus?: number;\n /**\n * Callback fired when setDenyStatus() is called. Used by shellSettled\n * to short-circuit when any component catches a DenySignal in-tree,\n * regardless of which component caught it (AccessGate, TracedLayout,\n * or PageDenyBoundary). See TIM-1208.\n */\n onDenyStatus?: () => void;\n /**\n * Dev-only: getter for the current request's RSC debug components.\n * Set by renderRoute() so onPipelineError can include component tree\n * context for render-phase errors without module-level shared state.\n */\n debugComponentsGetter?: () => DebugComponentEntry[];\n /**\n * Index of the first segment whose AccessGate denied during render.\n * Recorded by AccessGate when it catches a DenySignal so post-render\n * consumers (buildSegmentInfo, X-Timber-Segments header) can bound\n * the segment list without a pre-render access pass. Infinity when\n * no segment denied. See TIM-1363.\n */\n _firstDeniedSegmentIndex?: number;\n /**\n * Metadata entries collected during render by MetadataCollector.\n * Populated per-segment in tree order (root→page) during\n * renderToReadableStream. Entries may contain promises for dynamic\n * metadata (resolved concurrently with page rendering, TIM-1367).\n * MetadataHead awaits all entries before merging.\n * See TIM-1363, TIM-1367, design/16-metadata.md.\n */\n _metadataEntries?: Array<{\n metadata: import('./types.js').Metadata | Promise<import('./types.js').Metadata | null>;\n isPage: boolean;\n }>;\n}\n\n/** A single outgoing cookie entry in the cookie jar. */\nexport interface CookieEntry {\n name: string;\n value: string;\n options: import('./cookie-context.js').CookieOptions;\n}\n\n// ─── Tracing ──────────────────────────────────────────────────────────────\n// Used by: tracing.ts (getTraceId(), getSpanId())\n// Design doc: design/17-logging.md\n\nexport interface TraceStore {\n /** 32-char lowercase hex trace ID (OTEL or UUID fallback). */\n traceId: string;\n /** OTEL span ID if available, undefined otherwise. */\n spanId?: string;\n /**\n * Innermost active native platform span (e.g. Cloudflare's `Span` from\n * tracing.enterSpan()). Tracked so setSpanAttribute() can set attributes\n * after span creation — the platform API has no getActiveSpan() equivalent.\n * Saved/restored by withSpan() around each callback. See TIM-1135.\n */\n platformSpan?: import('./tracing.js').PlatformSpan;\n}\n\n/** @internal — import via tracing.ts public API */\nexport const traceAls = getOrCreateAls<TraceStore>(Symbol.for('timber:trace-als'));\n\n// ─── Server-Timing ────────────────────────────────────────────────────────\n// Used by: server-timing.ts (recordTiming(), withTiming())\n// Design doc: (dev-only performance instrumentation)\n\nexport interface TimingStore {\n entries: import('./server-timing.js').TimingEntry[];\n}\n\n/** @internal — import via server-timing.ts public API */\nexport const timingAls = getOrCreateAls<TimingStore>(Symbol.for('timber:timing-als'));\n\n// ─── Revalidation ─────────────────────────────────────────────────────────\n// Used by: actions.ts (revalidatePath(), revalidateTag())\n// Design doc: design/08-forms-and-actions.md\n\nexport interface RevalidationState {\n /** Paths to re-render (populated by revalidatePath calls). */\n paths: string[];\n /** Tags to invalidate (populated by revalidateTag calls). */\n tags: string[];\n}\n\n/** @internal — import via actions.ts public API */\nexport const revalidationAls = getOrCreateAls<RevalidationState>(\n Symbol.for('timber:revalidation-als')\n);\n\n// ─── Form Flash ───────────────────────────────────────────────────────────\n// Used by: form-flash.ts (getFormFlash())\n// Design doc: design/08-forms-and-actions.md §\"No-JS Error Round-Trip\"\n\n/** @internal — import via form-flash.ts public API */\nexport const formFlashAls = getOrCreateAls<import('./form-flash.js').FormFlashData>(\n Symbol.for('timber:form-flash-als')\n);\n\n// ─── Early Hints Sender ──────────────────────────────────────────────────\n// Used by: early-hints-sender.ts (sendEarlyHints103())\n// Design doc: design/02-rendering-pipeline.md §\"Early Hints (103)\"\n\n/** Function that sends Link header values as a 103 Early Hints response. */\nexport type EarlyHintsSenderFn = (links: string[]) => void;\n\n/** @internal — import via early-hints-sender.ts public API */\nexport const earlyHintsSenderAls = getOrCreateAls<EarlyHintsSenderFn>(\n Symbol.for('timber:early-hints-sender-als')\n);\n\n// ─── waitUntil Bridge ────────────────────────────────────────────────────\n// Used by: waituntil-bridge.ts (waitUntil())\n// Design doc: design/11-platform.md §\"waitUntil()\"\n\n/** Function that extends the request lifecycle with a background promise. */\nexport type WaitUntilFn = (promise: Promise<unknown>) => void;\n\n/** @internal — import via waituntil-bridge.ts public API */\nexport const waitUntilAls = getOrCreateAls<WaitUntilFn>(Symbol.for('timber:wait-until-als'));\n\n// ─── Workers Cache Purge Bridge ─────────────────────────────────────────\n// Used by: workers-cache-bridge.ts (runWithWorkersCachePurge())\n// Design doc: design/06-caching.md, design/25-production-deployments.md\n\n/** Workers Cache purge function from Cloudflare ExecutionContext.cache. */\nexport interface WorkersCachePurgeCtx {\n purge(opts: { tags: string[] }): Promise<void>;\n}\n\n/** @internal — import via workers-cache-bridge.ts public API */\nexport const workersCachePurgeAls = getOrCreateAls<WorkersCachePurgeCtx>(\n Symbol.for('timber:workers-cache-purge-als')\n);\n"],"mappings":";;;;;;;;;;;;;AAiDA,SAAS,eAAkB,QAAsC;CAC/D,MAAM,IAAI;CACV,MAAM,WAAW,EAAE;CACnB,IAAI,oBAAoB,mBACtB,OAAO;CAET,MAAM,UAAU,IAAI,kBAAqB;CACzC,EAAE,UAAU;CACZ,OAAO;AACT;;AAOA,IAAa,oBAAoB,eAC/B,OAAO,IAAI,4BAA4B,CACzC;;AA6HA,IAAa,WAAW,eAA2B,OAAO,IAAI,kBAAkB,CAAC;;AAWjF,IAAa,YAAY,eAA4B,OAAO,IAAI,mBAAmB,CAAC;;AAcpF,IAAa,kBAAkB,eAC7B,OAAO,IAAI,yBAAyB,CACtC;;AAOA,IAAa,eAAe,eAC1B,OAAO,IAAI,uBAAuB,CACpC;;AAUA,IAAa,sBAAsB,eACjC,OAAO,IAAI,+BAA+B,CAC5C;;AAUA,IAAa,eAAe,eAA4B,OAAO,IAAI,uBAAuB,CAAC;;AAY3F,IAAa,uBAAuB,eAClC,OAAO,IAAI,gCAAgC,CAC7C"}
|
|
1
|
+
{"version":3,"file":"als-registry-C6kcfprT.js","names":[],"sources":["../../src/server/als-registry.ts"],"sourcesContent":["/**\n * Centralized AsyncLocalStorage registry for server-side per-request state.\n *\n * ALL ALS instances used by the server framework live here. Individual\n * modules (request-context.ts, tracing.ts, actions.ts, etc.) import from\n * this registry and re-export public accessor functions.\n *\n * Why: ALS instances require singleton semantics — if two copies of the\n * same ALS exist (one from a relative import, one from a barrel import,\n * or the same file re-evaluated by Vite after an HMR invalidation), one\n * module writes to its copy and another reads from an empty copy.\n * Centralizing ALS creation in a single module + anchoring each instance\n * on `globalThis` under a `Symbol.for(...)` key eliminates this class of\n * bug, including the Vite-dev case where module instance split would\n * otherwise hand two copies of this file out of a single evaluation cycle.\n *\n * The `timber-shims` plugin remaps `@timber-js/app/server` to src/ in\n * RSC and SSR environments so import-specifier paths converge here, and\n * the `globalThis[SYMBOL_FOR]` anchor below closes the remaining gap: any\n * second evaluation of this module picks up the ALS the first evaluation\n * already stored rather than instantiating a fresh one. Same pattern the\n * Cloudflare bindings ALS uses in `adapters/cloudflare.ts`.\n *\n * DO NOT create ALS instances outside this file. If you need a new ALS,\n * add it here and import from `./als-registry.js` in the consuming module.\n * Inside this file, always route through `getOrCreateAls(symbol)` — a raw\n * `new AsyncLocalStorage()` reintroduces the instance-split hazard.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\". Regression test:\n * `tests/als-registry-singleton.test.ts`.\n */\n\nimport '#server-only-guard';\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { DebugComponentEntry } from './rsc-entry/helpers.ts';\n\n/**\n * Return a process-wide singleton `AsyncLocalStorage` keyed by `symbol`.\n *\n * `Symbol.for(...)` keys live in the cross-realm symbol registry, and\n * `globalThis` is shared across every module evaluation inside the same\n * Node.js process. Together they give a stable anchor that survives\n * re-evaluation of this file — the second evaluation observes the\n * `globalThis[symbol]` slot already populated by the first and hands\n * back the identical ALS instance.\n */\nfunction getOrCreateAls<T>(symbol: symbol): AsyncLocalStorage<T> {\n const g = globalThis as unknown as Record<symbol, unknown>;\n const existing = g[symbol];\n if (existing instanceof AsyncLocalStorage) {\n return existing as AsyncLocalStorage<T>;\n }\n const created = new AsyncLocalStorage<T>();\n g[symbol] = created;\n return created;\n}\n\n// ─── Request Context ──────────────────────────────────────────────────────\n// Used by: request-context.ts (getHeaders(), getCookies(), getSearchParams())\n// Design doc: design/04-authorization.md\n\n/** @internal — import via request-context.ts public API */\nexport const requestContextAls = getOrCreateAls<RequestContextStore>(\n Symbol.for('timber:request-context-als')\n);\n\nexport interface RequestContextStore {\n /** Incoming request headers (read-only view). */\n headers: Headers;\n /** Raw cookie header string, parsed lazily into a Map on first access. */\n cookieHeader: string;\n /** Lazily-parsed cookie map (mutable — reflects write-overlay from set()). */\n parsedCookies?: Map<string, string>;\n /** Original (pre-overlay) frozen headers, kept for overlay merging. */\n originalHeaders: Headers;\n /**\n * Raw URLSearchParams for the current request.\n * To get typed parsed params, import a search params definition and\n * call `.parse(searchParams())`.\n */\n searchParams: URLSearchParams;\n /**\n * Raw search string from the request URL (e.g. \"?foo=bar&baz=1\").\n * Available synchronously for use in `redirect()` with `preserveSearchParams`.\n */\n searchString: string;\n /**\n * Coerced segment params for the current request.\n * Set by the pipeline after route matching and param coercion, before\n * middleware and rendering. Pages and layouts read params via\n * `getSegmentParams()` instead of receiving them as a prop.\n *\n * See design/07-routing.md §\"params.ts — Convention File for Typed Params\"\n */\n segmentParams?: CoercedParams;\n /**\n * The matched segment path (e.g. '/(browse)/[artistSlug]/[year]').\n * Includes route groups and parallel slots — uniquely identifies the\n * segment in the route tree. Set by the pipeline alongside segmentParams.\n * Used by getSegmentParams() for dev-mode validation.\n * See design/41-global-params.md §Runtime Validation\n */\n matchedSegmentPath?: string;\n /**\n * Per-slot coerced segment params, keyed by the slot's full tree path\n * (e.g. '/(browse)/@shows/[artistSlug]/[...year]').\n *\n * Each slot walks its own segment tree and may interpret URL parts\n * differently from the main route (e.g. catch-all vs dynamic). This\n * map stores slot-specific params so getSegmentParams(segmentPath)\n * returns the correct types for slot pages.\n *\n * See design/07-routing.md §\"Parallel Slot Params\"\n */\n slotParams?: SlotParamsRecord;\n /** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */\n cookieJar: Map<string, CookieEntry>;\n /** Whether the response has flushed (headers committed). */\n flushed: boolean;\n /** Whether the current context allows cookie mutation. */\n mutableContext: boolean;\n /**\n * Set by AccessGate or PageDenyBoundary when a DenySignal is caught\n * server-side (inside the React tree, before React Flight sees it).\n * The pipeline reads this after render to set the HTTP status code.\n * See TIM-666.\n */\n denyStatus?: number;\n /**\n * Callback fired when setDenyStatus() is called. Used by shellSettled\n * to short-circuit when any component catches a DenySignal in-tree,\n * regardless of which component caught it (AccessGate, TracedLayout,\n * or PageDenyBoundary). See TIM-1208.\n */\n onDenyStatus?: () => void;\n /**\n * Dev-only: getter for the current request's RSC debug components.\n * Set by renderRoute() so onPipelineError can include component tree\n * context for render-phase errors without module-level shared state.\n */\n debugComponentsGetter?: () => DebugComponentEntry[];\n /**\n * Index of the first segment whose AccessGate denied during render.\n * Recorded by AccessGate when it catches a DenySignal so post-render\n * consumers (buildSegmentInfo, X-Timber-Segments header) can bound\n * the segment list without a pre-render access pass. Infinity when\n * no segment denied. See TIM-1363.\n */\n _firstDeniedSegmentIndex?: number;\n /**\n * Metadata entries collected during render by MetadataCollector.\n * Populated per-segment in tree order (root→page) during\n * renderToReadableStream. Entries may contain promises for dynamic\n * metadata (resolved concurrently with page rendering, TIM-1367).\n * MetadataHead awaits all entries before merging.\n * See TIM-1363, TIM-1367, design/16-metadata.md.\n */\n _metadataEntries?: Array<{\n metadata: import('./types.ts').Metadata | Promise<import('./types.ts').Metadata | null>;\n isPage: boolean;\n }>;\n}\n\n/** A single outgoing cookie entry in the cookie jar. */\nexport interface CookieEntry {\n name: string;\n value: string;\n options: import('./cookie-context.ts').CookieOptions;\n}\n\n// ─── Tracing ──────────────────────────────────────────────────────────────\n// Used by: tracing.ts (getTraceId(), getSpanId())\n// Design doc: design/17-logging.md\n\nexport interface TraceStore {\n /** 32-char lowercase hex trace ID (OTEL or UUID fallback). */\n traceId: string;\n /** OTEL span ID if available, undefined otherwise. */\n spanId?: string;\n /**\n * Innermost active native platform span (e.g. Cloudflare's `Span` from\n * tracing.enterSpan()). Tracked so setSpanAttribute() can set attributes\n * after span creation — the platform API has no getActiveSpan() equivalent.\n * Saved/restored by withSpan() around each callback. See TIM-1135.\n */\n platformSpan?: import('./tracing.ts').PlatformSpan;\n}\n\n/** @internal — import via tracing.ts public API */\nexport const traceAls = getOrCreateAls<TraceStore>(Symbol.for('timber:trace-als'));\n\n// ─── Server-Timing ────────────────────────────────────────────────────────\n// Used by: server-timing.ts (recordTiming(), withTiming())\n// Design doc: (dev-only performance instrumentation)\n\nexport interface TimingStore {\n entries: import('./server-timing.ts').TimingEntry[];\n}\n\n/** @internal — import via server-timing.ts public API */\nexport const timingAls = getOrCreateAls<TimingStore>(Symbol.for('timber:timing-als'));\n\n// ─── Revalidation ─────────────────────────────────────────────────────────\n// Used by: actions.ts (revalidatePath(), revalidateTag())\n// Design doc: design/08-forms-and-actions.md\n\nexport interface RevalidationState {\n /** Paths to re-render (populated by revalidatePath calls). */\n paths: string[];\n /** Tags to invalidate (populated by revalidateTag calls). */\n tags: string[];\n}\n\n/** @internal — import via actions.ts public API */\nexport const revalidationAls = getOrCreateAls<RevalidationState>(\n Symbol.for('timber:revalidation-als')\n);\n\n// ─── Form Flash ───────────────────────────────────────────────────────────\n// Used by: form-flash.ts (getFormFlash())\n// Design doc: design/08-forms-and-actions.md §\"No-JS Error Round-Trip\"\n\n/** @internal — import via form-flash.ts public API */\nexport const formFlashAls = getOrCreateAls<import('./form-flash.ts').FormFlashData>(\n Symbol.for('timber:form-flash-als')\n);\n\n// ─── Early Hints Sender ──────────────────────────────────────────────────\n// Used by: early-hints-sender.ts (sendEarlyHints103())\n// Design doc: design/02-rendering-pipeline.md §\"Early Hints (103)\"\n\n/** Function that sends Link header values as a 103 Early Hints response. */\nexport type EarlyHintsSenderFn = (links: string[]) => void;\n\n/** @internal — import via early-hints-sender.ts public API */\nexport const earlyHintsSenderAls = getOrCreateAls<EarlyHintsSenderFn>(\n Symbol.for('timber:early-hints-sender-als')\n);\n\n// ─── waitUntil Bridge ────────────────────────────────────────────────────\n// Used by: waituntil-bridge.ts (waitUntil())\n// Design doc: design/11-platform.md §\"waitUntil()\"\n\n/** Function that extends the request lifecycle with a background promise. */\nexport type WaitUntilFn = (promise: Promise<unknown>) => void;\n\n/** @internal — import via waituntil-bridge.ts public API */\nexport const waitUntilAls = getOrCreateAls<WaitUntilFn>(Symbol.for('timber:wait-until-als'));\n\n// ─── Workers Cache Purge Bridge ─────────────────────────────────────────\n// Used by: workers-cache-bridge.ts (runWithWorkersCachePurge())\n// Design doc: design/06-caching.md, design/25-production-deployments.md\n\n/** Workers Cache purge function from Cloudflare ExecutionContext.cache. */\nexport interface WorkersCachePurgeCtx {\n purge(opts: { tags: string[] }): Promise<void>;\n}\n\n/** @internal — import via workers-cache-bridge.ts public API */\nexport const workersCachePurgeAls = getOrCreateAls<WorkersCachePurgeCtx>(\n Symbol.for('timber:workers-cache-purge-als')\n);\n"],"mappings":";;;;;;;;;;;;;AAiDA,SAAS,eAAkB,QAAsC;CAC/D,MAAM,IAAI;CACV,MAAM,WAAW,EAAE;CACnB,IAAI,oBAAoB,mBACtB,OAAO;CAET,MAAM,UAAU,IAAI,kBAAqB;CACzC,EAAE,UAAU;CACZ,OAAO;AACT;;AAOA,IAAa,oBAAoB,eAC/B,OAAO,IAAI,4BAA4B,CACzC;;AA6HA,IAAa,WAAW,eAA2B,OAAO,IAAI,kBAAkB,CAAC;;AAWjF,IAAa,YAAY,eAA4B,OAAO,IAAI,mBAAmB,CAAC;;AAcpF,IAAa,kBAAkB,eAC7B,OAAO,IAAI,yBAAyB,CACtC;;AAOA,IAAa,eAAe,eAC1B,OAAO,IAAI,uBAAuB,CACpC;;AAUA,IAAa,sBAAsB,eACjC,OAAO,IAAI,+BAA+B,CAC5C;;AAUA,IAAa,eAAe,eAA4B,OAAO,IAAI,uBAAuB,CAAC;;AAY3F,IAAa,uBAAuB,eAClC,OAAO,IAAI,gCAAgC,CAC7C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"als-slots-BEEIPKYm.js","names":[],"sources":["../../src/shared/als-slots.ts"],"sourcesContent":["/**\n * ALS getter slots for the isomorphic `define*` modules.\n *\n * `searchParams.get()` and `segmentParams.get()` read per-request state out of\n * `server/als-registry.ts`, but the modules that expose them are *isomorphic*:\n * `search-params/define.ts` and `segment-params/define.ts` are imported by\n * `'use client'` components for their hooks, so neither may import\n * `node:async_hooks` — not even transitively, because the dep optimizer\n * pre-bundles them before per-environment resolution runs. So the server\n * registers its getter into a slot at module-init time and the define modules\n * read the slot.\n *\n * The slots live here, in a module with **no imports at all**, rather than in\n * the define modules themselves. That is the whole point: `request-context.ts`\n * has to reach the setter, and when the setter lived in\n * `search-params/define.ts` every server entry that touched\n * `@timber-js/app/server` pulled that module — and therefore its `'use client'`\n * import of `client/use-query-states.ts`, and therefore `nuqs` — into the\n * server graph. `nuqs` named-imports `createContext`/`useContext` from React,\n * which React's `react-server` export withholds, so `@timber-js/app`,\n * `@timber-js/app/server`, `@timber-js/app/cookies` and `@timber-js/app/cache`\n * all failed to instantiate under the `react-server` condition (TIM-1298).\n * A leaf module breaks the edge without a registration ceremony on either side.\n *\n * Registration must be eager, not a call-time `await import()`: the async\n * microtask a dynamic import introduces loses AsyncLocalStorage context inside\n * React's RSC Flight renderer, which broke `getSearchParams()` in parallel slot\n * pages (TIM-523).\n *\n * See design/23-search-params.md §\"defineSearchParams — The Factory\" and\n * design/41-global-params.md.\n */\n\nimport type { CoercedParams } from './param-value.
|
|
1
|
+
{"version":3,"file":"als-slots-BEEIPKYm.js","names":[],"sources":["../../src/shared/als-slots.ts"],"sourcesContent":["/**\n * ALS getter slots for the isomorphic `define*` modules.\n *\n * `searchParams.get()` and `segmentParams.get()` read per-request state out of\n * `server/als-registry.ts`, but the modules that expose them are *isomorphic*:\n * `search-params/define.ts` and `segment-params/define.ts` are imported by\n * `'use client'` components for their hooks, so neither may import\n * `node:async_hooks` — not even transitively, because the dep optimizer\n * pre-bundles them before per-environment resolution runs. So the server\n * registers its getter into a slot at module-init time and the define modules\n * read the slot.\n *\n * The slots live here, in a module with **no imports at all**, rather than in\n * the define modules themselves. That is the whole point: `request-context.ts`\n * has to reach the setter, and when the setter lived in\n * `search-params/define.ts` every server entry that touched\n * `@timber-js/app/server` pulled that module — and therefore its `'use client'`\n * import of `client/use-query-states.ts`, and therefore `nuqs` — into the\n * server graph. `nuqs` named-imports `createContext`/`useContext` from React,\n * which React's `react-server` export withholds, so `@timber-js/app`,\n * `@timber-js/app/server`, `@timber-js/app/cookies` and `@timber-js/app/cache`\n * all failed to instantiate under the `react-server` condition (TIM-1298).\n * A leaf module breaks the edge without a registration ceremony on either side.\n *\n * Registration must be eager, not a call-time `await import()`: the async\n * microtask a dynamic import introduces loses AsyncLocalStorage context inside\n * React's RSC Flight renderer, which broke `getSearchParams()` in parallel slot\n * pages (TIM-523).\n *\n * See design/23-search-params.md §\"defineSearchParams — The Factory\" and\n * design/41-global-params.md.\n */\n\nimport type { CoercedParams } from './param-value.ts';\n\ninterface AlsSlot<T> {\n /** Register the server-side getter. Called once at module-init time. */\n set(fn: () => T): void;\n /** Read through to the registered getter, or throw the client-side message. */\n get(): T;\n}\n\nfunction createAlsSlot<T>(message: string): AlsSlot<T> {\n let fn: (() => T) | undefined;\n return {\n set(next) {\n fn = next;\n },\n get() {\n if (!fn) throw new Error(message);\n return fn();\n },\n };\n}\n\nconst searchParamsSlot = createAlsSlot<URLSearchParams>(\n '[timber] searchParams.get() is only available on the server. ' +\n 'Use searchParams.useQueryStates() on the client.'\n);\n\nconst segmentParamsSlot = createAlsSlot<CoercedParams>(\n '[timber] segmentParams.get() is only available on the server. ' +\n 'Use segmentParams.useSegmentParams() on the client.'\n);\n\n/** @internal Called from `server/request-context.ts` at module init. */\nexport const _setGetSearchParamsFn = searchParamsSlot.set;\n/** @internal Read by `search-params/define.ts` inside `.get()`. */\nexport const getSearchParamsFromAls = searchParamsSlot.get;\n\n/** @internal Called from `server/request-context.ts` at module init. */\nexport const _setGetSegmentParamsFn = segmentParamsSlot.set;\n/** @internal Read by `segment-params/define.ts` inside `.get()`. */\nexport const getSegmentParamsFromAls = segmentParamsSlot.get;\n"],"mappings":";AA0CA,SAAS,cAAiB,SAA6B;CACrD,IAAI;CACJ,OAAO;EACL,IAAI,MAAM;GACR,KAAK;EACP;EACA,MAAM;GACJ,IAAI,CAAC,IAAI,MAAM,IAAI,MAAM,OAAO;GAChC,OAAO,GAAG;EACZ;CACF;AACF;AAEA,IAAM,mBAAmB,cACvB,+GAEF;AAEA,IAAM,oBAAoB,cACxB,mHAEF;;AAGA,IAAa,wBAAwB,iBAAiB;;AAEtD,IAAa,yBAAyB,iBAAiB;;AAGvD,IAAa,yBAAyB,kBAAkB;AAEjB,kBAAkB"}
|
|
@@ -268,7 +268,8 @@ async function buildStaticDirectoryOutput(config, buildDir, outDir, publicDirNam
|
|
|
268
268
|
await mkdir(outDir, { recursive: true });
|
|
269
269
|
const publicDir = join(outDir, publicDirName);
|
|
270
270
|
await mkdir(publicDir, { recursive: true });
|
|
271
|
-
|
|
271
|
+
const clientDir = join(buildDir, "client");
|
|
272
|
+
await cp(clientDir, publicDir, {
|
|
272
273
|
recursive: true,
|
|
273
274
|
filter: (src) => {
|
|
274
275
|
if (config.clientJavascriptDisabled && src.endsWith(".js")) return false;
|
|
@@ -366,4 +367,4 @@ async function collectFiles(dir) {
|
|
|
366
367
|
//#endregion
|
|
367
368
|
export { IMMUTABLE_CACHE as a, writeHeadersFile as i, findUnhashedStaticFiles as n, runSharedBuildSteps as r, buildStaticDirectoryOutput as t };
|
|
368
369
|
|
|
369
|
-
//# sourceMappingURL=build-output-helper-
|
|
370
|
+
//# sourceMappingURL=build-output-helper-9dqCv1pZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-output-helper-9dqCv1pZ.js","names":[],"sources":["../../src/adapters/shared.ts","../../src/adapters/build-output-helper.ts"],"sourcesContent":["// Shared adapter utilities — leaf module with zero deps on server/, client/,\n// or any Vite-dependent code.\n//\n// Adapters are loaded by Node at Vite startup time, before Vite's module\n// resolver is available. This module uses only Node built-ins and can be\n// imported by both adapters via a relative path.\n//\n// IMPORTANT: Do NOT add imports from ../server/, ../client/, ../plugins/,\n// or any module that transitively depends on Vite. A test in\n// tests/adapters/shared-no-forbidden-imports.test.ts enforces this.\n// ../shared/ is exempt: it is dependency-free by the same rule.\n\nimport { withBasePath } from '../shared/base-path.ts';\n\n/**\n * The deployment base an adapter should write platform rules against.\n *\n * `config.base` is optional on `TimberConfig` because adapters can be invoked\n * by callers that predate it (a hand-written adapter, a test harness). Falling\n * back to `'/'` reproduces the pre-TIM-1274 output exactly, which is correct\n * for the only deployments those callers can have: a build under a non-root\n * base always goes through `plugins/adapter-build.ts`, which populates it.\n *\n * Typed structurally rather than as `TimberConfig` so this module keeps no\n * import of `./types.js` at runtime.\n */\nexport function adapterBase(config: { base?: string }): string {\n return config.base ?? '/';\n}\n\n// ─── Static asset cache headers ──────────────────────────────────────────\n\n/** Cache-Control value for hashed (immutable) assets. */\nexport const IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\n\n/** Cache-Control value for unhashed static assets. */\nexport const STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\n/**\n * Generate a `_headers` file for static asset cache control.\n *\n * The `_headers` file is a platform convention supported by Cloudflare Workers\n * Static Assets, Cloudflare Pages, and Netlify. It maps URL patterns to\n * HTTP response headers.\n *\n * Only hashed (immutable) asset paths are listed here by default. When\n * `unhashedPaths` is provided, per-file rules are appended for unhashed\n * static assets (favicon.ico, robots.txt, etc.) so they get Cache-Control\n * on platforms like Cloudflare Workers Static Assets where the server\n * handler never sees static file requests.\n *\n * A `/*` catch-all is NOT used — it would be additive on Cloudflare,\n * producing duplicate Cache-Control directives on hashed assets\n * (invalid per RFC 9111).\n *\n * **Every rule path is written in deployed-URL space** (TIM-1274). The\n * platform matches these patterns against the request path, which under a\n * non-root `base` sits below the prefix: with `base: '/repo/'` the assets are\n * at `/repo/assets/*`, so a rule saying `/assets/*` matches nothing. Silent\n * for the cache rules (slower, still correct) and not silent for the\n * Content-Type rules — an extensionless API route output is unreadable on\n * Cloudflare Workers Static Assets without one.\n *\n * The prefix is applied at exactly one point, `rulePath()`, which every rule\n * in this function goes through. Prefixing the three wildcards and\n * hand-checking that the per-file rules agree is how the next rule gets\n * missed.\n */\n// Cloudflare Workers Static Assets caps _headers at 100 rules.\n// Reserve 3 for the wildcard hashed-asset rules (/assets/*, /_timber/fonts/*, /_rsc/*).\nconst MAX_PER_FILE_RULES = 97;\n\nexport interface HeadersFileOptions {\n /**\n * The deployment base, already normalized by `normalizeBasePath()` —\n * leading and trailing slash, `'/'` at the domain root.\n *\n * Required rather than defaulted: a caller that forgets it would emit\n * root-relative rules that match nothing, which is the bug this closes.\n */\n base: string;\n /** Unhashed static files (route-space paths) needing per-file cache rules. */\n unhashedPaths?: string[];\n /** Route-space path → Content-Type, for extensionless API route outputs. */\n contentTypes?: Record<string, string>;\n /**\n * Verbatim contents of the user's `public/_headers`, appended after the\n * generated rules. See `mergeUserHeaders` for why it goes last.\n */\n userRules?: string;\n}\n\n/**\n * Count the rules in a `_headers` file — a rule is a path line, which is any\n * line that is neither blank, a comment, nor an indented header.\n *\n * @internal Exported for testing.\n */\nexport function countHeaderRules(content: string): number {\n return content\n .split('\\n')\n .filter((line) => line.trim() !== '' && !line.startsWith('#') && !/^\\s/.test(line)).length;\n}\n\n/** One `_headers` block: a path pattern and the headers it sets. */\nexport interface HeaderRule {\n pattern: string;\n headers: Array<{ name: string; value: string }>;\n}\n\n/**\n * Parse a `_headers` file. A non-indented, non-comment line opens a rule; the\n * indented `Name: value` lines beneath it belong to that rule.\n *\n * @internal Exported for testing.\n */\nexport function parseHeaderRules(content: string): HeaderRule[] {\n const rules: HeaderRule[] = [];\n for (const raw of content.split('\\n')) {\n const line = raw.replace(/\\r$/, '');\n if (line.trim() === '' || line.trimStart().startsWith('#')) continue;\n if (/^\\s/.test(line)) {\n const current = rules[rules.length - 1];\n if (!current) continue; // header with no rule above it — not ours to fix\n const colon = line.indexOf(':');\n if (colon === -1) continue;\n current.headers.push({\n name: line.slice(0, colon).trim(),\n value: line.slice(colon + 1).trim(),\n });\n } else {\n rules.push({ pattern: line.trim(), headers: [] });\n }\n }\n return rules;\n}\n\n/**\n * Whether a user pattern can match paths a generated pattern also matches.\n *\n * Deliberately narrow: equality, or a prefix-glob (`/x/*`, `/*`) whose literal\n * prefix contains the other's. Anything cleverer risks claiming an overlap\n * that is not there, and the only thing this drives is a warning — a false\n * positive is noise a developer cannot act on.\n */\nfunction patternsOverlap(a: string, b: string): boolean {\n if (a === b) return true;\n const prefixOf = (p: string) => (p.endsWith('*') ? p.slice(0, -1) : null);\n const [pa, pb] = [prefixOf(a), prefixOf(b)];\n if (pa !== null && (b.startsWith(pa) || pa === '')) return true;\n if (pb !== null && (a.startsWith(pb) || pb === '')) return true;\n return false;\n}\n\nexport function generateHeadersFile({\n base,\n unhashedPaths,\n contentTypes,\n userRules,\n}: HeadersFileOptions): string {\n /** Route-space rule path → deployed-URL rule path. */\n const rulePath = (path: string) => withBasePath(base, path);\n\n // Cloudflare and Netlify apply *every* matching rule, so emitting a header\n // the user also sets on the same path yields two definitions of it — a\n // comma-joined `Cache-Control` that is invalid per RFC 9111, and neither\n // side's intent. This module already refuses to emit a `/*` catch-all for\n // that reason; appending the user's file verbatim reintroduced it through\n // the back door (TIM-1326).\n //\n // An explicit app rule beats a framework default, so where the user names\n // the same pattern and the same header, ours is dropped. Only exact pattern\n // matches are resolved this way: silently dropping asset immutability\n // because a `/*` rule *might* subsume it would be a worse surprise than the\n // duplicate, so overlapping-but-different patterns warn instead.\n const userRuleList = userRules ? parseHeaderRules(userRules) : [];\n const overriddenExactly = new Set(\n userRuleList.flatMap((rule) =>\n rule.headers.map((h) => `${rule.pattern}\\0${h.name.toLowerCase()}`)\n )\n );\n const overridden = (pattern: string, header: string) =>\n overriddenExactly.has(`${pattern}\\0${header.toLowerCase()}`);\n\n const warnedOverlaps = new Set<string>();\n const emit = (pattern: string, header: string, value: string): string => {\n if (overridden(pattern, header)) return '';\n for (const rule of userRuleList) {\n if (rule.pattern === pattern) continue;\n if (!patternsOverlap(rule.pattern, pattern)) continue;\n for (const h of rule.headers) {\n if (h.name.toLowerCase() !== header.toLowerCase()) continue;\n const key = `${rule.pattern}\\0${pattern}\\0${header.toLowerCase()}`;\n if (warnedOverlaps.has(key)) continue;\n warnedOverlaps.add(key);\n console.warn(\n `[timber] _headers: your rule \"${rule.pattern}\" sets ${header}, which ` +\n `timber also sets on \"${pattern}\". Cloudflare and Netlify apply both, ` +\n `so the response gets two ${header} values. Use the exact pattern ` +\n `\"${pattern}\" to replace timber's rule, or narrow yours so they do ` +\n `not overlap.`\n );\n }\n }\n return `\\n${pattern}\\n ${header}: ${value}\\n`;\n };\n\n let content = `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n`;\n for (const path of ['/assets/*', '/_timber/fonts/*', '/_rsc/*']) {\n content += emit(rulePath(path), 'Cache-Control', IMMUTABLE_CACHE);\n }\n\n // Content-Type rules are higher priority than cache headers for\n // extensionless files — without them the file is unreadable.\n const allCtEntries = contentTypes\n ? Object.entries(contentTypes).sort(([a], [b]) => a.localeCompare(b))\n : [];\n const ctEntries = allCtEntries.slice(0, MAX_PER_FILE_RULES);\n if (allCtEntries.length > MAX_PER_FILE_RULES) {\n console.warn(\n `[timber] _headers: ${allCtEntries.length} content-type rules exceed ` +\n `Cloudflare's 100-rule limit. Only the first ${MAX_PER_FILE_RULES} ` +\n `extensionless API routes will get Content-Type headers.`\n );\n }\n const remainingBudget = MAX_PER_FILE_RULES - ctEntries.length;\n\n if (unhashedPaths && unhashedPaths.length > 0) {\n const capped = unhashedPaths.slice(0, Math.max(0, remainingBudget));\n if (unhashedPaths.length > remainingBudget) {\n console.warn(\n `[timber] _headers: ${unhashedPaths.length} unhashed static files + ` +\n `${ctEntries.length} content-type rules found, but Cloudflare limits ` +\n `_headers to 100 rules. Only the first ${capped.length} unhashed ` +\n `files will get Cache-Control headers.`\n );\n }\n // Section bodies are built before their comment header so a section the\n // user has overridden entirely does not leave a heading over nothing.\n const body = capped.map((p) => emit(rulePath(p), 'Cache-Control', STATIC_CACHE)).join('');\n if (body !== '') {\n content += `\\n# Unhashed static assets — short-lived cache with revalidation\\n${body}`;\n }\n }\n\n const ctBody = ctEntries\n .map(([urlPath, ct]) => emit(rulePath(urlPath), 'Content-Type', ct))\n .join('');\n if (ctBody !== '') {\n content += `\\n# Content-Type for extensionless API routes\\n${ctBody}`;\n }\n\n // User rules last, matching the `_redirects` merge. Their paths are left\n // verbatim: `_redirects` prefixes the rules it generates because it wrote\n // them in route space, but these are the user's own text, already authored\n // against the URLs the site is deployed at.\n if (userRules && userRules.trim() !== '') {\n content += `\\n# User-authored rules from public/_headers\\n\\n${userRules.trim()}\\n`;\n\n const total = countHeaderRules(content);\n if (total > MAX_PER_FILE_RULES + 3) {\n console.warn(\n `[timber] _headers: ${total} rules after merging public/_headers, over ` +\n `Cloudflare's ${MAX_PER_FILE_RULES + 3}-rule limit. Rules past the ` +\n `limit are dropped by the platform — remove some, or move static ` +\n `assets under a directory the wildcard rules already cover.`\n );\n }\n }\n\n return content;\n}\n","// Shared build-output helper for adapter buildOutput() methods.\n//\n// Steps 1–5 of every adapter's buildOutput are platform-agnostic:\n// 1. Copy client assets to the public/static directory\n// 2. Write _headers file for static asset cache control\n// 3. Copy RSC + SSR server bundles\n// 4. Write manifest-init module (if present)\n//\n// Only the final steps (entry codegen, platform config) vary per adapter.\n//\n// IMPORTANT: This module must remain a leaf — no imports from ../server/,\n// ../client/, or Vite-dependent code. See adapters/shared.ts header.\n\nimport { writeFile, mkdir, cp, rm, readdir, readFile } from 'node:fs/promises';\nimport { join, basename, relative } from 'node:path';\nimport { adapterBase, generateHeadersFile } from './shared.ts';\n// Isomorphic, zero-import module — safe to load from an adapter, which Node's\n// native ESM loader evaluates before Vite's resolver exists. Same precedent as\n// `adapters/shared.ts` importing `../shared/base-path.js`.\nimport {\n HEADERS_FILE,\n USER_MERGED_PLATFORM_FILES,\n USER_PLATFORM_DIR,\n} from '../shared/static-platform-files.ts';\nimport { detectFsIdentity, fsKey } from './fs-identity.ts';\nimport type { TimberConfig } from './types.ts';\n\n/** Options for the shared build output steps. */\nexport interface BuildOutputBaseOptions {\n /** Resolved timber config. */\n config: TimberConfig;\n /** Root build directory (e.g. `.timber/build`). */\n buildDir: string;\n /** Adapter-specific output directory (e.g. `.timber/build/nitro`). */\n outDir: string;\n /**\n * Name of the public/static directory within outDir.\n * Nitro uses 'public', Cloudflare uses 'static'.\n */\n publicDirName: string;\n}\n\n/**\n * Run the platform-agnostic build output steps shared by all adapters:\n *\n * 1. Create the output directory\n * 2. Copy client assets to the public/static directory\n * 3. Write the `_headers` file for static asset cache control\n * 4. Copy RSC and SSR server bundles into the output directory\n * 5. Write the manifest-init module if present\n *\n * Returns the resolved public directory path for further adapter-specific use.\n */\nexport async function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise<string> {\n const { config, buildDir, outDir, publicDirName } = opts;\n\n // 1. Create the output directory.\n await mkdir(outDir, { recursive: true });\n\n // 2. Copy client assets to public/static directory.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const publicDir = join(outDir, publicDirName);\n await mkdir(publicDir, { recursive: true });\n // Find WASM files that are referenced by client JS (legitimate client WASM)\n // vs orphaned server-only WASM (e.g. @takumi-rs for OG image generation).\n // Only strip orphaned WASM to avoid breaking client-side WASM usage.\n const referencedWasm = await findReferencedWasm(clientDir);\n\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: (src: string) => {\n if (src.endsWith('.wasm') && !referencedWasm.has(basename(src))) {\n return false;\n }\n if (config.clientJavascriptDisabled && src.endsWith('.js')) return false;\n return true;\n },\n }).catch((e: NodeJS.ErrnoException) => {\n // Client dir may not exist when client JavaScript is disabled\n if (e.code !== 'ENOENT') throw e;\n });\n\n // 3. Write _headers file for static asset cache control.\n // Scan for unhashed files so they get per-file Cache-Control rules in the\n // _headers file. On platforms like Cloudflare Workers Static Assets, the\n // worker never sees static file requests — _headers is the only mechanism.\n await writeHeadersFile(publicDir, config, await findUnhashedStaticFiles(publicDir));\n\n // 4. Copy RSC + SSR server bundles into the output directory.\n await cp(join(buildDir, 'rsc'), join(outDir, 'rsc'), { recursive: true });\n await cp(join(buildDir, 'ssr'), join(outDir, 'ssr'), { recursive: true }).catch(\n (e: NodeJS.ErrnoException) => {\n // SSR dir may not exist for static-only builds\n if (e.code !== 'ENOENT') throw e;\n }\n );\n\n // 4b. Copy prebuilt/ flight payloads if present (cache.component artifacts).\n // These are produced by the post-build capture pass (design/44) and read\n // at request time via the adapter's PrebuiltPayloadSource.\n await cp(join(buildDir, 'prebuilt'), join(outDir, 'prebuilt'), { recursive: true }).catch(\n (e: NodeJS.ErrnoException) => {\n // No prebuilt dir when the app has no cache.component calls\n if (e.code !== 'ENOENT') throw e;\n }\n );\n\n // 5. Write manifest-init module if present.\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\n }\n\n return publicDir;\n}\n\n/**\n * Find unhashed static files in the public directory that need per-file\n * Cache-Control rules in the `_headers` file.\n *\n * Excludes:\n * - Files under `assets/` and `_timber/` (covered by wildcard rules)\n * - Underscore-prefixed files (`_headers`, `_worker.js`, etc.)\n *\n * @internal Exported for testing.\n */\n// Well-known metadata files that should be prioritized when capping\n// _headers rules, so they're never dropped in favor of less important files.\nconst PRIORITY_FILES = new Set([\n '/favicon.ico',\n '/robots.txt',\n '/sitemap.xml',\n '/manifest.json',\n '/manifest.webmanifest',\n '/apple-touch-icon.png',\n]);\n\nexport async function findUnhashedStaticFiles(publicDir: string): Promise<string[]> {\n let files: string[];\n try {\n files = await collectFiles(publicDir);\n } catch {\n return [];\n }\n\n const priority: string[] = [];\n const rest: string[] = [];\n for (const file of files) {\n const rel = relative(publicDir, file).replace(/\\\\/g, '/');\n if (rel.startsWith('assets/') || rel.startsWith('_timber/')) continue;\n if (rel.startsWith('rsc/') || rel.startsWith('ssr/') || rel.startsWith('_rsc/')) continue;\n if (basename(rel).startsWith('_')) continue;\n const urlPath = '/' + rel;\n if (PRIORITY_FILES.has(urlPath)) {\n priority.push(urlPath);\n } else {\n rest.push(urlPath);\n }\n }\n\n return [...priority.sort(), ...rest.sort()];\n}\n\n/**\n * Scan client JS files for references to .wasm filenames.\n * Returns the set of WASM basenames that are actually imported/referenced\n * by client code and must be preserved in the static output.\n */\nasync function findReferencedWasm(clientDir: string): Promise<Set<string>> {\n const referenced = new Set<string>();\n\n let files: string[];\n try {\n files = await collectFiles(clientDir);\n } catch {\n return referenced;\n }\n\n const jsFiles = files.filter((f) => f.endsWith('.js'));\n const wasmFiles = files.filter((f) => f.endsWith('.wasm')).map((f) => basename(f));\n if (wasmFiles.length === 0) return referenced;\n\n for (const jsFile of jsFiles) {\n const content = await readFile(jsFile, 'utf-8');\n for (const wasmName of wasmFiles) {\n if (content.includes(wasmName)) {\n referenced.add(wasmName);\n }\n }\n }\n\n return referenced;\n}\n\n/**\n * Build a static-only output directory (no server entry). Copies client\n * assets and pre-rendered HTML/RSC/API files into `outDir/publicDirName`.\n * Used by both Cloudflare and Nitro adapters in `output: 'static'` mode.\n */\nexport async function buildStaticDirectoryOutput(\n config: TimberConfig,\n buildDir: string,\n outDir: string,\n publicDirName: string\n): Promise<void> {\n // Clean stale artifacts from prior builds (e.g. server-mode builds that\n // left rsc/, ssr/, _worker.js in the outDir). Without this, a static\n // build after a server build inherits 140+ unused server chunks.\n await rm(outDir, { recursive: true, force: true }).catch(() => {});\n await mkdir(outDir, { recursive: true });\n\n const publicDir = join(outDir, publicDirName);\n await mkdir(publicDir, { recursive: true });\n\n // Copy client assets (JS, CSS, fonts, images).\n const clientDir = join(buildDir, 'client');\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: (src: string) => {\n if (config.clientJavascriptDisabled && src.endsWith('.js')) return false;\n return true;\n },\n }).catch((e: NodeJS.ErrnoException) => {\n if (e.code !== 'ENOENT') throw e;\n });\n\n // Copy pre-rendered static pages (HTML + RSC flight + API routes).\n //\n // This is the one place the build merges two independently-produced trees\n // into a single directory, and `cp` resolves an overlap by overwriting with\n // no diagnostic — the deployment then contains one of the two artifacts and\n // nothing recorded which. Refuse instead (TIM-1266).\n if (config.staticPagesDir) {\n await assertNoTreeCollision(config.staticPagesDir, publicDir);\n await cp(config.staticPagesDir, publicDir, { recursive: true });\n }\n}\n\n/**\n * Write the `_headers` file, folding in the user's `public/_headers`.\n *\n * Every adapter used to `writeFile` a generated `_headers` over whatever\n * Vite had copied out of `public/`, so an app shipping its own CSP, CORS, or\n * cache rules deployed without them and without a word (TIM-1326). The\n * generated rules and the user's are not competitors — they address different\n * paths — so this merges rather than refusing, the way `_redirects` has since\n * TIM-1249.\n *\n * **Read from the project, not the output.** The generated file lands under\n * the same name in the output directory, and `runSharedBuildSteps` does not\n * clean `outDir`; a reader aimed there would fold the previous build's\n * generated rules back in and compound them on every rebuild — the defect\n * this pass fixed for `_redirects`.\n *\n * User rules go last. On Cloudflare and Netlify a `_headers` file applies\n * every matching rule, so appending means the user's rules take effect; where\n * a platform resolves a conflict in favour of the earlier rule instead, the\n * framework keeps the paths it owns and the user keeps the rest. Under either\n * reading this is no worse than the previous behaviour, which was to drop the\n * user's rules entirely.\n *\n * @internal Exported for testing.\n */\nexport async function writeHeadersFile(\n dir: string,\n config: TimberConfig,\n unhashedPaths: string[]\n): Promise<void> {\n const userRules = await readUserPlatformFile(config.root, HEADERS_FILE);\n await writeFile(\n join(dir, HEADERS_FILE),\n generateHeadersFile({\n base: adapterBase(config),\n unhashedPaths,\n contentTypes: config.staticContentTypes,\n userRules: userRules ?? undefined,\n })\n );\n}\n\n/**\n * Read a user-authored platform file from the project's `public/` directory.\n * Returns null when the project has none, or when no root is configured —\n * a caller without a root cannot locate the project at all, and inventing\n * one from `process.cwd()` would read a different project's file.\n */\nasync function readUserPlatformFile(\n root: string | undefined,\n name: string\n): Promise<string | null> {\n if (!root) return null;\n try {\n return await readFile(join(root, USER_PLATFORM_DIR, name), 'utf-8');\n } catch {\n return null;\n }\n}\n\n/**\n * Throw if copying `srcDir` over `destDir` would overwrite an existing file.\n *\n * Compared against the destination **as it stands**, not against the other\n * source tree: the first copy is filtered (client JS is dropped when client\n * JavaScript is disabled), so the files that are actually there are the only\n * ones a second copy can clobber.\n *\n * @internal Exported for testing.\n */\nexport async function assertNoTreeCollision(srcDir: string, destDir: string): Promise<void> {\n const [srcFiles, destFiles, identity] = await Promise.all([\n collectFiles(srcDir).catch(() => [] as string[]),\n collectFiles(destDir).catch(() => [] as string[]),\n // `cp` will resolve these paths the way the destination filesystem does,\n // so the comparison predicting it has to as well: on macOS a hand-written\n // `public/About/index.html` and a generated `about/index.html` are one\n // file, and an exact-string set waves the overwrite through (TIM-1266).\n detectFsIdentity(destDir),\n ]);\n\n const key = (p: string) => fsKey(p, identity);\n const existing = new Set(destFiles.map((f) => key(relative(destDir, f).replace(/\\\\/g, '/'))));\n const exempt = new Set(USER_MERGED_PLATFORM_FILES.map(key));\n const collisions = srcFiles\n .map((f) => relative(srcDir, f).replace(/\\\\/g, '/'))\n .filter((rel) => existing.has(key(rel)) && !exempt.has(key(rel)))\n .sort();\n\n if (collisions.length === 0) return;\n\n throw new Error(\n `[timber] Build output: ${collisions.length} file(s) in the pre-rendered site ` +\n `collide with files already in the output directory. Copying would silently ` +\n `overwrite them — a generated page and a file in public/ cannot share a path:\\n` +\n collisions.map((c) => ` - ${c}`).join('\\n')\n );\n}\n\nasync function collectFiles(dir: string): Promise<string[]> {\n const result: string[] = [];\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n result.push(...(await collectFiles(fullPath)));\n } else {\n result.push(fullPath);\n }\n }\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as addSpanEventSync, I as getWaitUntil, d as logSwrRefetchFailed, h as swallow } from "./logger-
|
|
1
|
+
import { B as addSpanEventSync, I as getWaitUntil, d as logSwrRefetchFailed, h as swallow } from "./logger-pumCm3Il.js";
|
|
2
2
|
import { t as fnv1aHash } from "./fast-hash-C-tVYrLb.js";
|
|
3
3
|
import { t as estimateByteSize } from "./sizeof-lXffQ1Vr.js";
|
|
4
4
|
import { t as warnLossyJsonValue } from "./json-lossy-check-ClNvBM_3.js";
|
|
@@ -490,9 +490,7 @@ var RedisCacheHandler = class {
|
|
|
490
490
|
case "expireGT":
|
|
491
491
|
await this.client.expireGT(op.key, op.seconds);
|
|
492
492
|
break;
|
|
493
|
-
case "del":
|
|
494
|
-
await this.client.del(op.key);
|
|
495
|
-
break;
|
|
493
|
+
case "del": await this.client.del(op.key);
|
|
496
494
|
}
|
|
497
495
|
}
|
|
498
496
|
async invalidate(opts) {
|
|
@@ -654,7 +652,7 @@ var TagAwareCacheHandler = class {
|
|
|
654
652
|
const key = this.tagVersionKey(tag);
|
|
655
653
|
const raw = await this.store.get(key);
|
|
656
654
|
const version = Math.max(raw !== null ? parseInt(raw, 10) : 0, Date.now()) + 1;
|
|
657
|
-
await this.store.set(key, String(version),
|
|
655
|
+
await this.store.set(key, String(version), 31536e3);
|
|
658
656
|
}
|
|
659
657
|
};
|
|
660
658
|
//#endregion
|
|
@@ -1000,9 +998,9 @@ async function lookupPrebuiltPayload(componentId, cacheKey) {
|
|
|
1000
998
|
* See design/45-cache-lifetimes.md §ISR mechanics.
|
|
1001
999
|
*/
|
|
1002
1000
|
/** Default ceiling TTL for tag-only entries (24 hours), in seconds. */
|
|
1003
|
-
var CEILING_TTL_SECONDS =
|
|
1001
|
+
var CEILING_TTL_SECONDS = 86400;
|
|
1004
1002
|
/** Deploy-long physical TTL for seed-backed overlay records (90 days). */
|
|
1005
|
-
var DEPLOY_LONG_TTL_SECONDS =
|
|
1003
|
+
var DEPLOY_LONG_TTL_SECONDS = 7776e3;
|
|
1006
1004
|
function isOverlayRecord(v) {
|
|
1007
1005
|
return v !== null && typeof v === "object" && v._t === "co";
|
|
1008
1006
|
}
|
|
@@ -1193,4 +1191,4 @@ var cache = {
|
|
|
1193
1191
|
//#endregion
|
|
1194
1192
|
export { setDefaultSingleflightTimeout as _, getManifestEntry as a, wasInvalidatedSince as b, lookupPrebuiltPayload as c, checkVersionSkew as d, MemoryCacheHandler as f, setCacheHandler as g, getCacheHandler as h, storeOverlayEntry as i, setPrebuiltPayloadSource as l, RedisCacheHandler as m, lookupOverlay as n, hasPrebuiltPayloadSource as o, TagAwareCacheHandler as p, overlayKey as r, isParamIndependent as s, cache as t, applyReloadHeaders as u, currentInvalidationEpoch as v, createSingleflight as x, lastInvalidationEpochFor as y };
|
|
1195
1193
|
|
|
1196
|
-
//# sourceMappingURL=cache-api-
|
|
1194
|
+
//# sourceMappingURL=cache-api-CQeYzA5g.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-api-CQeYzA5g.js","names":[],"sources":["../../src/cache/stable-stringify.ts","../../src/cache/singleflight.ts","../../src/cache/invalidation-epoch.ts","../../src/cache/timber-cache.ts","../../src/cache/handler-store.ts","../../src/cache/redis-handler.ts","../../src/cache/tag-aware-handler.ts","../../src/cache/index.ts","../../src/server/version-skew.ts","../../src/server/prebuilt/manifest-tag-index.ts","../../src/server/prebuilt/payload-source.ts","../../src/server/prebuilt/overlay.ts","../../src/cache/cache-api.ts"],"sourcesContent":["/**\n * Deterministic JSON serialization with sorted object keys.\n * Used for cache key generation — ensures { a: 1, b: 2 } and { b: 2, a: 1 }\n * produce the same string.\n *\n * Matches JSON.stringify semantics for toJSON: Date serializes to its ISO\n * string, URL to its href, and class instances with a custom toJSON use it.\n * Map and Set serialize their entries explicitly (insertion-order\n * independent). Values that would serialize lossily — functions, symbols,\n * and non-plain objects with no enumerable own state (RegExp, class\n * instances holding data behind getters or internal slots) — throw instead:\n * every such value would collapse to the same cache key, silently returning\n * one argument's cached data for another (TIM-1020).\n */\nexport function stableStringify(value: unknown): string {\n if (value === null || value === undefined) return String(value);\n\n const type = typeof value;\n if (type === 'function' || type === 'symbol') {\n throw new TypeError(\n `stableStringify: cannot serialize a ${type} into a stable cache key — ` +\n 'all such values would collide. Provide an explicit `key` option to cache().'\n );\n }\n if (type !== 'object') return JSON.stringify(value);\n\n if (Array.isArray(value)) {\n return '[' + value.map((item) => stableStringify(item)).join(',') + ']';\n }\n\n // Honor toJSON like JSON.stringify does (Date → ISO string, URL → href,\n // custom class serialization). JSON.stringify does not re-invoke toJSON\n // on the value it returns, so a toJSON that returns `this` falls through\n // to plain own-property serialization instead of recursing forever.\n const withToJSON = value as { toJSON?: () => unknown };\n if (typeof withToJSON.toJSON === 'function') {\n const json = withToJSON.toJSON();\n if (json !== value) return stableStringify(json);\n }\n\n // Map/Set have no enumerable own properties — serialize entries explicitly.\n // Entries are sorted after serialization so insertion order doesn't change\n // the key. The Map(/Set( prefixes can't collide with object ('{'), array\n // ('['), or string ('\"') output.\n if (value instanceof Map) {\n const entries = [...value.entries()].map(\n ([k, v]) => stableStringify(k) + '=>' + stableStringify(v)\n );\n return 'Map(' + entries.sort().join(',') + ')';\n }\n if (value instanceof Set) {\n const members = [...value].map((member) => stableStringify(member));\n return 'Set(' + members.sort().join(',') + ')';\n }\n\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n const pairs: string[] = [];\n for (const key of keys) {\n if (obj[key] === undefined) continue;\n // A callable own toJSON only reaches this loop via the returns-`this`\n // fall-through above. It's the serialization protocol, not data —\n // JSON.stringify omits it too.\n if (key === 'toJSON' && typeof obj[key] === 'function') continue;\n pairs.push(JSON.stringify(key) + ':' + stableStringify(obj[key]));\n }\n\n // A non-plain object that serializes to '{}' carries its state behind\n // getters or internal slots (RegExp, sessions, ORM entities). Serializing\n // it would give every instance the same cache key.\n if (pairs.length === 0) {\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) {\n const name = (value as object).constructor?.name || 'Object';\n throw new TypeError(\n `stableStringify: ${name} instance has no serializable own state — ` +\n 'all instances would share one cache key. Provide an explicit `key` ' +\n 'option to cache(), or add a toJSON() method.'\n );\n }\n }\n\n return '{' + pairs.join(',') + '}';\n}\n","/**\n * Singleflight coalesces concurrent calls with the same key into a single\n * execution. All callers receive the same result (or error).\n *\n * Per-process, in-memory. Each process coalesces independently.\n *\n * An optional `timeoutMs` prevents hung `fn()` calls from permanently\n * blocking all future callers for that key. When set, `fn()` is raced\n * against a timeout — if the timeout fires first, the promise rejects\n * with `SingleflightTimeoutError`, `finally` cleans up the key, and\n * subsequent callers can retry. See TIM-518.\n */\n\nexport interface SingleflightOptions {\n /** Maximum time (ms) a coalesced call may run before being rejected. */\n timeoutMs?: number;\n}\n\nexport interface Singleflight {\n do<T>(key: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T>;\n}\n\n/**\n * Error thrown when a singleflight call exceeds `timeoutMs`.\n * Exported so callers can distinguish timeout from other errors.\n */\nexport class SingleflightTimeoutError extends Error {\n constructor(key: string, timeoutMs: number) {\n super(`Singleflight timeout: key \"${key}\" exceeded ${timeoutMs}ms`);\n this.name = 'SingleflightTimeoutError';\n }\n}\n\nexport function createSingleflight(opts?: SingleflightOptions): Singleflight {\n const inflight = new Map<string, Promise<unknown>>();\n const timeoutMs = opts?.timeoutMs;\n\n return {\n do<T>(key: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {\n const existing = inflight.get(key);\n if (existing) return existing as Promise<T>;\n\n const ac = new AbortController();\n let promise: Promise<T>;\n\n if (timeoutMs != null && timeoutMs > 0) {\n // Race fn() against a timeout to prevent hung calls from\n // permanently blocking the key. See TIM-518.\n // Abort the signal on timeout so the callback can skip\n // side effects (e.g. cache writes). See TIM-1092.\n promise = new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n ac.abort();\n reject(new SingleflightTimeoutError(key, timeoutMs));\n }, timeoutMs);\n try {\n fn(ac.signal).then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (err) => {\n clearTimeout(timer);\n reject(err);\n }\n );\n } catch (err) {\n clearTimeout(timer);\n reject(err);\n }\n });\n } else {\n promise = fn(ac.signal);\n }\n\n const tracked = promise.finally(() => {\n inflight.delete(key);\n });\n\n inflight.set(key, tracked);\n return tracked as Promise<T>;\n },\n };\n}\n","/**\n * Per-process invalidation epoch tracking for timber.cache (TIM-1028).\n *\n * handler.set for a cache miss runs after `fn()` resolves. An explicit\n * `cache.invalidate({ tag })` / `revalidateTag` that lands while fn is in\n * flight would otherwise be silently undone — the just-invalidated value\n * re-stored as fresh for the full TTL, resurrecting pre-mutation data.\n *\n * Every invalidation bumps a monotonic epoch and records it against the\n * invalidated key/tag. Before storing a result, the cache wrapper checks\n * whether its key or any of its tags were invalidated after fn started and\n * skips the write if so. Skipping is always safe: the only cost is one\n * extra cache miss on the next call.\n *\n * Scope: per-process, like the singleflight map. An invalidation issued by\n * another instance against a shared handler (Redis/KV) during fn flight is\n * not observable here — closing that window fully would require store-level\n * versioning, which no CacheHandler backend offers.\n */\n\nlet epoch = 0;\n\n/**\n * Last-invalidated epoch per `key:<k>` / `tag:<t>` entry. Insertion order is\n * epoch order (entries are re-inserted on update), so the oldest entry is\n * always first — pruning evicts lowest epochs.\n */\nconst lastInvalidated = new Map<string, number>();\n\n/**\n * Bound on tracked entries so high-cardinality tag invalidation (e.g.\n * per-user tags) cannot grow this map without limit.\n */\nconst MAX_TRACKED_ENTRIES = 10_000;\n\n/**\n * Highest epoch evicted from the map. Any execution that started at or\n * before this epoch conservatively counts as invalidated — history was\n * truncated, so we can no longer prove it wasn't.\n */\nlet evictedEpochFloor = 0;\n\n/** The current epoch. Capture before running fn(), pass to wasInvalidatedSince. */\nexport function currentInvalidationEpoch(): number {\n return epoch;\n}\n\n/**\n * Record an invalidation. Call BEFORE handler.invalidate() so an execution\n * whose set-check races the invalidation errs toward skipping the write.\n */\nexport function recordInvalidation(opts: { key?: string; tag?: string }): void {\n epoch++;\n if (opts.key) track(`key:${opts.key}`);\n if (opts.tag) track(`tag:${opts.tag}`);\n}\n\nfunction track(entry: string): void {\n lastInvalidated.delete(entry);\n lastInvalidated.set(entry, epoch);\n while (lastInvalidated.size > MAX_TRACKED_ENTRIES) {\n const oldest = lastInvalidated.entries().next().value;\n if (oldest === undefined) break;\n evictedEpochFloor = Math.max(evictedEpochFloor, oldest[1]);\n lastInvalidated.delete(oldest[0]);\n }\n}\n\n/**\n * Whether the key or any tag was invalidated after `startEpoch` (the epoch\n * captured when fn started executing).\n */\nexport function wasInvalidatedSince(startEpoch: number, key: string, tags: string[]): boolean {\n return lastInvalidationEpochFor(key, tags) > startEpoch;\n}\n\n/**\n * The highest epoch at which the key or any of the tags was invalidated\n * (0 if never). Used to scope the singleflight key: callers arriving after\n * an invalidation must not coalesce onto a flight that started before it —\n * they would receive the pre-invalidation result the invalidation was\n * issued to discard.\n */\nexport function lastInvalidationEpochFor(key: string, tags: string[]): number {\n let last = evictedEpochFloor;\n const keyEpoch = lastInvalidated.get(`key:${key}`);\n if (keyEpoch !== undefined && keyEpoch > last) last = keyEpoch;\n for (const tag of tags) {\n const tagEpoch = lastInvalidated.get(`tag:${tag}`);\n if (tagEpoch !== undefined && tagEpoch > last) last = tagEpoch;\n }\n return last;\n}\n","import type { CacheHandler, CacheOptions } from './index.ts';\nimport { stableStringify } from './stable-stringify.ts';\nimport { createSingleflight } from './singleflight.ts';\nimport {\n currentInvalidationEpoch,\n lastInvalidationEpochFor,\n wasInvalidatedSince,\n} from './invalidation-epoch.ts';\nimport { addSpanEventSync } from '../server/tracing.ts';\nimport { logSwrRefetchFailed, swallow } from '../server/logger.ts';\nimport { getWaitUntil } from '../server/waituntil-bridge.ts';\nimport { fnv1aHash } from './fast-hash.ts';\n\nlet defaultSingleflight = createSingleflight();\n\n/**\n * Per-key monotonic generation counter for CAS writes (TIM-1158).\n *\n * Unbounded, like the singleflight map — the key space is the same set of\n * cache keys the handler already tracks. Pruning was removed because it\n * desyncs from MemoryCacheHandler.generations: a pruned key restarts at 1\n * while the handler still holds the old higher generation, causing fresh\n * writes to be rejected as stale.\n */\nconst writeGenerations = new Map<string, number>();\nfunction nextGeneration(key: string): number {\n const gen = (writeGenerations.get(key) ?? 0) + 1;\n writeGenerations.set(key, gen);\n return gen;\n}\n\n/**\n * Set the timeout for the module-level default singleflight.\n * Called at framework boot with renderTimeoutMs from timber.config.ts so that\n * cache calls without a per-call timeoutMs still get deadlock protection.\n * See design/06-caching.md §\"Singleflight Timeout — Deadlock Risk\".\n */\nexport function setDefaultSingleflightTimeout(timeoutMs: number): void {\n defaultSingleflight = createSingleflight({ timeoutMs });\n}\n\n/**\n * Generate a cache key from function identity and serialized args.\n *\n * Uses FNV-1a (fast non-crypto hash) instead of SHA-256. Cache keys don't\n * need collision resistance — they need speed. The fnId prefix provides\n * namespace isolation; the hash covers the args.\n *\n * See TIM-370 for perf motivation.\n */\nfunction defaultKeyGenerator(fnId: string, args: unknown[]): string {\n const raw = fnId + ':' + stableStringify(args);\n return fnId + ':' + fnv1aHash(raw);\n}\n\n/**\n * Resolve tags from the options — supports static array or function form.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction resolveTags<Fn extends (...args: any[]) => any>(\n opts: CacheOptions<Fn>,\n args: Parameters<Fn>\n): string[] {\n if (!opts.tags) return [];\n if (Array.isArray(opts.tags)) return opts.tags;\n return opts.tags(...args);\n}\n\n/** Per-process count of byte-identical fn sources seen by deriveFallbackFnId. */\nconst fallbackIdOccurrences = new Map<string, number>();\nlet hasWarnedFallbackId = false;\n\n/**\n * Derive a fallback fnId for callsites the timber-cache-transform Vite plugin\n * could not statically detect (aliased bindings, user re-export wrappers).\n *\n * The base is fnv1a(fn.toString()) — deterministic across instances and cold\n * starts of the same build, so two *different* functions can never swap fnIds\n * by module-evaluation order (the TIM-997 poisoning scenario that the old\n * process-local counter allowed; see TIM-1054). A per-process occurrence\n * suffix disambiguates byte-identical duplicate sources. That residue is the\n * one remaining hazard: closures produced by the same factory share source\n * text but capture different values, and their occurrence order may differ\n * across instances — such callsites need an explicit `key` option.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction deriveFallbackFnId(fn: (...args: any[]) => any): string {\n const sourceHash = fnv1aHash(fn.toString());\n const occurrence = fallbackIdOccurrences.get(sourceHash) ?? 0;\n fallbackIdOccurrences.set(sourceHash, occurrence + 1);\n\n if (!hasWarnedFallbackId && typeof process !== 'undefined' && process.env?.NODE_ENV !== 'test') {\n hasWarnedFallbackId = true;\n console.warn(\n '[timber] cache() callsite has no build-time stable ID and no explicit `key` option — ' +\n 'the call is not statically visible to the timber Vite plugin (e.g. an aliased ' +\n 'binding like `const c = cache` or a re-export wrapper module). Falling back to a ' +\n 'function-source hash for the cache key. With a shared CacheHandler (Redis/KV) this ' +\n 'is safe for distinct functions, but byte-identical closures from a factory can swap ' +\n 'cache entries across instances. Provide an explicit `key` option to make the cache ' +\n 'key contract stable and auditable.'\n );\n }\n\n return `timber-cache:fn:${sourceHash}:${occurrence}`;\n}\n\n/**\n * Creates a cached wrapper around an async function.\n *\n * - FNV-1a default keys with normalized JSON args\n * - Singleflight: concurrent misses → single execution AND single\n * handler.set — the write happens inside the coalesced callback (TIM-1028)\n * - Writes are skipped when the key/tags were invalidated while fn was in\n * flight, so explicit invalidation is never silently undone (TIM-1028)\n * - SWR: serve stale immediately, background refetch\n * - Tags as string[] or function of args\n * - The handler is resolved per invocation via `getHandler`, never captured\n * at wrapper-creation time — module-scope cache() wrappers created before\n * the framework's boot wiring calls setCacheHandler() still use the\n * configured handler once it arrives (TIM-1029)\n * - No ALS dependency for correctness — cache() works outside a request\n * context. The waitUntil ALS bridge is read optionally so the SWR\n * background refetch survives handler return on Workers/Lambda.\n *\n * Cache hits/misses are recorded as OTEL span events on the enclosing\n * span (not child spans). The DevSpanProcessor reads these for dev log output.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function createCache<Fn extends (...args: any[]) => Promise<any>>(\n fn: Fn,\n opts: CacheOptions<Fn>,\n getHandler: () => CacheHandler,\n stableId?: string\n): Fn {\n // With an explicit `key` option the fnId never reaches key generation, so\n // no fallback derivation (or warning) is needed in that case.\n const fnId = stableId ?? (opts.key ? undefined : deriveFallbackFnId(fn));\n\n // Per-call timeoutMs gets a dedicated singleflight instance. Otherwise\n // resolve defaultSingleflight per invocation (not captured at creation\n // time) so wrappers created before boot pick up the timeout once\n // setDefaultSingleflightTimeout is called (TIM-1032, same rationale as\n // getCacheHandler for TIM-1029).\n const perCallSf =\n opts.timeoutMs !== undefined ? createSingleflight({ timeoutMs: opts.timeoutMs }) : undefined;\n\n // Cast to Fn to preserve the original function's generic call signature.\n // Without this, generic type parameters (e.g. <T> in apiFetch<T>) are\n // erased and callers lose type safety on the return type.\n return (async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {\n const sf = perCallSf ?? defaultSingleflight;\n const key = opts.key ? opts.key(...args) : defaultKeyGenerator(fnId as string, args);\n\n const cacheStart = performance.now();\n const cached = await getHandler().get(key);\n\n if (cached && !cached.stale) {\n // Record as OTEL span event on enclosing span (not a child span).\n // Fire-and-forget — no microtask overhead on the cache hot path.\n addSpanEventSync('timber.cache.hit', {\n key,\n duration_ms: Math.round(performance.now() - cacheStart),\n });\n return cached.value as Awaited<ReturnType<Fn>>;\n }\n\n // From here fn() will execute (SWR refetch or miss). Scope the\n // singleflight key by the last invalidation epoch affecting this\n // key/tags: a caller arriving after an invalidation must not coalesce\n // onto a flight that started before it — it would receive the\n // pre-invalidation result the invalidation was issued to discard.\n // NUL separator: cannot collide with user-provided key strings.\n const tags = resolveTags(opts, args);\n const flightKey = `${key}\\u0000${lastInvalidationEpochFor(key, tags)}`;\n\n /**\n * Run fn and store the result — exactly once per coalesced execution,\n * inside the singleflight callback. The write is skipped when:\n * - the key/tags were invalidated after fn started (TIM-1028), or\n * - the singleflight timed out (signal aborted) — the timed-out\n * flight's write could overwrite a newer value from a subsequent\n * flight (TIM-1092), or\n * - the handler rejects the write because a newer generation has\n * already been stored (TIM-1158 CAS guard).\n */\n const executeAndStore = async (signal: AbortSignal): Promise<Awaited<ReturnType<Fn>>> => {\n const startEpoch = currentInvalidationEpoch();\n const generation = nextGeneration(key);\n const result = await fn(...args);\n if (!signal.aborted && !wasInvalidatedSince(startEpoch, key, tags)) {\n try {\n await getHandler().set(key, result, { ttl: opts.ttl, tags, generation });\n } catch (err) {\n // Cache-write failure must not fail the request (TIM-1046).\n // JSON-serializing handlers (Redis, KV) throw on BigInt; network\n // errors can surface from any shared handler. The result is valid\n // — serve it uncached rather than rejecting a successful fn().\n swallow(err, `cache set failed for key \"${key}\"`);\n }\n }\n return result;\n };\n\n if (cached && cached.stale && opts.staleWhileRevalidate) {\n // Record stale cache hit as OTEL span event (fire-and-forget).\n addSpanEventSync('timber.cache.hit', {\n key,\n duration_ms: Math.round(performance.now() - cacheStart),\n stale: true,\n });\n // Serve stale immediately, trigger background refetch\n const refetch = sf\n .do(`swr:${flightKey}`, async (signal) => {\n try {\n await executeAndStore(signal);\n } catch (err) {\n // Failed refetch — stale entry continues to be served.\n logSwrRefetchFailed({ cacheKey: key, error: err });\n }\n })\n .catch(() => {\n // Singleflight promise rejection handled — stale continues.\n });\n // Platforms that cancel work when the handler returns (Cloudflare\n // Workers, Lambda) would otherwise kill the refetch mid-flight —\n // stale data re-served forever. Register the (never-rejecting)\n // refetch with the request's waitUntil; undefined outside a request\n // context or on platforms without lifecycle extension (Node servers\n // keep floating promises alive, so behavior there is unchanged).\n getWaitUntil()?.(refetch);\n return cached.value as Awaited<ReturnType<Fn>>;\n }\n\n // Cache miss (or stale without SWR) — execute with singleflight.\n // The handler.set lives inside the coalesced callback, so N concurrent\n // misses produce exactly one write (TIM-1028).\n const result = await sf.do(flightKey, executeAndStore);\n\n // Record cache miss as OTEL span event (fire-and-forget).\n addSpanEventSync('timber.cache.miss', {\n key,\n duration_ms: Math.round(performance.now() - cacheStart),\n });\n\n return result;\n }) as unknown as Fn;\n}\n","/**\n * Module-level cache handler singleton.\n *\n * Lazily initialized to MemoryCacheHandler on first access. The framework\n * replaces this at boot from timber.config.ts via setCacheHandler().\n *\n * This module avoids importing from ./index to prevent circular dependencies.\n */\n\n// Inline the interface to avoid circular import with index.ts\ninterface CacheHandlerLike {\n get(key: string): Promise<{ value: unknown; stale: boolean } | null>;\n set(key: string, value: unknown, opts: { ttl: number; tags: string[] }): Promise<void>;\n invalidate(opts: { key?: string; tag?: string }): Promise<void>;\n}\n\nlet handler: CacheHandlerLike | null = null;\n\nlet explicitlyConfigured = false;\n\n/** Replace the active cache handler. Called by the framework at boot. */\nexport function setCacheHandler(h: CacheHandlerLike): void {\n handler = h;\n explicitlyConfigured = true;\n}\n\n/** Whether the handler was explicitly configured (vs the default in-memory fallback). */\nexport function isCacheHandlerConfigured(): boolean {\n return explicitlyConfigured;\n}\n\n/**\n * Get the active cache handler. Creates a default MemoryCacheHandler on\n * first access if none has been set via setCacheHandler().\n */\nexport function getCacheHandler(): CacheHandlerLike {\n if (!handler) {\n // Inline a minimal LRU cache to avoid circular dep with index.ts.\n // In production, the framework always calls setCacheHandler() at boot.\n handler = createDefaultHandler();\n }\n return handler;\n}\n\nfunction createDefaultHandler(): CacheHandlerLike {\n const store = new Map<string, { value: unknown; expiresAt: number; tags: string[] }>();\n const maxEntries = 1000;\n\n return {\n async get(key) {\n const entry = store.get(key);\n if (!entry) return null;\n store.delete(key);\n store.set(key, entry);\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n },\n async set(key, value, opts) {\n if (store.has(key)) store.delete(key);\n while (store.size >= maxEntries) {\n const oldest = store.keys().next().value;\n if (oldest !== undefined) store.delete(oldest);\n else break;\n }\n store.set(key, { value, expiresAt: Date.now() + opts.ttl * 1000, tags: opts.tags });\n },\n async invalidate(opts) {\n if (opts.key) store.delete(opts.key);\n if (opts.tag) {\n for (const [key, entry] of store) {\n if (entry.tags.includes(opts.tag)) store.delete(key);\n }\n }\n },\n };\n}\n","import type { CacheHandler } from './index.ts';\nimport { warnLossyJsonValue } from './json-lossy-check.ts';\n\n/**\n * Minimal Redis client contract for `RedisCacheHandler`.\n *\n * This is deliberately NOT the native shape of any client library. ioredis,\n * node-redis v4, and @upstash/redis all disagree on TTL argument shapes\n * (positional `'EX'` vs `{ EX }` vs `{ ex }`), method casing (`sadd` vs\n * `sAdd`), and array handling in `del` — a contract that pattern-matches one\n * client lets another silently drop the expiry (TIM-1030: node-redis ignored\n * the positional `'EX'` and wrote keys with no TTL). Instead, TTL semantics\n * are explicit in the signature and you adapt your client with a thin\n * wrapper, so a mismatch is a type error rather than silent unbounded\n * keyspace growth.\n *\n * @example ioredis\n * ```ts\n * import Redis from 'ioredis';\n * const redis = new Redis(process.env.REDIS_URL);\n * const client: RedisClient = {\n * get: (k) => redis.get(k),\n * set: (k, v, ttl) => redis.set(k, v, 'EX', ttl),\n * expireNX: (k, s) => redis.expire(k, s, 'NX'),\n * expireGT: (k, s) => redis.expire(k, s, 'GT'),\n * del: (k) => redis.del(k),\n * sadd: (k, ...m) => redis.sadd(k, ...m),\n * srem: (k, ...m) => redis.srem(k, ...m),\n * smembers: (k) => redis.smembers(k),\n * ttl: (k) => redis.ttl(k),\n * execMulti: async (ops) => {\n * const p = redis.multi();\n * for (const op of ops) {\n * switch (op.op) {\n * case 'set': p.set(op.key, op.value, 'EX', op.ttlSeconds); break;\n * case 'sadd': p.sadd(op.key, ...op.members); break;\n * case 'expireNX': p.expire(op.key, op.seconds, 'NX'); break;\n * case 'expireGT': p.expire(op.key, op.seconds, 'GT'); break;\n * case 'del': p.del(op.key); break;\n * }\n * }\n * await p.exec();\n * },\n * };\n * ```\n *\n * @example node-redis v4\n * ```ts\n * import { createClient } from 'redis';\n * const redis = createClient({ url: process.env.REDIS_URL });\n * await redis.connect();\n * const client: RedisClient = {\n * get: (k) => redis.get(k),\n * set: (k, v, ttl) => redis.set(k, v, { EX: ttl }),\n * expireNX: (k, s) => redis.expire(k, s, 'NX'),\n * expireGT: (k, s) => redis.expire(k, s, 'GT'),\n * del: (k) => redis.del(k),\n * sadd: (k, ...m) => redis.sAdd(k, m),\n * srem: (k, ...m) => redis.sRem(k, m),\n * smembers: (k) => redis.sMembers(k),\n * ttl: (k) => redis.ttl(k),\n * };\n * ```\n *\n * @example @upstash/redis\n * ```ts\n * import { Redis } from '@upstash/redis';\n * // automaticDeserialization must be off: the handler stores JSON strings\n * // and parses them itself — auto-parsing would hand back objects.\n * const redis = new Redis({ url, token, automaticDeserialization: false });\n * const client: RedisClient = {\n * get: (k) => redis.get(k),\n * set: (k, v, ttl) => redis.set(k, v, { ex: ttl }),\n * expireNX: (k, s) => redis.expire(k, s, 'NX'),\n * expireGT: (k, s) => redis.expire(k, s, 'GT'),\n * del: (k) => redis.del(k),\n * sadd: (k, ...m) => redis.sadd(k, m[0], ...m.slice(1)),\n * srem: (k, ...m) => redis.srem(k, m[0], ...m.slice(1)),\n * smembers: (k) => redis.smembers(k),\n * ttl: (k) => redis.ttl(k),\n * };\n * ```\n */\nexport type RedisWriteOp =\n | { op: 'set'; key: string; value: string; ttlSeconds: number }\n | { op: 'sadd'; key: string; members: string[] }\n | { op: 'expireNX'; key: string; seconds: number }\n | { op: 'expireGT'; key: string; seconds: number }\n | { op: 'del'; key: string };\n\nexport interface RedisClient {\n /** `GET key` — the stored string, or null when absent. */\n get(key: string): Promise<string | null>;\n /**\n * `SET key value EX ttlSeconds` — store with an expiry. The TTL is a\n * required positional argument so no adapter can forget it.\n */\n set(key: string, value: string, ttlSeconds: number): Promise<unknown>;\n /**\n * `EXPIRE key seconds NX` — set the TTL only if the key has no existing\n * expiry (Redis 7.0+). Used to set the initial TTL on new tag sets.\n */\n expireNX(key: string, seconds: number): Promise<unknown>;\n /**\n * `EXPIRE key seconds GT` — update the TTL only if the new value is greater\n * than the current one (Redis 7.0+). Used to extend tag set TTLs without\n * allowing a short-TTL writer to shrink them.\n */\n expireGT(key: string, seconds: number): Promise<unknown>;\n /** `DEL key` — delete a single key. Multi-key deletes loop per key. */\n del(key: string): Promise<unknown>;\n /** `SADD key member [member ...]` — add members to a set. */\n sadd(key: string, ...members: string[]): Promise<unknown>;\n /** `SREM key member [member ...]` — remove members from a set. */\n srem(key: string, ...members: string[]): Promise<unknown>;\n /** `SMEMBERS key` — all members of a set. */\n smembers(key: string): Promise<string[]>;\n /**\n * `TTL key` — remaining time-to-live in seconds. Returns -2 if the key does\n * not exist, -1 if the key exists but has no expiry.\n */\n ttl(key: string): Promise<number>;\n /**\n * Execute a batch of write operations atomically via `MULTI/EXEC`.\n * When provided, `set()` uses this to make tag-index writes and the data\n * write atomic — no `invalidate({tag})` can interleave. Optional: when\n * absent, operations run sequentially (correct but not race-free).\n */\n execMulti?(ops: RedisWriteOp[]): Promise<void>;\n}\n\nconst KEY_PREFIX = 'timber:cache:';\nconst TAG_PREFIX = 'timber:tag:';\n\n/**\n * Redis-backed CacheHandler for distributed caching.\n *\n * All instances sharing the same Redis see each other's cache entries and\n * invalidations. Tag-based invalidation uses Redis Sets to track which keys\n * belong to which tags.\n *\n * Bring your own Redis client wrapped to the {@link RedisClient} contract —\n * see the adapter examples on the interface for ioredis, node-redis v4, and\n * @upstash/redis.\n */\nexport class RedisCacheHandler implements CacheHandler {\n private client: RedisClient;\n private prefix: string;\n\n constructor(client: RedisClient, opts?: { prefix?: string }) {\n this.client = client;\n this.prefix = opts?.prefix ?? '';\n }\n\n private cacheKey(key: string): string {\n return `${this.prefix}${KEY_PREFIX}${key}`;\n }\n\n private tagKey(tag: string): string {\n return `${this.prefix}${TAG_PREFIX}${tag}`;\n }\n\n async get(key: string): Promise<{ value: unknown; stale: boolean } | null> {\n const raw = await this.client.get(this.cacheKey(key));\n if (raw === null) return null;\n\n const entry = JSON.parse(raw) as { value: unknown; expiresAt: number };\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n async set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ): Promise<void> {\n warnLossyJsonValue(value, key);\n const ck = this.cacheKey(key);\n const expiresAt = Date.now() + opts.ttl * 1000;\n\n // Serialize upfront so BigInt/non-serializable errors throw before any\n // writes — prevents tag indexes pointing at stale data from a prior set().\n const payload = JSON.stringify({ value, expiresAt, tags: opts.tags });\n\n // Redis TTL with generous margin beyond the logical TTL to allow SWR reads\n // on stale entries. The logical staleness is determined by expiresAt.\n // We use 2x TTL + 60s as the Redis expiry so stale entries remain\n // available for SWR background refetches.\n const redisTtlSeconds = Math.max(opts.ttl * 2 + 60, 120);\n\n const writeOps: RedisWriteOp[] = [];\n for (const tag of opts.tags) {\n writeOps.push({ op: 'sadd', key: this.tagKey(tag), members: [key] });\n writeOps.push({ op: 'expireNX', key: this.tagKey(tag), seconds: redisTtlSeconds });\n writeOps.push({ op: 'expireGT', key: this.tagKey(tag), seconds: redisTtlSeconds });\n }\n writeOps.push({ op: 'set', key: ck, value: payload, ttlSeconds: redisTtlSeconds });\n\n if (this.client.execMulti) {\n await this.client.execMulti(writeOps);\n } else {\n for (const op of writeOps) {\n await this.execOp(op);\n }\n }\n }\n\n private async execOp(op: RedisWriteOp): Promise<void> {\n switch (op.op) {\n case 'set':\n await this.client.set(op.key, op.value, op.ttlSeconds);\n break;\n case 'sadd':\n await this.client.sadd(op.key, ...op.members);\n break;\n case 'expireNX':\n await this.client.expireNX(op.key, op.seconds);\n break;\n case 'expireGT':\n await this.client.expireGT(op.key, op.seconds);\n break;\n case 'del':\n await this.client.del(op.key);\n break;\n }\n }\n\n async invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n if (opts.key) {\n const raw = await this.client.get(this.cacheKey(opts.key));\n if (raw !== null) {\n const entry = JSON.parse(raw) as { tags?: string[] };\n if (entry.tags) {\n await Promise.all(entry.tags.map((tag) => this.client.srem(this.tagKey(tag), opts.key!)));\n }\n }\n await this.client.del(this.cacheKey(opts.key));\n }\n\n if (opts.tag) {\n const tk = this.tagKey(opts.tag);\n const keys = await this.client.smembers(tk);\n\n // Re-check each member before deleting — the tag set can contain stale\n // memberships from entries whose tags changed since they were added.\n // Mirrors TagAwareCacheHandler's eager strategy (tag-aware-handler.ts:183-198).\n await Promise.all(\n keys.map(async (k) => {\n const raw = await this.client.get(this.cacheKey(k));\n if (raw === null) {\n await this.client.srem(tk, k);\n return;\n }\n const entry = JSON.parse(raw) as { tags?: string[] };\n if (entry.tags?.includes(opts.tag!)) {\n await this.client.del(this.cacheKey(k));\n }\n await this.client.srem(tk, k);\n })\n );\n\n // Clean up the now-empty tag set so it doesn't consume memory\n const remaining = await this.client.smembers(tk);\n if (remaining.length === 0) {\n await this.client.del(tk);\n }\n }\n }\n}\n","/**\n * TagAwareCacheHandler — wraps any CacheStore and adds tag-based invalidation.\n *\n * Three strategies, selected by store capabilities (best available wins):\n *\n * 1. **Native** (store has setWithTags + invalidateTag): Delegates entirely.\n * The store owns tag indexing. Used for Vercel Runtime Cache, DO-backed\n * stores, or any platform with first-class tag support.\n *\n * 2. **Eager** (store has sadd/smembers/srem/sdel): Maintains an inverted\n * index via atomic set operations. Invalidation enumerates the set and\n * deletes entries. Used for Redis.\n *\n * 3. **Lazy** (bare get/set/del only): Version-stamps entries with tag\n * counters. Invalidation bumps a counter (O(1)). Reads validate stored\n * versions against current. Race-free, correct under eventual consistency.\n * Used for Cloudflare KV.\n *\n * Implements the CacheHandler interface consumed by timber-cache.ts.\n */\n\nimport type { CacheHandler } from './index.ts';\nimport type { CacheStore } from './store.ts';\n\nconst DATA_PREFIX = 'd:';\nconst TAG_SET_PREFIX = 'ts:';\nconst TAG_VERSION_PREFIX = 'tv:';\n\ntype Strategy = 'native' | 'eager' | 'lazy';\n\n/** Stored entry format for eager strategy. */\ninterface EagerEntry {\n value: unknown;\n expiresAt: number;\n tags: string[];\n}\n\n/** Stored entry format for lazy (version-stamp) strategy. */\ninterface LazyEntry {\n value: unknown;\n expiresAt: number;\n tags: string[];\n tagVersions: Record<string, number>;\n}\n\nfunction detectStrategy(store: CacheStore): Strategy {\n if (store.setWithTags && store.invalidateTag) return 'native';\n if (store.sadd && store.smembers && store.srem && store.sdel) return 'eager';\n return 'lazy';\n}\n\nexport class TagAwareCacheHandler implements CacheHandler {\n private store: CacheStore;\n private prefix: string;\n private strategy: Strategy;\n\n constructor(store: CacheStore, opts?: { prefix?: string }) {\n this.store = store;\n this.prefix = opts?.prefix ?? 'timber:';\n this.strategy = detectStrategy(store);\n }\n\n get consistency(): 'strong' | 'eventual' | 'local' {\n return this.store.consistency;\n }\n\n private dataKey(key: string): string {\n return `${this.prefix}${DATA_PREFIX}${key}`;\n }\n\n /** Namespace tags for native stores so prefix isolates shared stores. */\n private prefixTags(tags: string[]): string[] {\n return tags.map((t) => `${this.prefix}${t}`);\n }\n\n private tagSetKey(tag: string): string {\n return `${this.prefix}${TAG_SET_PREFIX}${tag}`;\n }\n\n private tagVersionKey(tag: string): string {\n return `${this.prefix}${TAG_VERSION_PREFIX}${tag}`;\n }\n\n async get(key: string): Promise<{ value: unknown; stale: boolean } | null> {\n const raw = await this.store.get(this.dataKey(key));\n if (raw === null) return null;\n\n if (this.strategy === 'native' || this.strategy === 'eager') {\n const entry = JSON.parse(raw) as EagerEntry;\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n // Lazy: validate tag versions before returning\n const entry = JSON.parse(raw) as LazyEntry;\n if (entry.tags.length > 0) {\n const currentVersions = await this.getTagVersions(entry.tags);\n for (const tag of entry.tags) {\n const stored = entry.tagVersions[tag] ?? 0;\n const current = currentVersions[tag] ?? 0;\n if (current > stored) return null;\n }\n }\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n async set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ): Promise<void> {\n const physicalTtl = Math.max(opts.ttl * 2 + 60, 120);\n\n if (this.strategy === 'native') {\n const entry: EagerEntry = {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n };\n const payload = JSON.stringify(entry);\n if (opts.tags.length > 0) {\n await this.store.setWithTags!(\n this.dataKey(key),\n payload,\n physicalTtl,\n this.prefixTags(opts.tags)\n );\n } else {\n await this.store.set(this.dataKey(key), payload, physicalTtl);\n }\n return;\n }\n\n if (this.strategy === 'eager') {\n const entry: EagerEntry = {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n };\n await this.store.set(this.dataKey(key), JSON.stringify(entry), physicalTtl);\n if (opts.tags.length > 0) {\n await Promise.all(\n opts.tags.map((tag) => this.store.sadd!(this.tagSetKey(tag), [key], physicalTtl))\n );\n }\n return;\n }\n\n // Lazy: snapshot current tag versions into the entry\n const tagVersions = opts.tags.length > 0 ? await this.getTagVersions(opts.tags) : {};\n const entry: LazyEntry = {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n tagVersions,\n };\n await this.store.set(this.dataKey(key), JSON.stringify(entry), physicalTtl);\n }\n\n async invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n if (opts.key) {\n if (this.strategy === 'eager') {\n const raw = await this.store.get(this.dataKey(opts.key));\n if (raw !== null) {\n const entry = JSON.parse(raw) as EagerEntry;\n if (entry.tags.length > 0) {\n await Promise.all(\n entry.tags.map((tag) => this.store.srem!(this.tagSetKey(tag), [opts.key!]))\n );\n }\n }\n }\n await this.store.del(this.dataKey(opts.key));\n }\n\n if (opts.tag) {\n const tag = opts.tag;\n if (this.strategy === 'native') {\n await this.store.invalidateTag!(`${this.prefix}${tag}`);\n } else if (this.strategy === 'eager') {\n const members = await this.store.smembers!(this.tagSetKey(tag));\n // Verify each member still carries the tag before deleting — the set\n // can contain stale memberships from overwritten entries or LRU eviction.\n // Remove only the snapshot members (via srem, not sdel) so concurrent\n // writes that add new members between smembers() and cleanup are preserved.\n await Promise.all(\n members.map(async (k) => {\n const raw = await this.store.get(this.dataKey(k));\n if (raw === null) {\n await this.store.srem!(this.tagSetKey(tag), [k]);\n return;\n }\n const entry = JSON.parse(raw) as EagerEntry;\n if (entry.tags.includes(tag)) {\n await this.store.del(this.dataKey(k));\n await this.store.srem!(this.tagSetKey(tag), [k]);\n } else {\n await this.store.srem!(this.tagSetKey(tag), [k]);\n }\n })\n );\n } else {\n await this.bumpTagVersion(opts.tag);\n }\n }\n }\n\n // --- Lazy strategy helpers ---\n\n private async getTagVersions(tags: string[]): Promise<Record<string, number>> {\n const versions: Record<string, number> = {};\n await Promise.all(\n tags.map(async (tag) => {\n const raw = await this.store.get(this.tagVersionKey(tag));\n versions[tag] = raw !== null ? parseInt(raw, 10) : 0;\n })\n );\n return versions;\n }\n\n private async bumpTagVersion(tag: string): Promise<void> {\n const key = this.tagVersionKey(tag);\n const raw = await this.store.get(key);\n const stored = raw !== null ? parseInt(raw, 10) : 0;\n // max(stored, now) + 1: strictly advancing even within the same\n // millisecond (two bumps in 1ms produce N, N+1 not N, N). On\n // eventually-consistent stores the read can be stale, but Date.now()\n // provides a floor — regression only if BOTH the read is stale AND the\n // clock is behind, bounded by the store's propagation window (~60s).\n const version = Math.max(stored, Date.now()) + 1;\n // Version keys must outlive all data entries stamped with them. Data\n // entries use physical TTL of max(ttl*2+60, 120); 365 days exceeds any\n // practical cache lifetime.\n await this.store.set(key, String(version), 365 * 86400);\n }\n}\n","// @timber-js/app/cache — Caching primitives\n\nimport { estimateByteSize } from './sizeof.ts';\n\nexport interface CacheHandler {\n get(key: string): Promise<{ value: unknown; stale: boolean } | null>;\n set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ): Promise<void>;\n invalidate(opts: { key?: string; tag?: string }): Promise<void>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface CacheOptions<Fn extends (...args: any[]) => any> {\n ttl: number;\n key?: (...args: Parameters<Fn>) => string;\n staleWhileRevalidate?: boolean;\n tags?: string[] | ((...args: Parameters<Fn>) => string[]);\n /** Timeout (ms) for singleflight-coalesced calls. Prevents hung fn() from\n * permanently blocking all future callers for the same cache key. See TIM-518. */\n timeoutMs?: number;\n}\n\nexport interface MemoryCacheHandlerOptions {\n /** Maximum number of entries. Oldest accessed entries are evicted first. Default: 1000. */\n maxEntries?: number;\n /**\n * @deprecated Use `maxEntries` instead. Will be removed in a future release.\n * Alias for `maxEntries` — maximum number of entries (not bytes).\n */\n maxSize?: number;\n /** Maximum total byte budget for all cached values. Oldest entries are evicted when exceeded. Default: no limit. */\n maxBytes?: number;\n /** Maximum byte size for a single cache entry. Entries exceeding this are silently dropped. Default: no limit. */\n maxEntryBytes?: number;\n}\n\nexport class MemoryCacheHandler implements CacheHandler {\n private store = new Map<\n string,\n { value: unknown; expiresAt: number; tags: string[]; byteSize: number }\n >();\n private generations = new Map<string, number>();\n private maxEntries: number;\n private maxBytes: number | undefined;\n private maxEntryBytes: number | undefined;\n private currentBytes = 0;\n private readonly _trackBytes: boolean;\n\n constructor(opts?: MemoryCacheHandlerOptions) {\n // maxEntries takes precedence over deprecated maxSize\n this.maxEntries = opts?.maxEntries ?? opts?.maxSize ?? 1000;\n this.maxBytes = opts?.maxBytes;\n this.maxEntryBytes = opts?.maxEntryBytes;\n this._trackBytes = this.maxBytes !== undefined || this.maxEntryBytes !== undefined;\n }\n\n async get(key: string) {\n const entry = this.store.get(key);\n if (!entry) return null;\n\n // Move to end of Map (most recently used) for LRU ordering\n this.store.delete(key);\n this.store.set(key, entry);\n\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n async set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ) {\n // CAS guard: skip write if a newer generation has already been written\n if (opts.generation !== undefined) {\n const current = this.generations.get(key) ?? 0;\n if (opts.generation < current) return;\n }\n\n const byteSize = this._trackBytes ? estimateByteSize(value) : 0;\n\n // Reject entries exceeding per-entry byte limit\n if (this.maxEntryBytes !== undefined && byteSize > this.maxEntryBytes) {\n return;\n }\n\n // If key already exists, delete first to refresh insertion order and reclaim bytes\n if (this.store.has(key)) {\n const existing = this.store.get(key)!;\n this.currentBytes -= existing.byteSize;\n this.store.delete(key);\n }\n\n // Evict oldest entries (front of Map) if at entry count capacity\n while (this.store.size >= this.maxEntries) {\n this.evictOldest();\n }\n\n // Evict oldest entries if byte budget would be exceeded\n if (this.maxBytes !== undefined) {\n while (this.currentBytes + byteSize > this.maxBytes && this.store.size > 0) {\n this.evictOldest();\n }\n // If the single entry exceeds the total byte budget, don't store it\n if (this.currentBytes + byteSize > this.maxBytes) {\n return;\n }\n }\n\n // Record generation only after all admission checks pass\n if (opts.generation !== undefined) {\n this.generations.set(key, opts.generation);\n }\n\n this.store.set(key, {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n byteSize,\n });\n this.currentBytes += byteSize;\n }\n\n async invalidate(opts: { key?: string; tag?: string }) {\n if (opts.key) {\n const entry = this.store.get(opts.key);\n if (entry) {\n this.currentBytes -= entry.byteSize;\n this.store.delete(opts.key);\n }\n this.generations.delete(opts.key);\n }\n if (opts.tag) {\n for (const [key, entry] of this.store) {\n if (entry.tags.includes(opts.tag)) {\n this.currentBytes -= entry.byteSize;\n this.store.delete(key);\n this.generations.delete(key);\n }\n }\n }\n }\n\n /** Number of entries currently in the cache. */\n get size(): number {\n return this.store.size;\n }\n\n /** Estimated total byte size of all cached values. Only tracked when maxBytes or maxEntryBytes is configured; returns 0 otherwise. */\n get bytes(): number {\n return this.currentBytes;\n }\n\n /** Evict the oldest entry (front of Map). */\n private evictOldest(): void {\n const oldest = this.store.keys().next().value;\n if (oldest !== undefined) {\n const entry = this.store.get(oldest)!;\n this.currentBytes -= entry.byteSize;\n this.store.delete(oldest);\n this.generations.delete(oldest);\n }\n }\n}\n\nexport { RedisCacheHandler } from './redis-handler.ts';\nexport type { RedisClient, RedisWriteOp } from './redis-handler.ts';\nexport { cache } from './cache-api.ts';\nexport { setCacheHandler, getCacheHandler } from './handler-store.ts';\nexport { setDefaultSingleflightTimeout } from './timber-cache.ts';\n// NOTE: registerCachedFunction (runtime for 'use cache' directive) removed.\n// Future feature pending design doc. See design/06-caching.md.\nexport { estimateByteSize } from './sizeof.ts';\n\n// --- Cache Store layering (design/46) ---\nexport { TagAwareCacheHandler } from './tag-aware-handler.ts';\nexport type { CacheStore } from './store.ts';\n// stableStringify, createSingleflight, and SingleflightTimeoutError are\n// internal utilities used by the cache implementation. They are not\n// re-exported here — import directly from the source files if needed\n// within the package. See TIM-720.\n","/**\n * Version Skew Detection — graceful recovery when stale clients hit new deployments.\n *\n * When a new version of the app is deployed, clients with open tabs still have\n * the old JavaScript bundle. Without version skew handling, these stale clients\n * will experience:\n *\n * 1. Server action calls that crash (action IDs are content-hashed)\n * 2. Chunk load failures (old filenames gone from CDN)\n * 3. RSC payload mismatches (component references differ between builds)\n *\n * This module implements deployment ID comparison:\n * - A per-build deployment ID is generated at build time (see build-manifest.ts)\n * - The client sends it via `X-Timber-Deployment-Id` header on every RSC/action request\n * - The server compares it against the current build's ID\n * - On mismatch: signal the client to reload (not crash)\n *\n * The deployment ID is always-on in production. Dev mode skips the check\n * (HMR handles code updates without full reloads).\n *\n * See design/25-production-deployments.md, TIM-446\n */\n\n// ─── Constants ───────────────────────────────────────────────────\n\n/** Header sent by the client with every RSC/action request. */\nexport const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';\n\n/** Response header that signals the client to do a full page reload. */\nexport const RELOAD_HEADER = 'X-Timber-Reload';\n\n// ─── Deployment ID ───────────────────────────────────────────────\n\n/**\n * The current build's deployment ID. Set at startup from the manifest init\n * module (globalThis.__TIMBER_DEPLOYMENT_ID__). Null in dev mode.\n */\nlet currentDeploymentId: string | null = null;\n\n/**\n * Set the current deployment ID. Called once at server startup from the\n * manifest init module. In dev mode this is never called (deployment ID\n * checks are skipped).\n */\nexport function setDeploymentId(id: string): void {\n currentDeploymentId = id;\n}\n\n/**\n * Get the current deployment ID. Returns null in dev mode.\n */\nexport function getDeploymentId(): string | null {\n return currentDeploymentId;\n}\n\n// ─── Skew Detection ──────────────────────────────────────────────\n\n/** Result of a version skew check. */\nexport interface SkewCheckResult {\n /** Whether the client's deployment ID matches the server's. */\n ok: boolean;\n /** The client's deployment ID (null if header not sent — e.g., initial page load). */\n clientId: string | null;\n}\n\n/**\n * Check if a request's deployment ID matches the current build.\n *\n * Returns `{ ok: true }` when:\n * - Dev mode (no deployment ID set — HMR handles updates)\n * - No deployment ID header (initial page load, non-RSC request)\n * - Deployment IDs match\n *\n * Returns `{ ok: false }` when:\n * - Client sends a deployment ID that differs from the current build\n */\nexport function checkVersionSkew(req: Request): SkewCheckResult {\n // Dev mode — no deployment ID checks (HMR handles updates)\n if (!currentDeploymentId) {\n return { ok: true, clientId: null };\n }\n\n const clientId = req.headers.get(DEPLOYMENT_ID_HEADER);\n\n // No header — initial page load or non-RSC request. Always OK.\n if (!clientId) {\n return { ok: true, clientId: null };\n }\n\n // Compare deployment IDs\n if (clientId === currentDeploymentId) {\n return { ok: true, clientId };\n }\n\n return { ok: false, clientId };\n}\n\n/**\n * Apply version skew reload headers to a response.\n * Sets X-Timber-Reload: 1 to signal the client to do a full page reload.\n */\nexport function applyReloadHeaders(headers: Headers): void {\n headers.set(RELOAD_HEADER, '1');\n}\n","/**\n * Manifest tag index — maps tags to seed cache keys for tombstone writes.\n *\n * When `revalidateTag(tag)` fires, the framework must write tombstones for\n * every build-seed entry carrying that tag. The manifest records tags per\n * entry; this module inverts the index at startup so the lookup is O(1) per\n * tag instead of a full manifest scan per invalidation.\n *\n * The index is built lazily on first access and cached for the process\n * lifetime — the manifest is immutable within a deploy.\n *\n * See design/45-cache-lifetimes.md §ISR mechanics.\n */\n\nimport type { PrebuiltManifest } from './payload-source.ts';\n\nexport interface SeedKeyInfo {\n componentId: string;\n cacheKey: string;\n}\n\n/**\n * Build the inverted tag → seed key index from the manifest.\n *\n * Returns a Map where each tag maps to the set of (componentId, cacheKey)\n * pairs whose manifest entries carry that tag. Only entries with non-empty\n * `tags` arrays participate.\n */\nexport function buildTagIndex(manifest: PrebuiltManifest): Map<string, SeedKeyInfo[]> {\n const index = new Map<string, SeedKeyInfo[]>();\n for (const [componentId, component] of Object.entries(manifest)) {\n if (!component || typeof component !== 'object' || !('entries' in component)) continue;\n for (const [cacheKey, entry] of Object.entries(component.entries)) {\n if (!entry.tags || entry.tags.length === 0) continue;\n for (const tag of entry.tags) {\n let list = index.get(tag);\n if (!list) {\n list = [];\n index.set(tag, list);\n }\n list.push({ componentId, cacheKey });\n }\n }\n }\n return index;\n}\n","/**\n * Prebuilt payload source — the adapter seam through which the runtime\n * reads build-time flight artifacts (`.timber/dist/prebuilt/`).\n *\n * The runtime never touches the filesystem directly: adapters register a\n * source at startup (Node: fs reads; Cloudflare: the assets binding —\n * design/44-render-at-build-time.md §Adapter Integration). Artifacts are\n * produced by the post-build capture pass (TIM-1119, prebuilt-builder.ts);\n * adapter registration of the source is TIM-1122. Until a source is\n * registered (adapter not wired yet, dev mode, no artifacts built), every\n * lookup misses and cache.component wrappers render live — the designed\n * dynamic-fallback behavior.\n *\n * Phase 3 (design/45 runtime tier) inserts the CacheHandler overlay lookup\n * in front of `lookupPrebuiltPayload` — overlay first, then artifact. The\n * single-lookup-function shape here is that seam.\n */\n\nimport { initTagIndex, resetTagIndex } from './overlay.ts';\nimport type { CoercedParams } from '../../shared/param-value.ts';\n\nexport interface PrebuiltManifestEntry {\n /** Segment params this entry was captured under. */\n params: CoercedParams;\n /** Payload path relative to the prebuilt/ directory. */\n file: string;\n /** Payload size in bytes (build report / diagnostics). */\n bytes?: number;\n /** Runtime invalidation tags (design/45 ISR tier). */\n tags?: string[];\n /** Epoch ms when this entry was captured (seed freshness for ISR). */\n buildTimestamp?: number;\n}\n\nexport interface PrebuiltManifest {\n [componentId: string]: {\n entries: Record<string, PrebuiltManifestEntry>;\n /** When true, the component does not read segment params — the cache key uses `{}`. */\n paramIndependent?: boolean;\n };\n}\n\nexport interface PrebuiltPayloadSource {\n /** Load prebuilt/manifest.json. Null when no artifacts exist. */\n loadManifest(): Promise<PrebuiltManifest | null>;\n /** Read a `.flight` payload by manifest file path. Null when missing. */\n readPayload(file: string): Promise<Uint8Array | null>;\n}\n\nlet source: PrebuiltPayloadSource | null = null;\nlet manifestPromise: Promise<PrebuiltManifest | null> | null = null;\n\n/**\n * Ceiling on the memoized manifest load. Without it, a stalled\n * `loadManifest()` would leave `manifestPromise` pending forever and every\n * subsequent cache.component render would await the same hung promise —\n * the coalesced-promise deadlock class from design/13-security.md (A5).\n * On expiry the load memoizes as null (permanent dynamic fallback, same\n * policy as a failed load) and callers proceed immediately.\n */\nconst MANIFEST_LOAD_TIMEOUT_MS = 5_000;\n\n/**\n * Per-read ceiling on payload reads. The read is awaited inside a server\n * component render — a stalled adapter promise would otherwise hang the\n * RSC stream until the render timeout kills the whole page, when the\n * correct degradation is a miss → live render (codex review round 4 on\n * PR #835).\n */\nconst PAYLOAD_READ_TIMEOUT_MS = 5_000;\n\n/** Race a source promise against a timeout that resolves to `fallback`. */\nasync function withTimeout<T>(\n work: Promise<T>,\n timeoutMs: number,\n fallback: T,\n onTimeout: () => void\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n work,\n new Promise<T>((resolve) => {\n timer = setTimeout(() => {\n onTimeout();\n resolve(fallback);\n }, timeoutMs);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function loadManifestWithTimeout(\n src: PrebuiltPayloadSource\n): Promise<PrebuiltManifest | null> {\n let manifest: PrebuiltManifest | null;\n try {\n manifest = await withTimeout(src.loadManifest(), MANIFEST_LOAD_TIMEOUT_MS, null, () => {\n console.error(\n `[timber] prebuilt manifest load exceeded ${MANIFEST_LOAD_TIMEOUT_MS}ms — all cache.component lookups will render dynamically`\n );\n });\n } catch (error) {\n console.error(\n '[timber] failed to load prebuilt manifest — all cache.component lookups will miss:',\n error\n );\n return null;\n }\n if (manifest) {\n initTagIndex(manifest);\n }\n return manifest;\n}\n\n/**\n * Register the payload source. Called once at server startup by the\n * adapter/entry wiring (TIM-1122); tests inject fakes. Passing null\n * unregisters (and drops the memoized manifest).\n */\nexport function setPrebuiltPayloadSource(next: PrebuiltPayloadSource | null): void {\n source = next;\n manifestPromise = null;\n if (!next) resetTagIndex();\n}\n\n/**\n * Whether a source is registered — the gate for both marker emission and\n * splice-transform installation. False means zero prebuilt overhead.\n */\nexport function hasPrebuiltPayloadSource(): boolean {\n return source !== null;\n}\n\n/**\n * Ensure the manifest is loaded (and the tag index initialized). No-op\n * when no source is registered. Used by the tombstone path to avoid\n * skipping tombstones on cold processes where the first request is an\n * action rather than a page render.\n */\nexport async function ensureManifestLoaded(): Promise<void> {\n if (source === null) return;\n manifestPromise ??= loadManifestWithTimeout(source);\n await manifestPromise;\n}\n\n/**\n * Look up a manifest entry (metadata only, no payload read).\n * Used by the overlay to check seed existence and freshness.\n */\nexport async function isParamIndependent(componentId: string): Promise<boolean> {\n if (source === null) return false;\n manifestPromise ??= loadManifestWithTimeout(source);\n const manifest = await manifestPromise;\n return manifest?.[componentId]?.paramIndependent === true;\n}\n\nexport async function getManifestEntry(\n componentId: string,\n cacheKey: string\n): Promise<PrebuiltManifestEntry | null> {\n if (source === null) return null;\n manifestPromise ??= loadManifestWithTimeout(source);\n const manifest = await manifestPromise;\n return manifest?.[componentId]?.entries?.[cacheKey] ?? null;\n}\n\n/**\n * Look up a cached payload: manifest entry → payload bytes.\n * Null on any miss or read failure — callers fall back to live render.\n *\n * The manifest is loaded once and memoized (design/44 §manifest.json:\n * loaded at startup, held in memory; payloads read on demand). A failed\n * manifest load is memoized as null rather than retried per-request — a\n * broken artifact should degrade to dynamic rendering, not add a failing\n * read to every request.\n */\nexport async function lookupPrebuiltPayload(\n componentId: string,\n cacheKey: string\n): Promise<Uint8Array | null> {\n if (source === null) return null;\n manifestPromise ??= loadManifestWithTimeout(source);\n const manifest = await manifestPromise;\n const entry = manifest?.[componentId]?.entries?.[cacheKey];\n if (!entry) return null;\n try {\n return await withTimeout(source.readPayload(entry.file), PAYLOAD_READ_TIMEOUT_MS, null, () => {\n console.error(\n `[timber] prebuilt payload read for \"${entry.file}\" exceeded ${PAYLOAD_READ_TIMEOUT_MS}ms — rendering dynamically`\n );\n });\n } catch (error) {\n console.error(\n `[timber] failed to read prebuilt payload \"${entry.file}\" for ${componentId} — rendering dynamically:`,\n error\n );\n return null;\n }\n}\n","/**\n * Component cache overlay — runtime CacheHandler layer for ISR.\n *\n * Sits in front of the immutable build artifact (`.flight` files) and\n * stores replacement payloads and tombstones via the configured\n * CacheHandler. Overlay entries are per-deploy namespaced:\n * `timber:c:{deploymentId}:{componentId}:{hash}`\n *\n * The CacheHandler contract is untouched — tombstones and the tag index\n * live above the interface as value conventions. Redis, KV, and custom\n * handlers work unmodified.\n *\n * See design/45-cache-lifetimes.md §ISR mechanics.\n */\n\nimport { getCacheHandler, isCacheHandlerConfigured } from '../../cache/handler-store.ts';\nimport { MemoryCacheHandler } from '../../cache/index.ts';\nimport { getDeploymentId } from '../version-skew.ts';\nimport { buildTagIndex, type SeedKeyInfo } from './manifest-tag-index.ts';\nimport type { PrebuiltManifest } from './payload-source.ts';\nimport { ensureManifestLoaded } from './payload-source.ts';\n\n/** Default ceiling TTL for tag-only entries (24 hours), in seconds. */\nconst CEILING_TTL_SECONDS = 24 * 60 * 60;\n\n/** Deploy-long physical TTL for seed-backed overlay records (90 days). */\nconst DEPLOY_LONG_TTL_SECONDS = 90 * 24 * 60 * 60;\n\n// ─── Overlay record shapes (value conventions above CacheHandler) ────────\n\n/**\n * A cached component flight payload stored in the overlay.\n * The `_t` discriminant distinguishes this from a tombstone.\n */\nexport interface ComponentOverlayRecord {\n _t: 'co';\n /** Base64-encoded flight payload bytes. */\n p: string;\n /** Logical freshness deadline (Date.now() + ttl * 1000). */\n fu: number;\n /** Whether a build-artifact seed exists for this key. */\n s: boolean;\n}\n\n/**\n * A tombstone marking a build seed as invalidated.\n * Written by revalidateTag for seeds carrying the invalidated tag.\n */\nexport interface ComponentTombstoneRecord {\n _t: 'ct';\n}\n\ntype OverlayValue = ComponentOverlayRecord | ComponentTombstoneRecord;\n\nexport function isOverlayRecord(v: unknown): v is ComponentOverlayRecord {\n return v !== null && typeof v === 'object' && (v as Record<string, unknown>)._t === 'co';\n}\n\nexport function isTombstone(v: unknown): v is ComponentTombstoneRecord {\n return v !== null && typeof v === 'object' && (v as Record<string, unknown>)._t === 'ct';\n}\n\n// ─── Overlay key generation ──────────────────────────────────────────────\n\nexport function overlayKey(componentId: string, cacheKey: string): string {\n const deploymentId = getDeploymentId() ?? 'dev';\n return `timber:c:${deploymentId}:${componentId}:${cacheKey}`;\n}\n\n// ─── Overlay read ────────────────────────────────────────────────────────\n\nexport interface OverlayLookupResult {\n kind: 'fresh' | 'stale' | 'tombstone';\n /** The overlay value (null for tombstones). */\n record: ComponentOverlayRecord | null;\n /** Raw flight payload bytes (decoded from base64). Null for tombstones. */\n payload: Uint8Array | null;\n}\n\n/**\n * Look up an overlay entry in the cache handler.\n * Returns null on a clean miss (no overlay entry at all).\n */\nexport async function lookupOverlay(\n componentId: string,\n cacheKey: string\n): Promise<OverlayLookupResult | null> {\n const handler = getCacheHandler();\n const key = overlayKey(componentId, cacheKey);\n const result = await handler.get(key);\n if (!result) return null;\n\n const value = result.value as OverlayValue;\n if (isTombstone(value)) {\n return { kind: 'tombstone', record: null, payload: null };\n }\n if (isOverlayRecord(value)) {\n const logicallyStale = Date.now() > value.fu;\n return {\n kind: logicallyStale ? 'stale' : 'fresh',\n record: value,\n payload: base64ToBytes(value.p),\n };\n }\n // Unknown value shape — treat as miss (forward compatibility).\n return null;\n}\n\n// ─── Overlay write ───────────────────────────────────────────────────────\n\n/**\n * Resolve the effective TTL for a component's runtime lifetime.\n * Tag-only entries (no explicit ttl) get the ceiling TTL (design/45\n * §Tag-only entries carry a ceiling TTL).\n */\nexport function effectiveTtl(ttl: number | undefined): number {\n return ttl ?? CEILING_TTL_SECONDS;\n}\n\n/**\n * Store a component flight payload in the overlay.\n */\nexport async function storeOverlayEntry(\n componentId: string,\n cacheKey: string,\n payloadBytes: Uint8Array,\n opts: {\n ttl?: number;\n tags: string[];\n hasSeed: boolean;\n }\n): Promise<void> {\n const handler = getCacheHandler();\n const key = overlayKey(componentId, cacheKey);\n const logicalTtl = effectiveTtl(opts.ttl);\n const physicalTtl = opts.hasSeed ? DEPLOY_LONG_TTL_SECONDS : logicalTtl;\n\n const record: ComponentOverlayRecord = {\n _t: 'co',\n p: bytesToBase64(payloadBytes),\n fu: Date.now() + logicalTtl * 1000,\n s: opts.hasSeed,\n };\n\n await handler.set(key, record, { ttl: physicalTtl, tags: opts.tags });\n}\n\n// ─── Tombstone writes ────────────────────────────────────────────────────\n\nlet tagIndex: Map<string, SeedKeyInfo[]> | null = null;\n\n/**\n * Initialize the tag index from the prebuilt manifest. Called once when the\n * manifest is loaded. Without a manifest (no prebuilt components), this is\n * never called and revalidateTag skips the tombstone path entirely.\n */\nexport function initTagIndex(manifest: PrebuiltManifest): void {\n tagIndex = buildTagIndex(manifest);\n emitIsrDiagnosticIfNeeded(manifest);\n}\n\n/**\n * Get the tag index, if initialized.\n * @internal — exported for testing.\n */\nexport function getTagIndex(): Map<string, SeedKeyInfo[]> | null {\n return tagIndex;\n}\n\n/**\n * Reset the tag index (for tests).\n * @internal\n */\nexport function resetTagIndex(): void {\n tagIndex = null;\n}\n\n/**\n * Write tombstones for all seed entries carrying the given tag.\n * Called from cache.invalidate (cache-api.ts) AFTER the handler's own\n * invalidation clears overlay entries.\n *\n * The tombstone prevents the immutable seed from being served after\n * invalidation — without it, a handler.invalidate that clears the overlay\n * would expose the (now-stale) build artifact on the next request.\n *\n * Ensures the manifest is loaded first so cold-process actions that\n * invalidate before any page render still write tombstones.\n */\nexport async function writeTombstonesForTag(tag: string): Promise<void> {\n if (!tagIndex) {\n await ensureManifestLoaded();\n }\n if (!tagIndex) return;\n const seeds = tagIndex.get(tag);\n if (!seeds || seeds.length === 0) return;\n\n const handler = getCacheHandler();\n const tombstone: ComponentTombstoneRecord = { _t: 'ct' };\n\n await Promise.all(\n seeds.map((seed) => {\n const key = overlayKey(seed.componentId, seed.cacheKey);\n return handler.set(key, tombstone, {\n ttl: DEPLOY_LONG_TTL_SECONDS,\n tags: [tag],\n });\n })\n );\n}\n\n// ─── ISR build diagnostic ────────────────────────────────────────────────\n\nlet isrDiagnosticEmitted = false;\n\nfunction emitIsrDiagnosticIfNeeded(manifest: PrebuiltManifest): void {\n if (isrDiagnosticEmitted) return;\n\n const handler = getCacheHandler();\n const isMemoryBased = handler instanceof MemoryCacheHandler || !isCacheHandlerConfigured();\n if (!isMemoryBased) return;\n\n for (const [, component] of Object.entries(manifest)) {\n if (!component || typeof component !== 'object' || !('entries' in component)) continue;\n for (const [, entry] of Object.entries(component.entries)) {\n if (entry.tags && entry.tags.length > 0) {\n isrDiagnosticEmitted = true;\n console.warn(\n '[timber] tag revalidation of prerendered components requires a shared cache ' +\n 'handler in multi-instance deployments. The current MemoryCacheHandler is ' +\n 'per-process — revalidateTag on one instance will not purge the build seed ' +\n 'on others. Configure a shared handler (Redis, KV) for production ISR.'\n );\n return;\n }\n }\n }\n}\n\n/**\n * Reset the diagnostic flag (for tests).\n * @internal\n */\nexport function resetIsrDiagnostic(): void {\n isrDiagnosticEmitted = false;\n}\n\n// ─── Encoding helpers ────────────────────────────────────────────────────\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64');\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n return new Uint8Array(Buffer.from(b64, 'base64'));\n}\n","import type { CacheOptions } from './index.ts';\nimport type { PrebuiltComponentOptions } from '../server/prebuilt-runtime.ts';\nimport { createCache } from './timber-cache.ts';\nimport { getCacheHandler } from './handler-store.ts';\nimport { recordInvalidation } from './invalidation-epoch.ts';\nimport { writeTombstonesForTag } from '../server/prebuilt/overlay.ts';\nimport { getCdnPurgeHandler } from '../cdn/purge-store.ts';\n\nconst componentFallbackWarned = new WeakSet<object>();\n\n/**\n * Cache namespace — `cache.data`, `cache.component`, `cache.invalidate`.\n *\n * ```ts\n * import { cache } from '@timber-js/app/cache';\n *\n * const getUser = cache.data(\n * async (id: string) => db.users.findUnique({ where: { id } }),\n * { ttl: 60, tags: (id) => [`user:${id}`] }\n * );\n * ```\n */\nexport const cache = {\n /**\n * Wrap an async function with cross-request caching.\n *\n * The configured cache handler (defaults to MemoryCacheHandler, overridable\n * via timber.config.ts) is re-resolved on every call, so module-scope\n * wrappers created before `setCacheHandler()` still pick up the configured\n * handler (TIM-1029).\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n data<Fn extends (...args: any[]) => Promise<any>>(\n fn: Fn,\n opts: CacheOptions<Fn>,\n stableId?: string\n ): Fn {\n return createCache(fn, opts, getCacheHandler, stableId);\n },\n\n /**\n * Invalidate cache entries by tag or key.\n *\n * ```ts\n * cache.invalidate({ tag: 'products' });\n * cache.invalidate({ key: 'user:abc' });\n * ```\n */\n async invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n // Record before the handler delete so a cached fn in flight skips its\n // post-resolve set instead of resurrecting the invalidated value (TIM-1028).\n recordInvalidation(opts);\n await getCacheHandler().invalidate(opts);\n // Write tombstones for build-seed entries carrying the tag (design/45\n // §ISR mechanics). Runs AFTER handler.invalidate so the tombstone\n // overwrites the cleared overlay slot. This covers ALL invalidation\n // paths — actions, webhooks, route handlers — not just executeAction.\n if (opts.tag) {\n await writeTombstonesForTag(opts.tag);\n }\n // Trigger CDN purge if a handler is configured (design/25 §Layer 2).\n // Purge is best-effort: a CDN API failure or timeout must not block\n // cache invalidation. 10s ceiling prevents a hung CDN API from stalling\n // the entire revalidateTag/action flow.\n if (opts.tag) {\n const purgeHandler = getCdnPurgeHandler();\n if (purgeHandler) {\n try {\n await Promise.race([\n purgeHandler.purgeTags([opts.tag]),\n new Promise<void>((_, reject) =>\n setTimeout(() => reject(new Error('CDN purge timed out (10s)')), 10_000)\n ),\n ]);\n } catch (err) {\n console.error('[timber] CDN purge failed for tag:', err);\n }\n }\n }\n },\n\n /**\n * Runtime fallback for `cache.component(...)` callsites the timber-prebuilt\n * transform does not rewrite — nested/computed/argument-position forms (see\n * plugins/prebuilt.ts). Only module-scope `const X = cache.component(...)`\n * declarations are transformed and eligible for prebuilding; everything else\n * lands here, warns once per component, and renders dynamically. Same\n * safety-net pattern as the data cache's runtime fnId fallback (TIM-1054):\n * an untraceable callsite degrades in performance, never in correctness.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n component<C extends (props: any) => unknown>(\n componentFn: C,\n _options?: PrebuiltComponentOptions\n ): C {\n if (!componentFallbackWarned.has(componentFn)) {\n componentFallbackWarned.add(componentFn);\n const name = componentFn.name || 'anonymous component';\n console.warn(\n `[timber] cache.component: \"${name}\" was not transformed at build time — ` +\n `rendering dynamically. Only module-scope declarations ` +\n `(const X = cache.component(...)) are prebuilt.`\n );\n }\n return componentFn;\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAcA,SAAgB,gBAAgB,OAAwB;CACtD,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO,OAAO,KAAK;CAE9D,MAAM,OAAO,OAAO;CACpB,IAAI,SAAS,cAAc,SAAS,UAClC,MAAM,IAAI,UACR,uCAAuC,KAAK,yGAE9C;CAEF,IAAI,SAAS,UAAU,OAAO,KAAK,UAAU,KAAK;CAElD,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,MAAM,KAAK,SAAS,gBAAgB,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI;CAOtE,MAAM,aAAa;CACnB,IAAI,OAAO,WAAW,WAAW,YAAY;EAC3C,MAAM,OAAO,WAAW,OAAO;EAC/B,IAAI,SAAS,OAAO,OAAO,gBAAgB,IAAI;CACjD;CAMA,IAAI,iBAAiB,KAInB,OAAO,SAHS,CAAC,GAAG,MAAM,QAAQ,CAAC,CAAC,CAAC,KAClC,CAAC,GAAG,OAAO,gBAAgB,CAAC,IAAI,OAAO,gBAAgB,CAAC,CAE3C,CAAA,CAAQ,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI;CAE7C,IAAI,iBAAiB,KAEnB,OAAO,SADS,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,WAAW,gBAAgB,MAAM,CACjD,CAAA,CAAQ,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI;CAG7C,MAAM,MAAM;CACZ,MAAM,OAAO,OAAO,KAAK,GAAG,CAAC,CAAC,KAAK;CACnC,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,IAAI,SAAS,KAAA,GAAW;EAI5B,IAAI,QAAQ,YAAY,OAAO,IAAI,SAAS,YAAY;EACxD,MAAM,KAAK,KAAK,UAAU,GAAG,IAAI,MAAM,gBAAgB,IAAI,IAAI,CAAC;CAClE;CAKA,IAAI,MAAM,WAAW,GAAG;EACtB,MAAM,QAAQ,OAAO,eAAe,KAAK;EACzC,IAAI,UAAU,OAAO,aAAa,UAAU,MAAM;GAChD,MAAM,OAAQ,MAAiB,aAAa,QAAQ;GACpD,MAAM,IAAI,UACR,oBAAoB,KAAK,4JAG3B;EACF;CACF;CAEA,OAAO,MAAM,MAAM,KAAK,GAAG,IAAI;AACjC;;;;;;;ACzDA,IAAa,2BAAb,cAA8C,MAAM;CAClD,YAAY,KAAa,WAAmB;EAC1C,MAAM,8BAA8B,IAAI,aAAa,UAAU,GAAG;EAClE,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,mBAAmB,MAA0C;CAC3E,MAAM,2BAAW,IAAI,IAA8B;CACnD,MAAM,YAAY,MAAM;CAExB,OAAO,EACL,GAAM,KAAa,IAAqD;EACtE,MAAM,WAAW,SAAS,IAAI,GAAG;EACjC,IAAI,UAAU,OAAO;EAErB,MAAM,KAAK,IAAI,gBAAgB;EAC/B,IAAI;EAEJ,IAAI,aAAa,QAAQ,YAAY,GAKnC,UAAU,IAAI,SAAY,SAAS,WAAW;GAC5C,MAAM,QAAQ,iBAAiB;IAC7B,GAAG,MAAM;IACT,OAAO,IAAI,yBAAyB,KAAK,SAAS,CAAC;GACrD,GAAG,SAAS;GACZ,IAAI;IACF,GAAG,GAAG,MAAM,CAAC,CAAC,MACX,UAAU;KACT,aAAa,KAAK;KAClB,QAAQ,KAAK;IACf,IACC,QAAQ;KACP,aAAa,KAAK;KAClB,OAAO,GAAG;IACZ,CACF;GACF,SAAS,KAAK;IACZ,aAAa,KAAK;IAClB,OAAO,GAAG;GACZ;EACF,CAAC;OAED,UAAU,GAAG,GAAG,MAAM;EAGxB,MAAM,UAAU,QAAQ,cAAc;GACpC,SAAS,OAAO,GAAG;EACrB,CAAC;EAED,SAAS,IAAI,KAAK,OAAO;EACzB,OAAO;CACT,EACF;AACF;;;;;;;;;;;;;;;;;;;;;;AC/DA,IAAI,QAAQ;;;;;;AAOZ,IAAM,kCAAkB,IAAI,IAAoB;;;;;AAMhD,IAAM,sBAAsB;;;;;;AAO5B,IAAI,oBAAoB;;AAGxB,SAAgB,2BAAmC;CACjD,OAAO;AACT;;;;;AAMA,SAAgB,mBAAmB,MAA4C;CAC7E;CACA,IAAI,KAAK,KAAK,MAAM,OAAO,KAAK,KAAK;CACrC,IAAI,KAAK,KAAK,MAAM,OAAO,KAAK,KAAK;AACvC;AAEA,SAAS,MAAM,OAAqB;CAClC,gBAAgB,OAAO,KAAK;CAC5B,gBAAgB,IAAI,OAAO,KAAK;CAChC,OAAO,gBAAgB,OAAO,qBAAqB;EACjD,MAAM,SAAS,gBAAgB,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC;EAChD,IAAI,WAAW,KAAA,GAAW;EAC1B,oBAAoB,KAAK,IAAI,mBAAmB,OAAO,EAAE;EACzD,gBAAgB,OAAO,OAAO,EAAE;CAClC;AACF;;;;;AAMA,SAAgB,oBAAoB,YAAoB,KAAa,MAAyB;CAC5F,OAAO,yBAAyB,KAAK,IAAI,IAAI;AAC/C;;;;;;;;AASA,SAAgB,yBAAyB,KAAa,MAAwB;CAC5E,IAAI,OAAO;CACX,MAAM,WAAW,gBAAgB,IAAI,OAAO,KAAK;CACjD,IAAI,aAAa,KAAA,KAAa,WAAW,MAAM,OAAO;CACtD,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,WAAW,gBAAgB,IAAI,OAAO,KAAK;EACjD,IAAI,aAAa,KAAA,KAAa,WAAW,MAAM,OAAO;CACxD;CACA,OAAO;AACT;;;AC/EA,IAAI,sBAAsB,mBAAmB;;;;;;;;;;AAW7C,IAAM,mCAAmB,IAAI,IAAoB;AACjD,SAAS,eAAe,KAAqB;CAC3C,MAAM,OAAO,iBAAiB,IAAI,GAAG,KAAK,KAAK;CAC/C,iBAAiB,IAAI,KAAK,GAAG;CAC7B,OAAO;AACT;;;;;;;AAQA,SAAgB,8BAA8B,WAAyB;CACrE,sBAAsB,mBAAmB,EAAE,UAAU,CAAC;AACxD;;;;;;;;;;AAWA,SAAS,oBAAoB,MAAc,MAAyB;CAClE,MAAM,MAAM,OAAO,MAAM,gBAAgB,IAAI;CAC7C,OAAO,OAAO,MAAM,UAAU,GAAG;AACnC;;;;AAMA,SAAS,YACP,MACA,MACU;CACV,IAAI,CAAC,KAAK,MAAM,OAAO,CAAC;CACxB,IAAI,MAAM,QAAQ,KAAK,IAAI,GAAG,OAAO,KAAK;CAC1C,OAAO,KAAK,KAAK,GAAG,IAAI;AAC1B;;AAGA,IAAM,wCAAwB,IAAI,IAAoB;AACtD,IAAI,sBAAsB;;;;;;;;;;;;;;AAgB1B,SAAS,mBAAmB,IAAqC;CAC/D,MAAM,aAAa,UAAU,GAAG,SAAS,CAAC;CAC1C,MAAM,aAAa,sBAAsB,IAAI,UAAU,KAAK;CAC5D,sBAAsB,IAAI,YAAY,aAAa,CAAC;CAEpD,IAAI,CAAC,uBAAuB,OAAO,YAAY,eAAA,QAAA,IAAA,aAAyC,QAAQ;EAC9F,sBAAsB;EACtB,QAAQ,KACN,khBAOF;CACF;CAEA,OAAO,mBAAmB,WAAW,GAAG;AAC1C;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,YACd,IACA,MACA,YACA,UACI;CAGJ,MAAM,OAAO,aAAa,KAAK,MAAM,KAAA,IAAY,mBAAmB,EAAE;CAOtE,MAAM,YACJ,KAAK,cAAc,KAAA,IAAY,mBAAmB,EAAE,WAAW,KAAK,UAAU,CAAC,IAAI,KAAA;CAKrF,QAAQ,OAAO,GAAG,SAA2D;EAC3E,MAAM,KAAK,aAAa;EACxB,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,GAAG,IAAI,IAAI,oBAAoB,MAAgB,IAAI;EAEnF,MAAM,aAAa,YAAY,IAAI;EACnC,MAAM,SAAS,MAAM,WAAW,CAAC,CAAC,IAAI,GAAG;EAEzC,IAAI,UAAU,CAAC,OAAO,OAAO;GAG3B,iBAAiB,oBAAoB;IACnC;IACA,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,UAAU;GACxD,CAAC;GACD,OAAO,OAAO;EAChB;EAQA,MAAM,OAAO,YAAY,MAAM,IAAI;EACnC,MAAM,YAAY,GAAG,IAAI,QAAQ,yBAAyB,KAAK,IAAI;;;;;;;;;;;EAYnE,MAAM,kBAAkB,OAAO,WAA0D;GACvF,MAAM,aAAa,yBAAyB;GAC5C,MAAM,aAAa,eAAe,GAAG;GACrC,MAAM,SAAS,MAAM,GAAG,GAAG,IAAI;GAC/B,IAAI,CAAC,OAAO,WAAW,CAAC,oBAAoB,YAAY,KAAK,IAAI,GAC/D,IAAI;IACF,MAAM,WAAW,CAAC,CAAC,IAAI,KAAK,QAAQ;KAAE,KAAK,KAAK;KAAK;KAAM;IAAW,CAAC;GACzE,SAAS,KAAK;IAKZ,QAAQ,KAAK,6BAA6B,IAAI,EAAE;GAClD;GAEF,OAAO;EACT;EAEA,IAAI,UAAU,OAAO,SAAS,KAAK,sBAAsB;GAEvD,iBAAiB,oBAAoB;IACnC;IACA,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,UAAU;IACtD,OAAO;GACT,CAAC;GAED,MAAM,UAAU,GACb,GAAG,OAAO,aAAa,OAAO,WAAW;IACxC,IAAI;KACF,MAAM,gBAAgB,MAAM;IAC9B,SAAS,KAAK;KAEZ,oBAAoB;MAAE,UAAU;MAAK,OAAO;KAAI,CAAC;IACnD;GACF,CAAC,CAAC,CACD,YAAY,CAEb,CAAC;GAOH,aAAa,CAAC,GAAG,OAAO;GACxB,OAAO,OAAO;EAChB;EAKA,MAAM,SAAS,MAAM,GAAG,GAAG,WAAW,eAAe;EAGrD,iBAAiB,qBAAqB;GACpC;GACA,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,UAAU;EACxD,CAAC;EAED,OAAO;CACT;AACF;;;ACvOA,IAAI,UAAmC;AAEvC,IAAI,uBAAuB;;AAG3B,SAAgB,gBAAgB,GAA2B;CACzD,UAAU;CACV,uBAAuB;AACzB;;AAGA,SAAgB,2BAAoC;CAClD,OAAO;AACT;;;;;AAMA,SAAgB,kBAAoC;CAClD,IAAI,CAAC,SAGH,UAAU,qBAAqB;CAEjC,OAAO;AACT;AAEA,SAAS,uBAAyC;CAChD,MAAM,wBAAQ,IAAI,IAAmE;CACrF,MAAM,aAAa;CAEnB,OAAO;EACL,MAAM,IAAI,KAAK;GACb,MAAM,QAAQ,MAAM,IAAI,GAAG;GAC3B,IAAI,CAAC,OAAO,OAAO;GACnB,MAAM,OAAO,GAAG;GAChB,MAAM,IAAI,KAAK,KAAK;GACpB,MAAM,QAAQ,KAAK,IAAI,IAAI,MAAM;GACjC,OAAO;IAAE,OAAO,MAAM;IAAO;GAAM;EACrC;EACA,MAAM,IAAI,KAAK,OAAO,MAAM;GAC1B,IAAI,MAAM,IAAI,GAAG,GAAG,MAAM,OAAO,GAAG;GACpC,OAAO,MAAM,QAAQ,YAAY;IAC/B,MAAM,SAAS,MAAM,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,WAAW,KAAA,GAAW,MAAM,OAAO,MAAM;SACxC;GACP;GACA,MAAM,IAAI,KAAK;IAAE;IAAO,WAAW,KAAK,IAAI,IAAI,KAAK,MAAM;IAAM,MAAM,KAAK;GAAK,CAAC;EACpF;EACA,MAAM,WAAW,MAAM;GACrB,IAAI,KAAK,KAAK,MAAM,OAAO,KAAK,GAAG;GACnC,IAAI,KAAK,KACF;SAAA,MAAM,CAAC,KAAK,UAAU,OACzB,IAAI,MAAM,KAAK,SAAS,KAAK,GAAG,GAAG,MAAM,OAAO,GAAG;GAAA;EAGzD;CACF;AACF;;;ACwDA,IAAM,aAAa;AACnB,IAAM,aAAa;;;;;;;;;;;;AAanB,IAAa,oBAAb,MAAuD;CACrD;CACA;CAEA,YAAY,QAAqB,MAA4B;EAC3D,KAAK,SAAS;EACd,KAAK,SAAS,MAAM,UAAU;CAChC;CAEA,SAAiB,KAAqB;EACpC,OAAO,GAAG,KAAK,SAAS,aAAa;CACvC;CAEA,OAAe,KAAqB;EAClC,OAAO,GAAG,KAAK,SAAS,aAAa;CACvC;CAEA,MAAM,IAAI,KAAiE;EACzE,MAAM,MAAM,MAAM,KAAK,OAAO,IAAI,KAAK,SAAS,GAAG,CAAC;EACpD,IAAI,QAAQ,MAAM,OAAO;EAEzB,MAAM,QAAQ,KAAK,MAAM,GAAG;EAC5B,MAAM,QAAQ,KAAK,IAAI,IAAI,MAAM;EACjC,OAAO;GAAE,OAAO,MAAM;GAAO;EAAM;CACrC;CAEA,MAAM,IACJ,KACA,OACA,MACe;EACf,mBAAmB,OAAO,GAAG;EAC7B,MAAM,KAAK,KAAK,SAAS,GAAG;EAC5B,MAAM,YAAY,KAAK,IAAI,IAAI,KAAK,MAAM;EAI1C,MAAM,UAAU,KAAK,UAAU;GAAE;GAAO;GAAW,MAAM,KAAK;EAAK,CAAC;EAMpE,MAAM,kBAAkB,KAAK,IAAI,KAAK,MAAM,IAAI,IAAI,GAAG;EAEvD,MAAM,WAA2B,CAAC;EAClC,KAAK,MAAM,OAAO,KAAK,MAAM;GAC3B,SAAS,KAAK;IAAE,IAAI;IAAQ,KAAK,KAAK,OAAO,GAAG;IAAG,SAAS,CAAC,GAAG;GAAE,CAAC;GACnE,SAAS,KAAK;IAAE,IAAI;IAAY,KAAK,KAAK,OAAO,GAAG;IAAG,SAAS;GAAgB,CAAC;GACjF,SAAS,KAAK;IAAE,IAAI;IAAY,KAAK,KAAK,OAAO,GAAG;IAAG,SAAS;GAAgB,CAAC;EACnF;EACA,SAAS,KAAK;GAAE,IAAI;GAAO,KAAK;GAAI,OAAO;GAAS,YAAY;EAAgB,CAAC;EAEjF,IAAI,KAAK,OAAO,WACd,MAAM,KAAK,OAAO,UAAU,QAAQ;OAEpC,KAAK,MAAM,MAAM,UACf,MAAM,KAAK,OAAO,EAAE;CAG1B;CAEA,MAAc,OAAO,IAAiC;EACpD,QAAQ,GAAG,IAAX;GACE,KAAK;IACH,MAAM,KAAK,OAAO,IAAI,GAAG,KAAK,GAAG,OAAO,GAAG,UAAU;IACrD;GACF,KAAK;IACH,MAAM,KAAK,OAAO,KAAK,GAAG,KAAK,GAAG,GAAG,OAAO;IAC5C;GACF,KAAK;IACH,MAAM,KAAK,OAAO,SAAS,GAAG,KAAK,GAAG,OAAO;IAC7C;GACF,KAAK;IACH,MAAM,KAAK,OAAO,SAAS,GAAG,KAAK,GAAG,OAAO;IAC7C;GACF,KAAK,OACH,MAAM,KAAK,OAAO,IAAI,GAAG,GAAG;EAEhC;CACF;CAEA,MAAM,WAAW,MAAqD;EACpE,IAAI,KAAK,KAAK;GACZ,MAAM,MAAM,MAAM,KAAK,OAAO,IAAI,KAAK,SAAS,KAAK,GAAG,CAAC;GACzD,IAAI,QAAQ,MAAM;IAChB,MAAM,QAAQ,KAAK,MAAM,GAAG;IAC5B,IAAI,MAAM,MACR,MAAM,QAAQ,IAAI,MAAM,KAAK,KAAK,QAAQ,KAAK,OAAO,KAAK,KAAK,OAAO,GAAG,GAAG,KAAK,GAAI,CAAC,CAAC;GAE5F;GACA,MAAM,KAAK,OAAO,IAAI,KAAK,SAAS,KAAK,GAAG,CAAC;EAC/C;EAEA,IAAI,KAAK,KAAK;GACZ,MAAM,KAAK,KAAK,OAAO,KAAK,GAAG;GAC/B,MAAM,OAAO,MAAM,KAAK,OAAO,SAAS,EAAE;GAK1C,MAAM,QAAQ,IACZ,KAAK,IAAI,OAAO,MAAM;IACpB,MAAM,MAAM,MAAM,KAAK,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC;IAClD,IAAI,QAAQ,MAAM;KAChB,MAAM,KAAK,OAAO,KAAK,IAAI,CAAC;KAC5B;IACF;IAEA,IADc,KAAK,MAAM,GACrB,CAAA,CAAM,MAAM,SAAS,KAAK,GAAI,GAChC,MAAM,KAAK,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC;IAExC,MAAM,KAAK,OAAO,KAAK,IAAI,CAAC;GAC9B,CAAC,CACH;GAIA,KAAI,MADoB,KAAK,OAAO,SAAS,EAAE,EAAA,CACjC,WAAW,GACvB,MAAM,KAAK,OAAO,IAAI,EAAE;EAE5B;CACF;AACF;;;ACpPA,IAAM,cAAc;AACpB,IAAM,iBAAiB;AACvB,IAAM,qBAAqB;AAmB3B,SAAS,eAAe,OAA6B;CACnD,IAAI,MAAM,eAAe,MAAM,eAAe,OAAO;CACrD,IAAI,MAAM,QAAQ,MAAM,YAAY,MAAM,QAAQ,MAAM,MAAM,OAAO;CACrE,OAAO;AACT;AAEA,IAAa,uBAAb,MAA0D;CACxD;CACA;CACA;CAEA,YAAY,OAAmB,MAA4B;EACzD,KAAK,QAAQ;EACb,KAAK,SAAS,MAAM,UAAU;EAC9B,KAAK,WAAW,eAAe,KAAK;CACtC;CAEA,IAAI,cAA+C;EACjD,OAAO,KAAK,MAAM;CACpB;CAEA,QAAgB,KAAqB;EACnC,OAAO,GAAG,KAAK,SAAS,cAAc;CACxC;;CAGA,WAAmB,MAA0B;EAC3C,OAAO,KAAK,KAAK,MAAM,GAAG,KAAK,SAAS,GAAG;CAC7C;CAEA,UAAkB,KAAqB;EACrC,OAAO,GAAG,KAAK,SAAS,iBAAiB;CAC3C;CAEA,cAAsB,KAAqB;EACzC,OAAO,GAAG,KAAK,SAAS,qBAAqB;CAC/C;CAEA,MAAM,IAAI,KAAiE;EACzE,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI,KAAK,QAAQ,GAAG,CAAC;EAClD,IAAI,QAAQ,MAAM,OAAO;EAEzB,IAAI,KAAK,aAAa,YAAY,KAAK,aAAa,SAAS;GAC3D,MAAM,QAAQ,KAAK,MAAM,GAAG;GAC5B,MAAM,QAAQ,KAAK,IAAI,IAAI,MAAM;GACjC,OAAO;IAAE,OAAO,MAAM;IAAO;GAAM;EACrC;EAGA,MAAM,QAAQ,KAAK,MAAM,GAAG;EAC5B,IAAI,MAAM,KAAK,SAAS,GAAG;GACzB,MAAM,kBAAkB,MAAM,KAAK,eAAe,MAAM,IAAI;GAC5D,KAAK,MAAM,OAAO,MAAM,MAAM;IAC5B,MAAM,SAAS,MAAM,YAAY,QAAQ;IAEzC,KADgB,gBAAgB,QAAQ,KAC1B,QAAQ,OAAO;GAC/B;EACF;EACA,MAAM,QAAQ,KAAK,IAAI,IAAI,MAAM;EACjC,OAAO;GAAE,OAAO,MAAM;GAAO;EAAM;CACrC;CAEA,MAAM,IACJ,KACA,OACA,MACe;EACf,MAAM,cAAc,KAAK,IAAI,KAAK,MAAM,IAAI,IAAI,GAAG;EAEnD,IAAI,KAAK,aAAa,UAAU;GAC9B,MAAM,QAAoB;IACxB;IACA,WAAW,KAAK,IAAI,IAAI,KAAK,MAAM;IACnC,MAAM,KAAK;GACb;GACA,MAAM,UAAU,KAAK,UAAU,KAAK;GACpC,IAAI,KAAK,KAAK,SAAS,GACrB,MAAM,KAAK,MAAM,YACf,KAAK,QAAQ,GAAG,GAChB,SACA,aACA,KAAK,WAAW,KAAK,IAAI,CAC3B;QAEA,MAAM,KAAK,MAAM,IAAI,KAAK,QAAQ,GAAG,GAAG,SAAS,WAAW;GAE9D;EACF;EAEA,IAAI,KAAK,aAAa,SAAS;GAC7B,MAAM,QAAoB;IACxB;IACA,WAAW,KAAK,IAAI,IAAI,KAAK,MAAM;IACnC,MAAM,KAAK;GACb;GACA,MAAM,KAAK,MAAM,IAAI,KAAK,QAAQ,GAAG,GAAG,KAAK,UAAU,KAAK,GAAG,WAAW;GAC1E,IAAI,KAAK,KAAK,SAAS,GACrB,MAAM,QAAQ,IACZ,KAAK,KAAK,KAAK,QAAQ,KAAK,MAAM,KAAM,KAAK,UAAU,GAAG,GAAG,CAAC,GAAG,GAAG,WAAW,CAAC,CAClF;GAEF;EACF;EAGA,MAAM,cAAc,KAAK,KAAK,SAAS,IAAI,MAAM,KAAK,eAAe,KAAK,IAAI,IAAI,CAAC;EACnF,MAAM,QAAmB;GACvB;GACA,WAAW,KAAK,IAAI,IAAI,KAAK,MAAM;GACnC,MAAM,KAAK;GACX;EACF;EACA,MAAM,KAAK,MAAM,IAAI,KAAK,QAAQ,GAAG,GAAG,KAAK,UAAU,KAAK,GAAG,WAAW;CAC5E;CAEA,MAAM,WAAW,MAAqD;EACpE,IAAI,KAAK,KAAK;GACZ,IAAI,KAAK,aAAa,SAAS;IAC7B,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI,KAAK,QAAQ,KAAK,GAAG,CAAC;IACvD,IAAI,QAAQ,MAAM;KAChB,MAAM,QAAQ,KAAK,MAAM,GAAG;KAC5B,IAAI,MAAM,KAAK,SAAS,GACtB,MAAM,QAAQ,IACZ,MAAM,KAAK,KAAK,QAAQ,KAAK,MAAM,KAAM,KAAK,UAAU,GAAG,GAAG,CAAC,KAAK,GAAI,CAAC,CAAC,CAC5E;IAEJ;GACF;GACA,MAAM,KAAK,MAAM,IAAI,KAAK,QAAQ,KAAK,GAAG,CAAC;EAC7C;EAEA,IAAI,KAAK,KAAK;GACZ,MAAM,MAAM,KAAK;GACjB,IAAI,KAAK,aAAa,UACpB,MAAM,KAAK,MAAM,cAAe,GAAG,KAAK,SAAS,KAAK;QACjD,IAAI,KAAK,aAAa,SAAS;IACpC,MAAM,UAAU,MAAM,KAAK,MAAM,SAAU,KAAK,UAAU,GAAG,CAAC;IAK9D,MAAM,QAAQ,IACZ,QAAQ,IAAI,OAAO,MAAM;KACvB,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI,KAAK,QAAQ,CAAC,CAAC;KAChD,IAAI,QAAQ,MAAM;MAChB,MAAM,KAAK,MAAM,KAAM,KAAK,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC;MAC/C;KACF;KAEA,IADc,KAAK,MAAM,GACrB,CAAA,CAAM,KAAK,SAAS,GAAG,GAAG;MAC5B,MAAM,KAAK,MAAM,IAAI,KAAK,QAAQ,CAAC,CAAC;MACpC,MAAM,KAAK,MAAM,KAAM,KAAK,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC;KACjD,OACE,MAAM,KAAK,MAAM,KAAM,KAAK,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC;IAEnD,CAAC,CACH;GACF,OACE,MAAM,KAAK,eAAe,KAAK,GAAG;EAEtC;CACF;CAIA,MAAc,eAAe,MAAiD;EAC5E,MAAM,WAAmC,CAAC;EAC1C,MAAM,QAAQ,IACZ,KAAK,IAAI,OAAO,QAAQ;GACtB,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI,KAAK,cAAc,GAAG,CAAC;GACxD,SAAS,OAAO,QAAQ,OAAO,SAAS,KAAK,EAAE,IAAI;EACrD,CAAC,CACH;EACA,OAAO;CACT;CAEA,MAAc,eAAe,KAA4B;EACvD,MAAM,MAAM,KAAK,cAAc,GAAG;EAClC,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI,GAAG;EAOpC,MAAM,UAAU,KAAK,IANN,QAAQ,OAAO,SAAS,KAAK,EAAE,IAAI,GAMjB,KAAK,IAAI,CAAC,IAAI;EAI/C,MAAM,KAAK,MAAM,IAAI,KAAK,OAAO,OAAO,GAAG,OAAW;CACxD;AACF;;;ACrMA,IAAa,qBAAb,MAAwD;CACtD,wBAAgB,IAAI,IAGlB;CACF,8BAAsB,IAAI,IAAoB;CAC9C;CACA;CACA;CACA,eAAuB;CACvB;CAEA,YAAY,MAAkC;EAE5C,KAAK,aAAa,MAAM,cAAc,MAAM,WAAW;EACvD,KAAK,WAAW,MAAM;EACtB,KAAK,gBAAgB,MAAM;EAC3B,KAAK,cAAc,KAAK,aAAa,KAAA,KAAa,KAAK,kBAAkB,KAAA;CAC3E;CAEA,MAAM,IAAI,KAAa;EACrB,MAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;EAChC,IAAI,CAAC,OAAO,OAAO;EAGnB,KAAK,MAAM,OAAO,GAAG;EACrB,KAAK,MAAM,IAAI,KAAK,KAAK;EAEzB,MAAM,QAAQ,KAAK,IAAI,IAAI,MAAM;EACjC,OAAO;GAAE,OAAO,MAAM;GAAO;EAAM;CACrC;CAEA,MAAM,IACJ,KACA,OACA,MACA;EAEA,IAAI,KAAK,eAAe,KAAA,GAAW;GACjC,MAAM,UAAU,KAAK,YAAY,IAAI,GAAG,KAAK;GAC7C,IAAI,KAAK,aAAa,SAAS;EACjC;EAEA,MAAM,WAAW,KAAK,cAAc,iBAAiB,KAAK,IAAI;EAG9D,IAAI,KAAK,kBAAkB,KAAA,KAAa,WAAW,KAAK,eACtD;EAIF,IAAI,KAAK,MAAM,IAAI,GAAG,GAAG;GACvB,MAAM,WAAW,KAAK,MAAM,IAAI,GAAG;GACnC,KAAK,gBAAgB,SAAS;GAC9B,KAAK,MAAM,OAAO,GAAG;EACvB;EAGA,OAAO,KAAK,MAAM,QAAQ,KAAK,YAC7B,KAAK,YAAY;EAInB,IAAI,KAAK,aAAa,KAAA,GAAW;GAC/B,OAAO,KAAK,eAAe,WAAW,KAAK,YAAY,KAAK,MAAM,OAAO,GACvE,KAAK,YAAY;GAGnB,IAAI,KAAK,eAAe,WAAW,KAAK,UACtC;EAEJ;EAGA,IAAI,KAAK,eAAe,KAAA,GACtB,KAAK,YAAY,IAAI,KAAK,KAAK,UAAU;EAG3C,KAAK,MAAM,IAAI,KAAK;GAClB;GACA,WAAW,KAAK,IAAI,IAAI,KAAK,MAAM;GACnC,MAAM,KAAK;GACX;EACF,CAAC;EACD,KAAK,gBAAgB;CACvB;CAEA,MAAM,WAAW,MAAsC;EACrD,IAAI,KAAK,KAAK;GACZ,MAAM,QAAQ,KAAK,MAAM,IAAI,KAAK,GAAG;GACrC,IAAI,OAAO;IACT,KAAK,gBAAgB,MAAM;IAC3B,KAAK,MAAM,OAAO,KAAK,GAAG;GAC5B;GACA,KAAK,YAAY,OAAO,KAAK,GAAG;EAClC;EACA,IAAI,KAAK,KACF;QAAA,MAAM,CAAC,KAAK,UAAU,KAAK,OAC9B,IAAI,MAAM,KAAK,SAAS,KAAK,GAAG,GAAG;IACjC,KAAK,gBAAgB,MAAM;IAC3B,KAAK,MAAM,OAAO,GAAG;IACrB,KAAK,YAAY,OAAO,GAAG;GAC7B;;CAGN;;CAGA,IAAI,OAAe;EACjB,OAAO,KAAK,MAAM;CACpB;;CAGA,IAAI,QAAgB;EAClB,OAAO,KAAK;CACd;;CAGA,cAA4B;EAC1B,MAAM,SAAS,KAAK,MAAM,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;EACxC,IAAI,WAAW,KAAA,GAAW;GACxB,MAAM,QAAQ,KAAK,MAAM,IAAI,MAAM;GACnC,KAAK,gBAAgB,MAAM;GAC3B,KAAK,MAAM,OAAO,MAAM;GACxB,KAAK,YAAY,OAAO,MAAM;EAChC;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;AC5IA,IAAa,uBAAuB;;AAGpC,IAAa,gBAAgB;;;;;AAQ7B,IAAI,sBAAqC;;;;AAczC,SAAgB,kBAAiC;CAC/C,OAAO;AACT;;;;;;;;;;;;AAuBA,SAAgB,iBAAiB,KAA+B;CAE9D,IAAI,CAAC,qBACH,OAAO;EAAE,IAAI;EAAM,UAAU;CAAK;CAGpC,MAAM,WAAW,IAAI,QAAQ,IAAI,oBAAoB;CAGrD,IAAI,CAAC,UACH,OAAO;EAAE,IAAI;EAAM,UAAU;CAAK;CAIpC,IAAI,aAAa,qBACf,OAAO;EAAE,IAAI;EAAM;CAAS;CAG9B,OAAO;EAAE,IAAI;EAAO;CAAS;AAC/B;;;;;AAMA,SAAgB,mBAAmB,SAAwB;CACzD,QAAQ,IAAI,eAAe,GAAG;AAChC;;;;;;;;;;AC3EA,SAAgB,cAAc,UAAwD;CACpF,MAAM,wBAAQ,IAAI,IAA2B;CAC7C,KAAK,MAAM,CAAC,aAAa,cAAc,OAAO,QAAQ,QAAQ,GAAG;EAC/D,IAAI,CAAC,aAAa,OAAO,cAAc,YAAY,EAAE,aAAa,YAAY;EAC9E,KAAK,MAAM,CAAC,UAAU,UAAU,OAAO,QAAQ,UAAU,OAAO,GAAG;GACjE,IAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,WAAW,GAAG;GAC5C,KAAK,MAAM,OAAO,MAAM,MAAM;IAC5B,IAAI,OAAO,MAAM,IAAI,GAAG;IACxB,IAAI,CAAC,MAAM;KACT,OAAO,CAAC;KACR,MAAM,IAAI,KAAK,IAAI;IACrB;IACA,KAAK,KAAK;KAAE;KAAa;IAAS,CAAC;GACrC;EACF;CACF;CACA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;ACIA,IAAI,SAAuC;AAC3C,IAAI,kBAA2D;;;;;;;;;AAU/D,IAAM,2BAA2B;;;;;;;;AASjC,IAAM,0BAA0B;;AAGhC,eAAe,YACb,MACA,WACA,UACA,WACY;CACZ,IAAI;CACJ,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CACxB,MACA,IAAI,SAAY,YAAY;GAC1B,QAAQ,iBAAiB;IACvB,UAAU;IACV,QAAQ,QAAQ;GAClB,GAAG,SAAS;EACd,CAAC,CACH,CAAC;CACH,UAAU;EACR,aAAa,KAAK;CACpB;AACF;AAEA,eAAe,wBACb,KACkC;CAClC,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,YAAY,IAAI,aAAa,GAAG,0BAA0B,YAAY;GACrF,QAAQ,MACN,4CAA4C,yBAAyB,yDACvE;EACF,CAAC;CACH,SAAS,OAAO;EACd,QAAQ,MACN,sFACA,KACF;EACA,OAAO;CACT;CACA,IAAI,UACF,aAAa,QAAQ;CAEvB,OAAO;AACT;;;;;;AAOA,SAAgB,yBAAyB,MAA0C;CACjF,SAAS;CACT,kBAAkB;CAClB,IAAI,CAAC,MAAM,cAAc;AAC3B;;;;;AAMA,SAAgB,2BAAoC;CAClD,OAAO,WAAW;AACpB;;;;;;;AAQA,eAAsB,uBAAsC;CAC1D,IAAI,WAAW,MAAM;CACrB,oBAAoB,wBAAwB,MAAM;CAClD,MAAM;AACR;;;;;AAMA,eAAsB,mBAAmB,aAAuC;CAC9E,IAAI,WAAW,MAAM,OAAO;CAC5B,oBAAoB,wBAAwB,MAAM;CAElD,QAAO,MADgB,gBAAA,GACL,YAAY,EAAE,qBAAqB;AACvD;AAEA,eAAsB,iBACpB,aACA,UACuC;CACvC,IAAI,WAAW,MAAM,OAAO;CAC5B,oBAAoB,wBAAwB,MAAM;CAElD,QAAO,MADgB,gBAAA,GACL,YAAY,EAAE,UAAU,aAAa;AACzD;;;;;;;;;;;AAYA,eAAsB,sBACpB,aACA,UAC4B;CAC5B,IAAI,WAAW,MAAM,OAAO;CAC5B,oBAAoB,wBAAwB,MAAM;CAElD,MAAM,SAAQ,MADS,gBAAA,GACE,YAAY,EAAE,UAAU;CACjD,IAAI,CAAC,OAAO,OAAO;CACnB,IAAI;EACF,OAAO,MAAM,YAAY,OAAO,YAAY,MAAM,IAAI,GAAG,yBAAyB,YAAY;GAC5F,QAAQ,MACN,uCAAuC,MAAM,KAAK,aAAa,wBAAwB,2BACzF;EACF,CAAC;CACH,SAAS,OAAO;EACd,QAAQ,MACN,6CAA6C,MAAM,KAAK,QAAQ,YAAY,4BAC5E,KACF;EACA,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;AClLA,IAAM,sBAAsB;;AAG5B,IAAM,0BAA0B;AA4BhC,SAAgB,gBAAgB,GAAyC;CACvE,OAAO,MAAM,QAAQ,OAAO,MAAM,YAAa,EAA8B,OAAO;AACtF;AAEA,SAAgB,YAAY,GAA2C;CACrE,OAAO,MAAM,QAAQ,OAAO,MAAM,YAAa,EAA8B,OAAO;AACtF;AAIA,SAAgB,WAAW,aAAqB,UAA0B;CAExE,OAAO,YADc,gBAAgB,KAAK,MACV,GAAG,YAAY,GAAG;AACpD;;;;;AAgBA,eAAsB,cACpB,aACA,UACqC;CACrC,MAAM,UAAU,gBAAgB;CAChC,MAAM,MAAM,WAAW,aAAa,QAAQ;CAC5C,MAAM,SAAS,MAAM,QAAQ,IAAI,GAAG;CACpC,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,QAAQ,OAAO;CACrB,IAAI,YAAY,KAAK,GACnB,OAAO;EAAE,MAAM;EAAa,QAAQ;EAAM,SAAS;CAAK;CAE1D,IAAI,gBAAgB,KAAK,GAEvB,OAAO;EACL,MAFqB,KAAK,IAAI,IAAI,MAAM,KAEjB,UAAU;EACjC,QAAQ;EACR,SAAS,cAAc,MAAM,CAAC;CAChC;CAGF,OAAO;AACT;;;;;;AASA,SAAgB,aAAa,KAAiC;CAC5D,OAAO,OAAO;AAChB;;;;AAKA,eAAsB,kBACpB,aACA,UACA,cACA,MAKe;CACf,MAAM,UAAU,gBAAgB;CAChC,MAAM,MAAM,WAAW,aAAa,QAAQ;CAC5C,MAAM,aAAa,aAAa,KAAK,GAAG;CACxC,MAAM,cAAc,KAAK,UAAU,0BAA0B;CAE7D,MAAM,SAAiC;EACrC,IAAI;EACJ,GAAG,cAAc,YAAY;EAC7B,IAAI,KAAK,IAAI,IAAI,aAAa;EAC9B,GAAG,KAAK;CACV;CAEA,MAAM,QAAQ,IAAI,KAAK,QAAQ;EAAE,KAAK;EAAa,MAAM,KAAK;CAAK,CAAC;AACtE;AAIA,IAAI,WAA8C;;;;;;AAOlD,SAAgB,aAAa,UAAkC;CAC7D,WAAW,cAAc,QAAQ;CACjC,0BAA0B,QAAQ;AACpC;;;;;AAcA,SAAgB,gBAAsB;CACpC,WAAW;AACb;;;;;;;;;;;;;AAcA,eAAsB,sBAAsB,KAA4B;CACtE,IAAI,CAAC,UACH,MAAM,qBAAqB;CAE7B,IAAI,CAAC,UAAU;CACf,MAAM,QAAQ,SAAS,IAAI,GAAG;CAC9B,IAAI,CAAC,SAAS,MAAM,WAAW,GAAG;CAElC,MAAM,UAAU,gBAAgB;CAChC,MAAM,YAAsC,EAAE,IAAI,KAAK;CAEvD,MAAM,QAAQ,IACZ,MAAM,KAAK,SAAS;EAClB,MAAM,MAAM,WAAW,KAAK,aAAa,KAAK,QAAQ;EACtD,OAAO,QAAQ,IAAI,KAAK,WAAW;GACjC,KAAK;GACL,MAAM,CAAC,GAAG;EACZ,CAAC;CACH,CAAC,CACH;AACF;AAIA,IAAI,uBAAuB;AAE3B,SAAS,0BAA0B,UAAkC;CACnE,IAAI,sBAAsB;CAI1B,IAAI,EAFY,gBACM,aAAmB,sBAAsB,CAAC,yBAAyB,IACrE;CAEpB,KAAK,MAAM,GAAG,cAAc,OAAO,QAAQ,QAAQ,GAAG;EACpD,IAAI,CAAC,aAAa,OAAO,cAAc,YAAY,EAAE,aAAa,YAAY;EAC9E,KAAK,MAAM,GAAG,UAAU,OAAO,QAAQ,UAAU,OAAO,GACtD,IAAI,MAAM,QAAQ,MAAM,KAAK,SAAS,GAAG;GACvC,uBAAuB;GACvB,QAAQ,KACN,sSAIF;GACA;EACF;CAEJ;AACF;AAYA,SAAS,cAAc,OAA2B;CAChD,OAAO,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,QAAQ;AAC7C;AAEA,SAAS,cAAc,KAAyB;CAC9C,OAAO,IAAI,WAAW,OAAO,KAAK,KAAK,QAAQ,CAAC;AAClD;;;ACvPA,IAAM,0CAA0B,IAAI,QAAgB;;;;;;;;;;;;;AAcpD,IAAa,QAAQ;;;;;;;;;CAUnB,KACE,IACA,MACA,UACI;EACJ,OAAO,YAAY,IAAI,MAAM,iBAAiB,QAAQ;CACxD;;;;;;;;;CAUA,MAAM,WAAW,MAAqD;EAGpE,mBAAmB,IAAI;EACvB,MAAM,gBAAgB,CAAC,CAAC,WAAW,IAAI;EAKvC,IAAI,KAAK,KACP,MAAM,sBAAsB,KAAK,GAAG;EAMtC,IAAI,KAAK,KAAK;GACZ,MAAM,eAAe,mBAAmB;GACxC,IAAI,cACF,IAAI;IACF,MAAM,QAAQ,KAAK,CACjB,aAAa,UAAU,CAAC,KAAK,GAAG,CAAC,GACjC,IAAI,SAAe,GAAG,WACpB,iBAAiB,uBAAO,IAAI,MAAM,2BAA2B,CAAC,GAAG,GAAM,CACzE,CACF,CAAC;GACH,SAAS,KAAK;IACZ,QAAQ,MAAM,sCAAsC,GAAG;GACzD;EAEJ;CACF;;;;;;;;;;CAYA,UACE,aACA,UACG;EACH,IAAI,CAAC,wBAAwB,IAAI,WAAW,GAAG;GAC7C,wBAAwB,IAAI,WAAW;GACvC,MAAM,OAAO,YAAY,QAAQ;GACjC,QAAQ,KACN,8BAA8B,KAAK,2IAGrC;EACF;EACA,OAAO;CACT;AACF"}
|