@shopify/hydrogen 2026.10.0-preview.4 → 2026.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.mjs +23 -12
- package/dist/client/client.d.mts +18 -15
- package/dist/client/client.mjs +8 -3
- package/dist/client/client.mjs.map +1 -1
- package/dist/client/errors.d.mts +35 -13
- package/dist/client/errors.mjs +23 -13
- package/dist/client/errors.mjs.map +1 -1
- package/dist/client/types.d.mts +140 -90
- package/dist/core/analytics/cart-tracker.d.mts +19 -10
- package/dist/core/analytics/cart-tracker.mjs +18 -10
- package/dist/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/core/analytics/events.d.mts +17 -11
- package/dist/core/analytics/events.mjs +13 -4
- package/dist/core/analytics/events.mjs.map +1 -1
- package/dist/core/analytics/types.d.mts +207 -118
- package/dist/core/analytics/utils/flatten-connection.d.mts +11 -9
- package/dist/core/analytics/utils/flatten-connection.mjs +7 -6
- package/dist/core/analytics/utils/flatten-connection.mjs.map +1 -1
- package/dist/core/cache/fetch-with-cache.d.mts +24 -4
- package/dist/core/cache/fetch-with-cache.mjs +17 -2
- package/dist/core/cache/fetch-with-cache.mjs.map +1 -1
- package/dist/core/cache/key.d.mts +2 -0
- package/dist/core/cache/key.mjs.map +1 -1
- package/dist/core/cache/run-with-cache.d.mts +56 -8
- package/dist/core/cache/run-with-cache.mjs +17 -6
- package/dist/core/cache/run-with-cache.mjs.map +1 -1
- package/dist/core/cache/store.d.mts +8 -6
- package/dist/core/cache/store.mjs.map +1 -1
- package/dist/core/cache/strategies.d.mts +27 -2
- package/dist/core/cache/strategies.mjs +6 -0
- package/dist/core/cache/strategies.mjs.map +1 -1
- package/dist/core/cart/actions.d.mts +37 -30
- package/dist/core/cart/actions.mjs +21 -9
- package/dist/core/cart/actions.mjs.map +1 -1
- package/dist/core/cart/attach-quantity-input.d.mts +8 -7
- package/dist/core/cart/attach-quantity-input.mjs +8 -7
- package/dist/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/core/cart/buyer-identity-sync.d.mts +7 -6
- package/dist/core/cart/buyer-identity-sync.mjs.map +1 -1
- package/dist/core/cart/cart.d.mts +59 -46
- package/dist/core/cart/cart.mjs +46 -29
- package/dist/core/cart/cart.mjs.map +1 -1
- package/dist/core/cart/cookie.d.mts +8 -4
- package/dist/core/cart/cookie.mjs +8 -4
- package/dist/core/cart/cookie.mjs.map +1 -1
- package/dist/core/cart/form.d.mts +34 -20
- package/dist/core/cart/form.mjs +6 -4
- package/dist/core/cart/form.mjs.map +1 -1
- package/dist/core/cart/get-cart.d.mts +27 -15
- package/dist/core/cart/get-cart.mjs +13 -8
- package/dist/core/cart/get-cart.mjs.map +1 -1
- package/dist/core/cart/quantity.d.mts +16 -5
- package/dist/core/cart/quantity.mjs +16 -5
- package/dist/core/cart/quantity.mjs.map +1 -1
- package/dist/core/cart/queries.d.mts +25 -19
- package/dist/core/cart/queries.mjs +15 -1
- package/dist/core/cart/queries.mjs.map +1 -1
- package/dist/core/cart/server-handlers.d.mts +82 -45
- package/dist/core/cart/server-handlers.mjs +4 -0
- package/dist/core/cart/server-handlers.mjs.map +1 -1
- package/dist/core/cart/state.d.mts +110 -64
- package/dist/core/cart/state.mjs +24 -6
- package/dist/core/cart/state.mjs.map +1 -1
- package/dist/core/collection/collection.d.mts +52 -53
- package/dist/core/collection/collection.mjs +5 -3
- package/dist/core/collection/collection.mjs.map +1 -1
- package/dist/core/collection/reconciler.d.mts +23 -16
- package/dist/core/collection/reconciler.mjs +8 -4
- package/dist/core/collection/reconciler.mjs.map +1 -1
- package/dist/core/collection/state.d.mts +31 -26
- package/dist/core/collection/state.mjs +4 -3
- package/dist/core/collection/state.mjs.map +1 -1
- package/dist/core/collection/url.d.mts +70 -29
- package/dist/core/collection/url.mjs +63 -25
- package/dist/core/collection/url.mjs.map +1 -1
- package/dist/core/constants.d.mts +1 -0
- package/dist/core/constants.mjs +1 -0
- package/dist/core/constants.mjs.map +1 -1
- package/dist/core/logging/logging.d.mts +14 -8
- package/dist/core/logging/logging.mjs +15 -10
- package/dist/core/logging/logging.mjs.map +1 -1
- package/dist/core/logging/types.d.mts +28 -11
- package/dist/core/logging/types.mjs.map +1 -1
- package/dist/core/money/format.d.mts +6 -4
- package/dist/core/money/format.mjs +19 -9
- package/dist/core/money/format.mjs.map +1 -1
- package/dist/core/money/types.d.mts +41 -25
- package/dist/core/predictive-search/form.d.mts +37 -7
- package/dist/core/predictive-search/form.mjs +21 -3
- package/dist/core/predictive-search/form.mjs.map +1 -1
- package/dist/core/predictive-search/queries.d.mts +18 -30
- package/dist/core/predictive-search/queries.mjs +7 -3
- package/dist/core/predictive-search/queries.mjs.map +1 -1
- package/dist/core/predictive-search/search.d.mts +25 -27
- package/dist/core/predictive-search/search.mjs +20 -19
- package/dist/core/predictive-search/search.mjs.map +1 -1
- package/dist/core/predictive-search/server-handlers.d.mts +25 -26
- package/dist/core/predictive-search/server-handlers.mjs +4 -0
- package/dist/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/core/predictive-search/store.d.mts +65 -24
- package/dist/core/predictive-search/store.mjs +12 -1
- package/dist/core/predictive-search/store.mjs.map +1 -1
- package/dist/core/predictive-search/url.d.mts +36 -37
- package/dist/core/predictive-search/url.mjs +14 -6
- package/dist/core/predictive-search/url.mjs.map +1 -1
- package/dist/core/product/form.d.mts +63 -16
- package/dist/core/product/form.mjs +8 -2
- package/dist/core/product/form.mjs.map +1 -1
- package/dist/core/product/options.d.mts +8 -10
- package/dist/core/product/options.mjs +8 -10
- package/dist/core/product/options.mjs.map +1 -1
- package/dist/core/product/product-form.d.mts +77 -29
- package/dist/core/product/product-form.mjs +32 -10
- package/dist/core/product/product-form.mjs.map +1 -1
- package/dist/core/product/state.d.mts +55 -32
- package/dist/core/product/url.d.mts +19 -13
- package/dist/core/product/url.mjs +10 -10
- package/dist/core/product/url.mjs.map +1 -1
- package/dist/core/request-context.d.mts +77 -19
- package/dist/core/request-context.mjs +5 -1
- package/dist/core/request-context.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-redirects.d.mts +12 -4
- package/dist/core/request-routing/handle-shopify-redirects.mjs +9 -2
- package/dist/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-routes.d.mts +5 -5
- package/dist/core/request-routing/handle-shopify-routes.mjs +7 -5
- package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs +79 -43
- package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
- package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
- package/dist/core/request-routing/interceptors/url-redirects.mjs +6 -1
- package/dist/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/core/request-routing/interceptors/well-known.mjs.map +1 -1
- package/dist/core/request-routing/registered-routes.d.mts +12 -0
- package/dist/core/request-routing/registered-routes.mjs +12 -0
- package/dist/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/core/request-routing/route-types.d.mts +43 -18
- package/dist/core/shop-pay/shop-pay.d.mts +71 -35
- package/dist/core/shop-pay/shop-pay.mjs +36 -10
- package/dist/core/shop-pay/shop-pay.mjs.map +1 -1
- package/dist/core/shopify-scripts/index.d.mts +15 -5
- package/dist/core/shopify-scripts/index.mjs +15 -5
- package/dist/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/core/shopify-scripts/initialize.d.mts +12 -4
- package/dist/core/shopify-scripts/initialize.mjs +12 -4
- package/dist/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/core/shopify-scripts/types.d.mts +58 -25
- package/dist/core/standard-routes/build.d.mts +11 -12
- package/dist/core/standard-routes/build.mjs +11 -12
- package/dist/core/standard-routes/build.mjs.map +1 -1
- package/dist/core/standard-routes/types.d.mts +30 -24
- package/dist/core/url.mjs +2 -1
- package/dist/core/url.mjs.map +1 -1
- package/dist/customer-account/client.d.mts +36 -69
- package/dist/customer-account/client.mjs +7 -8
- package/dist/customer-account/client.mjs.map +1 -1
- package/dist/customer-account/errors.d.mts +45 -36
- package/dist/customer-account/errors.mjs +21 -36
- package/dist/customer-account/errors.mjs.map +1 -1
- package/dist/customer-account/graphql.d.mts +11 -23
- package/dist/customer-account/graphql.mjs +3 -9
- package/dist/customer-account/graphql.mjs.map +1 -1
- package/dist/customer-account/session.d.mts +130 -100
- package/dist/customer-account/session.mjs +32 -10
- package/dist/customer-account/session.mjs.map +1 -1
- package/dist/customer-account/type-resolver.d.mts +7 -8
- package/dist/development/client/client.d.mts +18 -15
- package/dist/development/client/client.mjs +8 -3
- package/dist/development/client/client.mjs.map +1 -1
- package/dist/development/client/errors.d.mts +35 -13
- package/dist/development/client/errors.mjs +23 -13
- package/dist/development/client/errors.mjs.map +1 -1
- package/dist/development/client/types.d.mts +140 -90
- package/dist/development/core/analytics/cart-tracker.d.mts +19 -10
- package/dist/development/core/analytics/cart-tracker.mjs +18 -10
- package/dist/development/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/development/core/analytics/events.d.mts +17 -11
- package/dist/development/core/analytics/events.mjs +13 -4
- package/dist/development/core/analytics/events.mjs.map +1 -1
- package/dist/development/core/analytics/types.d.mts +207 -118
- package/dist/development/core/analytics/utils/flatten-connection.d.mts +11 -9
- package/dist/development/core/analytics/utils/flatten-connection.mjs +7 -6
- package/dist/development/core/analytics/utils/flatten-connection.mjs.map +1 -1
- package/dist/development/core/cache/fetch-with-cache.d.mts +24 -4
- package/dist/development/core/cache/fetch-with-cache.mjs +17 -2
- package/dist/development/core/cache/fetch-with-cache.mjs.map +1 -1
- package/dist/development/core/cache/key.d.mts +2 -0
- package/dist/development/core/cache/key.mjs.map +1 -1
- package/dist/development/core/cache/run-with-cache.d.mts +56 -8
- package/dist/development/core/cache/run-with-cache.mjs +17 -6
- package/dist/development/core/cache/run-with-cache.mjs.map +1 -1
- package/dist/development/core/cache/store.d.mts +8 -6
- package/dist/development/core/cache/store.mjs.map +1 -1
- package/dist/development/core/cache/strategies.d.mts +27 -2
- package/dist/development/core/cache/strategies.mjs +6 -0
- package/dist/development/core/cache/strategies.mjs.map +1 -1
- package/dist/development/core/cart/actions.d.mts +37 -30
- package/dist/development/core/cart/actions.mjs +21 -9
- package/dist/development/core/cart/actions.mjs.map +1 -1
- package/dist/development/core/cart/attach-quantity-input.d.mts +8 -7
- package/dist/development/core/cart/attach-quantity-input.mjs +8 -7
- package/dist/development/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/development/core/cart/buyer-identity-sync.d.mts +6 -5
- package/dist/development/core/cart/buyer-identity-sync.mjs.map +1 -1
- package/dist/development/core/cart/cart.d.mts +59 -46
- package/dist/development/core/cart/cart.mjs +46 -29
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/cart/cookie.d.mts +8 -4
- package/dist/development/core/cart/cookie.mjs +8 -4
- package/dist/development/core/cart/cookie.mjs.map +1 -1
- package/dist/development/core/cart/form.d.mts +34 -20
- package/dist/development/core/cart/form.mjs +6 -4
- package/dist/development/core/cart/form.mjs.map +1 -1
- package/dist/development/core/cart/get-cart.d.mts +27 -15
- package/dist/development/core/cart/get-cart.mjs +13 -8
- package/dist/development/core/cart/get-cart.mjs.map +1 -1
- package/dist/development/core/cart/quantity.d.mts +16 -5
- package/dist/development/core/cart/quantity.mjs +16 -5
- package/dist/development/core/cart/quantity.mjs.map +1 -1
- package/dist/development/core/cart/queries.d.mts +25 -19
- package/dist/development/core/cart/queries.mjs +15 -1
- package/dist/development/core/cart/queries.mjs.map +1 -1
- package/dist/development/core/cart/server-handlers.d.mts +82 -45
- package/dist/development/core/cart/server-handlers.mjs +4 -0
- package/dist/development/core/cart/server-handlers.mjs.map +1 -1
- package/dist/development/core/cart/state.d.mts +110 -64
- package/dist/development/core/cart/state.mjs +24 -6
- package/dist/development/core/cart/state.mjs.map +1 -1
- package/dist/development/core/collection/collection.d.mts +52 -53
- package/dist/development/core/collection/collection.mjs +5 -3
- package/dist/development/core/collection/collection.mjs.map +1 -1
- package/dist/development/core/collection/reconciler.d.mts +23 -16
- package/dist/development/core/collection/reconciler.mjs +8 -4
- package/dist/development/core/collection/reconciler.mjs.map +1 -1
- package/dist/development/core/collection/state.d.mts +31 -26
- package/dist/development/core/collection/state.mjs +4 -3
- package/dist/development/core/collection/state.mjs.map +1 -1
- package/dist/development/core/collection/url.d.mts +70 -29
- package/dist/development/core/collection/url.mjs +63 -25
- package/dist/development/core/collection/url.mjs.map +1 -1
- package/dist/development/core/constants.mjs.map +1 -1
- package/dist/development/core/logging/logging.d.mts +14 -8
- package/dist/development/core/logging/logging.mjs +15 -10
- package/dist/development/core/logging/logging.mjs.map +1 -1
- package/dist/development/core/logging/types.d.mts +28 -11
- package/dist/development/core/logging/types.mjs.map +1 -1
- package/dist/development/core/money/format.d.mts +6 -4
- package/dist/development/core/money/format.mjs +19 -9
- package/dist/development/core/money/format.mjs.map +1 -1
- package/dist/development/core/money/types.d.mts +41 -25
- package/dist/development/core/predictive-search/form.d.mts +37 -7
- package/dist/development/core/predictive-search/form.mjs +21 -3
- package/dist/development/core/predictive-search/form.mjs.map +1 -1
- package/dist/development/core/predictive-search/queries.d.mts +18 -30
- package/dist/development/core/predictive-search/queries.mjs +7 -3
- package/dist/development/core/predictive-search/queries.mjs.map +1 -1
- package/dist/development/core/predictive-search/search.d.mts +25 -27
- package/dist/development/core/predictive-search/search.mjs +20 -19
- package/dist/development/core/predictive-search/search.mjs.map +1 -1
- package/dist/development/core/predictive-search/server-handlers.d.mts +25 -26
- package/dist/development/core/predictive-search/server-handlers.mjs +4 -0
- package/dist/development/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/development/core/predictive-search/store.d.mts +65 -24
- package/dist/development/core/predictive-search/store.mjs +12 -1
- package/dist/development/core/predictive-search/store.mjs.map +1 -1
- package/dist/development/core/predictive-search/url.d.mts +36 -37
- package/dist/development/core/predictive-search/url.mjs +14 -6
- package/dist/development/core/predictive-search/url.mjs.map +1 -1
- package/dist/development/core/product/form.d.mts +63 -16
- package/dist/development/core/product/form.mjs +8 -2
- package/dist/development/core/product/form.mjs.map +1 -1
- package/dist/development/core/product/options.d.mts +8 -10
- package/dist/development/core/product/options.mjs +8 -10
- package/dist/development/core/product/options.mjs.map +1 -1
- package/dist/development/core/product/product-form.d.mts +77 -29
- package/dist/development/core/product/product-form.mjs +32 -10
- package/dist/development/core/product/product-form.mjs.map +1 -1
- package/dist/development/core/product/state.d.mts +55 -32
- package/dist/development/core/product/url.d.mts +19 -13
- package/dist/development/core/product/url.mjs +10 -10
- package/dist/development/core/product/url.mjs.map +1 -1
- package/dist/development/core/request-context.d.mts +77 -19
- package/dist/development/core/request-context.mjs +5 -1
- package/dist/development/core/request-context.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-redirects.d.mts +12 -4
- package/dist/development/core/request-routing/handle-shopify-redirects.mjs +9 -2
- package/dist/development/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.development.d.mts +9 -1
- package/dist/development/core/request-routing/handle-shopify-routes.development.mjs +7 -0
- package/dist/development/core/request-routing/handle-shopify-routes.development.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs +7 -5
- package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs +79 -43
- package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
- package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
- package/dist/development/core/request-routing/interceptors/url-redirects.mjs +6 -1
- package/dist/development/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/well-known.mjs.map +1 -1
- package/dist/development/core/request-routing/registered-routes.d.mts +12 -0
- package/dist/development/core/request-routing/registered-routes.mjs +12 -0
- package/dist/development/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/route-types.d.mts +43 -18
- package/dist/development/core/shop-pay/shop-pay.d.mts +71 -35
- package/dist/development/core/shop-pay/shop-pay.mjs +36 -10
- package/dist/development/core/shop-pay/shop-pay.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/index.d.mts +15 -5
- package/dist/development/core/shopify-scripts/index.mjs +15 -5
- package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/initialize.d.mts +12 -4
- package/dist/development/core/shopify-scripts/initialize.mjs +12 -4
- package/dist/development/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/types.d.mts +58 -25
- package/dist/development/core/standard-routes/build.d.mts +11 -12
- package/dist/development/core/standard-routes/build.mjs +11 -12
- package/dist/development/core/standard-routes/build.mjs.map +1 -1
- package/dist/development/core/standard-routes/types.d.mts +30 -24
- package/dist/development/core/url.mjs +2 -1
- package/dist/development/core/url.mjs.map +1 -1
- package/dist/development/customer-account/session.d.mts +45 -54
- package/dist/development/customer-account/session.mjs +20 -4
- package/dist/development/customer-account/session.mjs.map +1 -1
- package/dist/development/globals.d.mts +20 -11
- package/dist/development/graphql/generated/storefront-api-types.d.mts +127 -1
- package/dist/development/graphql/graphql.d.mts +17 -16
- package/dist/development/graphql/graphql.mjs +7 -8
- package/dist/development/graphql/graphql.mjs.map +1 -1
- package/dist/development/graphql/type-resolver.d.mts +15 -8
- package/dist/development/react/cart.d.mts +78 -32
- package/dist/development/react/cart.mjs +56 -29
- package/dist/development/react/cart.mjs.map +1 -1
- package/dist/development/react/collection.d.mts +40 -22
- package/dist/development/react/collection.mjs +27 -7
- package/dist/development/react/collection.mjs.map +1 -1
- package/dist/development/react/predictive-search.d.mts +65 -50
- package/dist/development/react/predictive-search.mjs +23 -21
- package/dist/development/react/predictive-search.mjs.map +1 -1
- package/dist/development/react/product.d.mts +45 -16
- package/dist/development/react/product.mjs +13 -11
- package/dist/development/react/product.mjs.map +1 -1
- package/dist/development/react/shop-pay.d.mts +15 -0
- package/dist/development/react/shop-pay.mjs +9 -0
- package/dist/development/react/shop-pay.mjs.map +1 -1
- package/dist/development/react/shopify-scripts.d.mts +23 -2
- package/dist/development/react/shopify-scripts.mjs +9 -0
- package/dist/development/react/shopify-scripts.mjs.map +1 -1
- package/dist/development/vue/cart.d.mts +28 -23
- package/dist/development/vue/cart.mjs +42 -25
- package/dist/development/vue/cart.mjs.map +1 -1
- package/dist/development/vue/collection.d.mts +20 -12
- package/dist/development/vue/collection.mjs +15 -7
- package/dist/development/vue/collection.mjs.map +1 -1
- package/dist/development/vue/index.d.mts +2 -2
- package/dist/development/vue/index.mjs +2 -2
- package/dist/development/vue/predictive-search.d.mts +31 -36
- package/dist/development/vue/predictive-search.mjs +16 -22
- package/dist/development/vue/predictive-search.mjs.map +1 -1
- package/dist/development/vue/product.d.mts +16 -15
- package/dist/development/vue/product.mjs +11 -11
- package/dist/development/vue/product.mjs.map +1 -1
- package/dist/development/vue/shop-pay.d.mts +14 -17
- package/dist/development/vue/shop-pay.mjs +8 -0
- package/dist/development/vue/shop-pay.mjs.map +1 -1
- package/dist/development/vue/shopify-scripts.d.mts +11 -0
- package/dist/development/vue/shopify-scripts.mjs +6 -0
- package/dist/development/vue/shopify-scripts.mjs.map +1 -1
- package/dist/globals.d.mts +20 -11
- package/dist/graphql/generated/storefront-api-types.d.mts +127 -1
- package/dist/graphql/graphql.d.mts +17 -16
- package/dist/graphql/graphql.mjs +7 -8
- package/dist/graphql/graphql.mjs.map +1 -1
- package/dist/graphql/type-resolver.d.mts +15 -8
- package/dist/react/cart.d.mts +78 -32
- package/dist/react/cart.mjs +56 -29
- package/dist/react/cart.mjs.map +1 -1
- package/dist/react/collection.d.mts +40 -22
- package/dist/react/collection.mjs +27 -7
- package/dist/react/collection.mjs.map +1 -1
- package/dist/react/predictive-search.d.mts +65 -50
- package/dist/react/predictive-search.mjs +23 -21
- package/dist/react/predictive-search.mjs.map +1 -1
- package/dist/react/product.d.mts +45 -16
- package/dist/react/product.mjs +13 -11
- package/dist/react/product.mjs.map +1 -1
- package/dist/react/shop-pay.d.mts +15 -0
- package/dist/react/shop-pay.mjs +9 -0
- package/dist/react/shop-pay.mjs.map +1 -1
- package/dist/react/shopify-scripts.d.mts +23 -2
- package/dist/react/shopify-scripts.mjs +9 -0
- package/dist/react/shopify-scripts.mjs.map +1 -1
- package/dist/ts-plugin/index.cjs +6 -0
- package/dist/ts-plugin/index.cjs.map +1 -1
- package/dist/ts-plugin/index.d.cts +6 -0
- package/dist/vite/local-https.d.mts +51 -17
- package/dist/vite/local-https.mjs +26 -5
- package/dist/vite/local-https.mjs.map +1 -1
- package/dist/vue/cart.d.mts +28 -23
- package/dist/vue/cart.mjs +42 -25
- package/dist/vue/cart.mjs.map +1 -1
- package/dist/vue/collection.d.mts +20 -12
- package/dist/vue/collection.mjs +15 -7
- package/dist/vue/collection.mjs.map +1 -1
- package/dist/vue/index.d.mts +2 -2
- package/dist/vue/index.mjs +2 -2
- package/dist/vue/predictive-search.d.mts +31 -36
- package/dist/vue/predictive-search.mjs +16 -22
- package/dist/vue/predictive-search.mjs.map +1 -1
- package/dist/vue/product.d.mts +16 -15
- package/dist/vue/product.mjs +11 -11
- package/dist/vue/product.mjs.map +1 -1
- package/dist/vue/shop-pay.d.mts +14 -17
- package/dist/vue/shop-pay.mjs +8 -0
- package/dist/vue/shop-pay.mjs.map +1 -1
- package/dist/vue/shopify-scripts.d.mts +11 -0
- package/dist/vue/shopify-scripts.mjs +6 -0
- package/dist/vue/shopify-scripts.mjs.map +1 -1
- package/package.json +3 -1
- package/skills/hydrogen-analytics/SKILL.md +1 -1
- package/skills/hydrogen-cart-ui/SKILL.md +28 -2
- package/skills/hydrogen-collection-browser/SKILL.md +9 -1
- package/skills/hydrogen-collection-browser/references/nextjs.md +1 -1
- package/skills/hydrogen-collection-browser/references/nuxt.md +1 -1
- package/skills/hydrogen-collection-browser/references/react.md +151 -32
- package/skills/hydrogen-customer-account/SKILL.md +2 -0
- package/skills/hydrogen-request-handlers/SKILL.md +21 -11
- package/skills/hydrogen-setup/references/analytics.md +14 -8
- package/skills/hydrogen-setup/steps/1-inspect-app.md +2 -2
- package/skills/hydrogen-setup/steps/6-product-detail-page.md +21 -11
- package/skills/hydrogen-storefront-client/SKILL.md +1 -1
- package/skills/hydrogen-storefront-client/references/caching.md +2 -0
- package/skills/hydrogen-variant-form/SKILL.md +25 -7
- package/skills/hydrogen-variant-form/references/nextjs.md +39 -4
- package/skills/hydrogen-variant-form/references/nuxt.md +83 -42
- package/skills/hydrogen-variant-form/references/react.md +45 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-with-cache.mjs","names":[],"sources":["../../../src/core/cache/fetch-with-cache.ts"],"sourcesContent":["import { HYDROGEN_CACHE_STATUS_PRODUCT } from \"./cache-status\";\nimport { type CacheKey } from \"./key\";\nimport {\n type CreateRunWithCacheOptions,\n type RunWithCache,\n type RunWithCacheResult,\n StaleFallbackDisabledError,\n createRunWithCache,\n} from \"./run-with-cache\";\nimport { type SerializableCacheValue } from \"./store\";\nimport { NO_STORE, type CachingStrategy } from \"./strategies\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\ntype FetchInput = Parameters<typeof fetch>[0];\ntype FetchInit = Parameters<typeof fetch>[1];\n\ntype SerializableResponse = SerializableCacheValue & {\n body: string;\n cacheable: boolean;\n headers: [string, string][];\n status: number;\n statusText: string;\n};\n\nexport type FetchCacheResponseContext = {\n ok: boolean;\n status: number;\n statusText: string;\n headers: Headers;\n url: string;\n text(): Promise<string>;\n json?: () => Promise<unknown>;\n};\n\nexport type FetchCacheOptions = {\n key: CacheKey;\n strategy: CachingStrategy;\n annotateCacheStatus?: boolean;\n /**\n * Runs only for OK responses with text-serializable bodies. Non-OK responses\n * and binary/streaming responses are never cached.\n */\n shouldCacheResponse?: (context: FetchCacheResponseContext) => MaybePromise<boolean>;\n};\n\ntype FetchWithCache = {\n (input: FetchInput, init?: FetchInit): Promise<Response>;\n (input: FetchInput, init: FetchInit | undefined, options: FetchCacheOptions): Promise<Response>;\n};\n\ntype CreateFetchWithCacheOptions = (\n | CreateRunWithCacheOptions\n | {\n runWithCache: RunWithCache;\n }\n) & {\n fetch?: typeof fetch;\n};\n\nconst excludedCachedResponseHeaders = new Set([\"set-cookie\", \"server-timing\"]);\nconst staleIfErrorStatuses = new Set([500, 502, 503, 504]);\n\nclass UnserializableResponseError extends StaleFallbackDisabledError {\n constructor() {\n super(\"Response body is not serializable for cache.\");\n }\n}\n\nclass StaleFallbackEligibleResponseError extends Error {\n constructor() {\n super(\"Origin returned an unserializable response eligible for stale-if-error fallback.\");\n }\n}\n\nclass ServerResponseError extends Error {\n readonly response: SerializableResponse;\n\n constructor(response: SerializableResponse) {\n super(\"Origin returned a response eligible for stale-if-error fallback.\");\n this.response = response;\n }\n}\n\nexport function createFetchWithCache({\n fetch: customFetch,\n ...options\n}: CreateFetchWithCacheOptions): FetchWithCache {\n const resolvedFetch = customFetch ?? globalThis.fetch;\n const runWithCache =\n \"runWithCache\" in options ? options.runWithCache : createRunWithCache(options);\n\n return async function fetchWithCache(\n input: FetchInput,\n init?: FetchInit,\n cacheOptions?: FetchCacheOptions,\n ): Promise<Response> {\n if (!cacheOptions) return resolvedFetch(input, init);\n\n if (cacheOptions.strategy.mode === NO_STORE) {\n return annotateResponse(\n await resolvedFetch(input, init),\n getCacheStatusHeader(\"fwd=bypass\"),\n cacheOptions,\n );\n }\n\n let passthroughResponse: Response | undefined;\n let result: RunWithCacheResult<SerializableResponse>;\n\n try {\n result = await runWithCache<SerializableResponse>(\n {\n key: cacheOptions.key,\n strategy: cacheOptions.strategy,\n },\n async () => {\n const response = await resolvedFetch(input, init);\n\n if (!response.ok) {\n if (isStaleIfErrorStatus(response.status)) {\n if (!canSerializeBodyAsText(response)) {\n passthroughResponse = response;\n throw new StaleFallbackEligibleResponseError();\n }\n\n throw new ServerResponseError(\n await serializeResponse(response, {\n cacheable: false,\n fallbackOnError: true,\n }),\n );\n }\n\n return {\n data: await serializeResponse(response, {\n cacheable: false,\n fallbackOnError: true,\n }),\n shouldCache: false,\n };\n }\n\n if (!canSerializeBodyAsText(response)) {\n passthroughResponse = response;\n\n // Signal bypass so the original streaming response can be returned\n // to this caller without attempting to serialize it for storage.\n throw new UnserializableResponseError();\n }\n\n const bodyReader = createBodyReader(response);\n const shouldCache = await shouldCacheResponse(response, bodyReader, cacheOptions);\n\n if (!shouldCache) {\n return {\n data: await serializeResponse(response, {\n bodyReader,\n cacheable: false,\n fallbackOnError: true,\n }),\n shouldCache: false,\n };\n }\n\n const data = await serializeResponse(response, {\n bodyReader,\n cacheable: true,\n fallbackOnError: true,\n });\n\n return {\n data,\n shouldCache: data.cacheable,\n };\n },\n );\n } catch (error) {\n if (error instanceof ServerResponseError) {\n return annotateResponse(\n fromSerializableResponse(error.response),\n getCacheStatusHeader(\"fwd=bypass\"),\n cacheOptions,\n );\n }\n\n if (error instanceof UnserializableResponseError) {\n return annotateResponse(\n // The raw Response body is not cache-serializable, so return the\n // original response. Refetch defensively if the response was not kept.\n passthroughResponse ?? (await resolvedFetch(input, init)),\n getCacheStatusHeader(\"fwd=bypass\"),\n cacheOptions,\n );\n }\n\n if (error instanceof StaleFallbackEligibleResponseError) {\n return annotateResponse(\n // Return the original unserializable response. This error type can\n // also trigger stale-if-error fallback when a stale entry exists.\n passthroughResponse ?? (await resolvedFetch(input, init)),\n getCacheStatusHeader(\"fwd=bypass\"),\n cacheOptions,\n );\n }\n\n throw error;\n }\n\n return annotateResponse(\n fromSerializableResponse(result.data),\n result.cacheStatus === \"hit\"\n ? getCacheStatusHeader(\"hit\")\n : getNonHitCacheStatusHeader(result.data.cacheable, result.cacheStatus),\n cacheOptions,\n );\n };\n}\n\nfunction shouldCacheResponse(\n response: Response,\n bodyReader: BodyReader,\n options: FetchCacheOptions,\n): MaybePromise<boolean> {\n const context: FetchCacheResponseContext = {\n ok: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: new Headers(response.headers),\n url: response.url,\n text: bodyReader.text,\n };\n\n if (isJsonResponse(response)) {\n context.json = bodyReader.json;\n }\n\n return options.shouldCacheResponse?.(context) ?? true;\n}\n\ntype BodyReader = {\n text(): Promise<string>;\n json(): Promise<unknown>;\n};\n\ntype SerializeResponseOptions = {\n bodyReader?: BodyReader;\n cacheable: boolean;\n fallbackOnError?: boolean;\n};\n\nfunction createBodyReader(response: Response): BodyReader {\n let bodyText: Promise<string> | undefined;\n let bodyJson: Promise<unknown> | undefined;\n\n const text = () => {\n bodyText ??= response.text();\n return bodyText;\n };\n\n return {\n text,\n json() {\n bodyJson ??= text().then((value) => JSON.parse(value));\n return bodyJson;\n },\n };\n}\n\nasync function serializeResponse(\n response: Response,\n { bodyReader, cacheable, fallbackOnError = false }: SerializeResponseOptions,\n): Promise<SerializableResponse> {\n if (!canSerializeBodyAsText(response)) return emptySerializableResponse(response);\n\n try {\n return {\n body: await (bodyReader ?? createBodyReader(response)).text(),\n cacheable,\n headers: getSerializableHeaders(response.headers, cacheable),\n status: response.status,\n statusText: response.statusText,\n };\n } catch (error) {\n if (!fallbackOnError) throw error;\n return emptySerializableResponse(response);\n }\n}\n\nfunction fromSerializableResponse(response: SerializableResponse): Response {\n return new Response(response.body, {\n headers: response.headers,\n status: response.status,\n statusText: response.statusText,\n });\n}\n\nfunction emptySerializableResponse(response: Response): SerializableResponse {\n return {\n body: \"\",\n cacheable: false,\n headers: [],\n status: response.status,\n statusText: response.statusText,\n };\n}\n\nfunction getSerializableHeaders(headers: Headers, cacheable: boolean): [string, string][] {\n if (!cacheable) return [...headers];\n\n // Cacheable serialized responses can be returned to future requests, so\n // per-request headers must not be copied.\n return [...headers].filter(([key]) => !excludedCachedResponseHeaders.has(key.toLowerCase()));\n}\n\nfunction annotateResponse(\n response: Response,\n cacheStatus: string,\n options: FetchCacheOptions,\n): Response {\n if (options.annotateCacheStatus === false) return response;\n\n const headers = new Headers(response.headers);\n headers.append(\"Cache-Status\", cacheStatus);\n\n return new Response(response.body, {\n headers,\n status: response.status,\n statusText: response.statusText,\n });\n}\n\nfunction getNonHitCacheStatusHeader(shouldCache: boolean, cacheStatus: \"miss\" | \"bypass\"): string {\n if (cacheStatus === \"bypass\") return getCacheStatusHeader(\"fwd=bypass\");\n return shouldCache\n ? getCacheStatusHeader(\"fwd=uri-miss; stored\")\n : getCacheStatusHeader(\"fwd=bypass\");\n}\n\nfunction getCacheStatusHeader(value: string): string {\n return `${HYDROGEN_CACHE_STATUS_PRODUCT}; ${value}`;\n}\n\nfunction isJsonResponse(response: Response): boolean {\n return response.headers.get(\"content-type\")?.toLowerCase().includes(\"json\") ?? false;\n}\n\nfunction isStaleIfErrorStatus(status: number): boolean {\n return staleIfErrorStatuses.has(status);\n}\n\nfunction canSerializeBodyAsText(response: Response): boolean {\n if (response.body == null) return true;\n\n const contentType = response.headers.get(\"content-type\");\n return contentType != null && /\\b(json|text|xml|html|graphql|javascript)\\b/i.test(contentType);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fetch-with-cache.mjs","names":[],"sources":["../../../src/core/cache/fetch-with-cache.ts"],"sourcesContent":["import { HYDROGEN_CACHE_STATUS_PRODUCT } from \"./cache-status\";\nimport { type CacheKey } from \"./key\";\nimport {\n type CreateRunWithCacheOptions,\n type RunWithCache,\n type RunWithCacheResult,\n StaleFallbackDisabledError,\n createRunWithCache,\n} from \"./run-with-cache\";\nimport { type SerializableCacheValue } from \"./store\";\nimport { NO_STORE, type CachingStrategy } from \"./strategies\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\ntype FetchInput = Parameters<typeof fetch>[0];\ntype FetchInit = Parameters<typeof fetch>[1];\n\ntype SerializableResponse = SerializableCacheValue & {\n body: string;\n cacheable: boolean;\n headers: [string, string][];\n status: number;\n statusText: string;\n};\n\nexport type FetchCacheResponseContext = {\n ok: boolean;\n status: number;\n statusText: string;\n headers: Headers;\n url: string;\n text(): Promise<string>;\n json?: () => Promise<unknown>;\n};\n\nexport type FetchCacheOptions = {\n /** Identifies the cached response. */\n key: CacheKey;\n /** Sets how long the response stays fresh, stale while revalidating, and stale after an error. */\n strategy: CachingStrategy;\n /** Set to `false` to leave the `Cache-Status` header off the response. */\n annotateCacheStatus?: boolean;\n /**\n * Decides whether to cache an OK response with a text body. Return `false` to skip caching the\n * response. Hydrogen never caches a response with an error status or a non-text body.\n */\n shouldCacheResponse?: (context: FetchCacheResponseContext) => MaybePromise<boolean>;\n /**\n * Creates the signal for a stale-while-revalidate refresh, which runs after\n * the caller already has the stale response. Without it, the refresh reuses\n * `init.signal`.\n */\n backgroundSignal?: () => AbortSignal;\n};\n\n/** A fetch function that caches the response when you pass cache options as the third argument. */\ntype FetchWithCache = {\n (input: FetchInput, init?: FetchInit): Promise<Response>;\n (input: FetchInput, init: FetchInit | undefined, options: FetchCacheOptions): Promise<Response>;\n};\n\n/** Pass a cache and an optional `waitUntil` function, or pass `runWithCache` from `createRunWithCache` to share its cache. Set `fetch` to replace the global fetch function. */\ntype CreateFetchWithCacheOptions = (\n | CreateRunWithCacheOptions\n | {\n runWithCache: RunWithCache;\n }\n) & {\n fetch?: typeof fetch;\n};\n\nconst excludedCachedResponseHeaders = new Set([\"set-cookie\", \"server-timing\"]);\nconst staleIfErrorStatuses = new Set([500, 502, 503, 504]);\n\nclass UnserializableResponseError extends StaleFallbackDisabledError {\n constructor() {\n super(\"Response body is not serializable for cache.\");\n }\n}\n\nclass StaleFallbackEligibleResponseError extends Error {\n constructor() {\n super(\"Origin returned an unserializable response eligible for stale-if-error fallback.\");\n }\n}\n\nclass ServerResponseError extends Error {\n readonly response: SerializableResponse;\n\n constructor(response: SerializableResponse) {\n super(\"Origin returned a response eligible for stale-if-error fallback.\");\n this.response = response;\n }\n}\n\n/**\n * Creates a fetch function that caches responses from third-party APIs. Call the function like\n * `fetch()`, and pass a `key` and `strategy` in the third argument to cache the response. Calls\n * without the third argument skip the cache. Return `false` from a `shouldCacheResponse` option to\n * skip caching one response.\n *\n * The function caches only OK responses with no body or with a JSON, text, XML, HTML, GraphQL, or JavaScript content type. Other responses skip the cache. The function removes the `set-cookie` and `server-timing` headers from each cached response, including the response that a cache miss returns. With `staleIfError`, the function serves a stale entry only after a `500`, `502`, `503`, or `504` status, or when the fetch throws.\n *\n * @param options - A cache, or the function from `createRunWithCache` to share its cache, plus an optional fetch function.\n * @returns A fetch function that adds a `Cache-Status` header to calls with cache options, unless `annotateCacheStatus` is `false`.\n * @publicDocs\n */\nexport function createFetchWithCache({\n fetch: customFetch,\n ...options\n}: CreateFetchWithCacheOptions): FetchWithCache {\n const resolvedFetch = customFetch ?? globalThis.fetch;\n const runWithCache =\n \"runWithCache\" in options ? options.runWithCache : createRunWithCache(options);\n\n return async function fetchWithCache(\n input: FetchInput,\n init?: FetchInit,\n cacheOptions?: FetchCacheOptions,\n ): Promise<Response> {\n if (!cacheOptions) return resolvedFetch(input, init);\n\n if (cacheOptions.strategy.mode === NO_STORE) {\n return annotateResponse(\n await resolvedFetch(input, init),\n getCacheStatusHeader(\"fwd=bypass\"),\n cacheOptions,\n );\n }\n\n let passthroughResponse: Response | undefined;\n let result: RunWithCacheResult<SerializableResponse>;\n\n try {\n result = await runWithCache<SerializableResponse>(\n {\n key: cacheOptions.key,\n strategy: cacheOptions.strategy,\n },\n async (context) => {\n const response = await resolvedFetch(\n input,\n context?.background && cacheOptions.backgroundSignal\n ? { ...init, signal: cacheOptions.backgroundSignal() }\n : init,\n );\n\n if (!response.ok) {\n if (isStaleIfErrorStatus(response.status)) {\n if (!canSerializeBodyAsText(response)) {\n passthroughResponse = response;\n throw new StaleFallbackEligibleResponseError();\n }\n\n throw new ServerResponseError(\n await serializeResponse(response, {\n cacheable: false,\n fallbackOnError: true,\n }),\n );\n }\n\n return {\n data: await serializeResponse(response, {\n cacheable: false,\n fallbackOnError: true,\n }),\n shouldCache: false,\n };\n }\n\n if (!canSerializeBodyAsText(response)) {\n passthroughResponse = response;\n\n // Signal bypass so the original streaming response can be returned\n // to this caller without attempting to serialize it for storage.\n throw new UnserializableResponseError();\n }\n\n const bodyReader = createBodyReader(response);\n const shouldCache = await shouldCacheResponse(response, bodyReader, cacheOptions);\n\n if (!shouldCache) {\n return {\n data: await serializeResponse(response, {\n bodyReader,\n cacheable: false,\n fallbackOnError: true,\n }),\n shouldCache: false,\n };\n }\n\n const data = await serializeResponse(response, {\n bodyReader,\n cacheable: true,\n fallbackOnError: true,\n });\n\n return {\n data,\n shouldCache: data.cacheable,\n };\n },\n );\n } catch (error) {\n if (error instanceof ServerResponseError) {\n return annotateResponse(\n fromSerializableResponse(error.response),\n getCacheStatusHeader(\"fwd=bypass\"),\n cacheOptions,\n );\n }\n\n if (error instanceof UnserializableResponseError) {\n return annotateResponse(\n // The raw Response body is not cache-serializable, so return the\n // original response. Refetch defensively if the response was not kept.\n passthroughResponse ?? (await resolvedFetch(input, init)),\n getCacheStatusHeader(\"fwd=bypass\"),\n cacheOptions,\n );\n }\n\n if (error instanceof StaleFallbackEligibleResponseError) {\n return annotateResponse(\n // Return the original unserializable response. This error type can\n // also trigger stale-if-error fallback when a stale entry exists.\n passthroughResponse ?? (await resolvedFetch(input, init)),\n getCacheStatusHeader(\"fwd=bypass\"),\n cacheOptions,\n );\n }\n\n throw error;\n }\n\n return annotateResponse(\n fromSerializableResponse(result.data),\n result.cacheStatus === \"hit\"\n ? getCacheStatusHeader(\"hit\")\n : getNonHitCacheStatusHeader(result.data.cacheable, result.cacheStatus),\n cacheOptions,\n );\n };\n}\n\nfunction shouldCacheResponse(\n response: Response,\n bodyReader: BodyReader,\n options: FetchCacheOptions,\n): MaybePromise<boolean> {\n const context: FetchCacheResponseContext = {\n ok: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: new Headers(response.headers),\n url: response.url,\n text: bodyReader.text,\n };\n\n if (isJsonResponse(response)) {\n context.json = bodyReader.json;\n }\n\n return options.shouldCacheResponse?.(context) ?? true;\n}\n\ntype BodyReader = {\n text(): Promise<string>;\n json(): Promise<unknown>;\n};\n\ntype SerializeResponseOptions = {\n bodyReader?: BodyReader;\n cacheable: boolean;\n fallbackOnError?: boolean;\n};\n\nfunction createBodyReader(response: Response): BodyReader {\n let bodyText: Promise<string> | undefined;\n let bodyJson: Promise<unknown> | undefined;\n\n const text = () => {\n bodyText ??= response.text();\n return bodyText;\n };\n\n return {\n text,\n json() {\n bodyJson ??= text().then((value) => JSON.parse(value));\n return bodyJson;\n },\n };\n}\n\nasync function serializeResponse(\n response: Response,\n { bodyReader, cacheable, fallbackOnError = false }: SerializeResponseOptions,\n): Promise<SerializableResponse> {\n if (!canSerializeBodyAsText(response)) return emptySerializableResponse(response);\n\n try {\n return {\n body: await (bodyReader ?? createBodyReader(response)).text(),\n cacheable,\n headers: getSerializableHeaders(response.headers, cacheable),\n status: response.status,\n statusText: response.statusText,\n };\n } catch (error) {\n if (!fallbackOnError) throw error;\n return emptySerializableResponse(response);\n }\n}\n\nfunction fromSerializableResponse(response: SerializableResponse): Response {\n return new Response(response.body, {\n headers: response.headers,\n status: response.status,\n statusText: response.statusText,\n });\n}\n\nfunction emptySerializableResponse(response: Response): SerializableResponse {\n return {\n body: \"\",\n cacheable: false,\n headers: [],\n status: response.status,\n statusText: response.statusText,\n };\n}\n\nfunction getSerializableHeaders(headers: Headers, cacheable: boolean): [string, string][] {\n if (!cacheable) return [...headers];\n\n // Cacheable serialized responses can be returned to future requests, so\n // per-request headers must not be copied.\n return [...headers].filter(([key]) => !excludedCachedResponseHeaders.has(key.toLowerCase()));\n}\n\nfunction annotateResponse(\n response: Response,\n cacheStatus: string,\n options: FetchCacheOptions,\n): Response {\n if (options.annotateCacheStatus === false) return response;\n\n const headers = new Headers(response.headers);\n headers.append(\"Cache-Status\", cacheStatus);\n\n return new Response(response.body, {\n headers,\n status: response.status,\n statusText: response.statusText,\n });\n}\n\nfunction getNonHitCacheStatusHeader(shouldCache: boolean, cacheStatus: \"miss\" | \"bypass\"): string {\n if (cacheStatus === \"bypass\") return getCacheStatusHeader(\"fwd=bypass\");\n return shouldCache\n ? getCacheStatusHeader(\"fwd=uri-miss; stored\")\n : getCacheStatusHeader(\"fwd=bypass\");\n}\n\nfunction getCacheStatusHeader(value: string): string {\n return `${HYDROGEN_CACHE_STATUS_PRODUCT}; ${value}`;\n}\n\nfunction isJsonResponse(response: Response): boolean {\n return response.headers.get(\"content-type\")?.toLowerCase().includes(\"json\") ?? false;\n}\n\nfunction isStaleIfErrorStatus(status: number): boolean {\n return staleIfErrorStatuses.has(status);\n}\n\nfunction canSerializeBodyAsText(response: Response): boolean {\n if (response.body == null) return true;\n\n const contentType = response.headers.get(\"content-type\");\n return contentType != null && /\\b(json|text|xml|html|graphql|javascript)\\b/i.test(contentType);\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
//#region src/core/cache/key.d.ts
|
|
2
|
+
/** One entry in an array cache key. The number `1` and the string `"1"` produce different keys. */
|
|
2
3
|
type CacheKeyPrimitive = string | number | boolean | null;
|
|
4
|
+
/** Identifies a cache entry, as a string or an array of strings, numbers, booleans, and `null`. Include every value that changes the cached data. */
|
|
3
5
|
type CacheKey = string | readonly CacheKeyPrimitive[];
|
|
4
6
|
//#endregion
|
|
5
7
|
export { CacheKey };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key.mjs","names":[],"sources":["../../../src/core/cache/key.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"key.mjs","names":[],"sources":["../../../src/core/cache/key.ts"],"sourcesContent":["/** One entry in an array cache key. The number `1` and the string `\"1\"` produce different keys. */\nexport type CacheKeyPrimitive = string | number | boolean | null;\n\n/** Identifies a cache entry, as a string or an array of strings, numbers, booleans, and `null`. Include every value that changes the cached data. */\nexport type CacheKey = string | readonly CacheKeyPrimitive[];\n\nconst textEncoder = new TextEncoder();\n\nexport async function hashCacheKey(key: CacheKey): Promise<string> {\n const serialized = typeof key === \"string\" ? `string:${key}` : `array:${serializeArrayKey(key)}`;\n const bytes = await crypto.subtle.digest(\"SHA-256\", textEncoder.encode(serialized));\n\n return `h2:${hexEncode(bytes)}`;\n}\n\nfunction serializeArrayKey(value: readonly unknown[]): string {\n const values: string[] = [];\n for (let index = 0; index < value.length; index++) {\n if (!(index in value)) {\n throw new TypeError(\"Cache key arrays cannot contain empty slots.\");\n }\n\n values.push(serializeCacheKeyPrimitive(value[index]));\n }\n\n return `[${values.join(\",\")}]`;\n}\n\nfunction serializeCacheKeyPrimitive(value: unknown): string {\n if (value === null) return \"null\";\n if (typeof value === \"string\") return JSON.stringify(value);\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n\n throw new TypeError(\n `Cache key arrays must contain only primitive values. Received: ${typeof value}`,\n );\n}\n\nfunction hexEncode(bytes: ArrayBuffer): string {\n return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"],"mappings":";AAMA,MAAM,cAAc,IAAI,aAAa;AAErC,eAAsB,aAAa,KAAgC;CACjE,MAAM,aAAa,OAAO,QAAQ,WAAW,UAAU,QAAQ,SAAS,kBAAkB,IAAI;AAG9F,QAAO,MAAM,UAAU,MAFH,OAAO,OAAO,OAAO,WAAW,YAAY,OAAO,WAAW,CAAC,CAEtD;;AAG/B,SAAS,kBAAkB,OAAmC;CAC5D,MAAM,SAAmB,EAAE;AAC3B,MAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS;AACjD,MAAI,EAAE,SAAS,OACb,OAAM,IAAI,UAAU,+CAA+C;AAGrE,SAAO,KAAK,2BAA2B,MAAM,OAAO,CAAC;;AAGvD,QAAO,IAAI,OAAO,KAAK,IAAI,CAAC;;AAG9B,SAAS,2BAA2B,OAAwB;AAC1D,KAAI,UAAU,KAAM,QAAO;AAC3B,KAAI,OAAO,UAAU,SAAU,QAAO,KAAK,UAAU,MAAM;AAC3D,KAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAChD,QAAO,OAAO,MAAM;AAGtB,OAAM,IAAI,UACR,kEAAkE,OAAO,QAC1E;;AAGH,SAAS,UAAU,OAA4B;AAC7C,QAAO,CAAC,GAAG,IAAI,WAAW,MAAM,CAAC,CAAC,KAAK,SAAS,KAAK,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG"}
|
|
@@ -3,30 +3,78 @@ import { CachingStrategy } from "./strategies.mjs";
|
|
|
3
3
|
import { KeyValueCacheLike, SerializableCacheValue, WebCacheLike } from "./store.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/core/cache/run-with-cache.d.ts
|
|
6
|
+
/** A value, or a promise that resolves to the value. */
|
|
6
7
|
type MaybePromise<T> = T | Promise<T>;
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Keeps the runtime alive until a background cache write or refresh finishes, such as a worker's `waitUntil` function. Hydrogen ignores errors that the function throws.
|
|
10
|
+
*/
|
|
11
|
+
type WaitUntil =
|
|
12
|
+
/**
|
|
13
|
+
* @param promise - The background cache write or refresh to wait for.
|
|
14
|
+
* @returns Nothing. Hydrogen ignores the return value.
|
|
15
|
+
*/
|
|
16
|
+
(promise: Promise<unknown>) => void;
|
|
17
|
+
/** The cache that holds entries, either a Web Cache API cache or a key-value store. */
|
|
8
18
|
type CacheInstance = WebCacheLike | KeyValueCacheLike;
|
|
19
|
+
/** The cache and the background-work function for `createRunWithCache`. */
|
|
9
20
|
type CreateRunWithCacheOptions = {
|
|
10
|
-
cache: CacheInstance;
|
|
21
|
+
/** The cache that holds entries. `createRunWithCache` throws a `TypeError` when the cache is neither a Web Cache API cache nor a key-value store. */cache: CacheInstance;
|
|
22
|
+
/**
|
|
23
|
+
* Keeps the runtime alive for background cache writes and refreshes. Without `waitUntil`, each call waits for its cache write before it resolves.
|
|
24
|
+
*/
|
|
11
25
|
waitUntil?: WaitUntil;
|
|
12
26
|
};
|
|
27
|
+
/** Options for one cached run. */
|
|
13
28
|
type RunWithCacheOptions = {
|
|
14
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Identifies the cache entry. `runWithCache` throws a `TypeError` for an array key with empty slots or with entries other than strings, numbers, booleans, and `null`.
|
|
31
|
+
*/
|
|
32
|
+
key: CacheKey; /** Sets how long the result stays fresh, how long the cache serves it stale while refreshing it, and how long the cache serves it stale after an error. */
|
|
15
33
|
strategy: CachingStrategy;
|
|
16
34
|
};
|
|
35
|
+
/** The callback's return value, with the data and whether to cache the data. */
|
|
17
36
|
type CacheDecision<T extends SerializableCacheValue> = {
|
|
18
37
|
/**
|
|
19
|
-
*
|
|
20
|
-
* through `JSON.stringify`; nested serializability is the caller's contract.
|
|
38
|
+
* The data to return and cache. Web Cache stores save the data as JSON.
|
|
21
39
|
*/
|
|
22
|
-
data: T;
|
|
40
|
+
data: T; /** Set to `false` to return the data without storing it. */
|
|
23
41
|
shouldCache: boolean;
|
|
24
42
|
};
|
|
43
|
+
/** The data, and whether the data came from the cache or the callback. */
|
|
25
44
|
type RunWithCacheResult<T extends SerializableCacheValue> = {
|
|
26
|
-
data: T;
|
|
45
|
+
/** The cached data on a hit, or the callback's data otherwise. */data: T; /** `hit` when the data comes from the cache, `miss` when the callback runs, and `bypass` for a `no-store` strategy. */
|
|
27
46
|
cacheStatus: "hit" | "miss" | "bypass";
|
|
28
47
|
};
|
|
29
|
-
|
|
48
|
+
/** The context that `runWithCache` passes to its callback. */
|
|
49
|
+
type RunWithCacheContext = {
|
|
50
|
+
/** `true` when the callback refreshes a stale entry after the cache already returned the stale value. */background: boolean;
|
|
51
|
+
};
|
|
52
|
+
/** Produces the data for a cached run and decides whether to cache the data. */
|
|
53
|
+
type RunCallback<T extends SerializableCacheValue> =
|
|
54
|
+
/**
|
|
55
|
+
* @param context - Tells the callback whether it refreshes a stale entry in the background.
|
|
56
|
+
* @returns The data and whether to cache the data, or a promise that resolves to both.
|
|
57
|
+
*/
|
|
58
|
+
(context?: RunWithCacheContext) => MaybePromise<CacheDecision<T>>;
|
|
59
|
+
/**
|
|
60
|
+
* Returns cached data for the key, or runs the callback and caches the result. Resolves with the
|
|
61
|
+
* data and the cache status.
|
|
62
|
+
*/
|
|
63
|
+
type RunWithCache =
|
|
64
|
+
/**
|
|
65
|
+
* @param options - The cache key and caching strategy for this call.
|
|
66
|
+
* @param run - Produces the data and decides whether to cache it. Runs whenever the cache has no fresh entry. The context's `background` field is `true` during a stale-while-revalidate refresh.
|
|
67
|
+
* @returns The data and the cache status.
|
|
68
|
+
*/
|
|
69
|
+
<T extends SerializableCacheValue>(options: RunWithCacheOptions, run: RunCallback<T>) => Promise<RunWithCacheResult<T>>;
|
|
70
|
+
/**
|
|
71
|
+
* Creates a function that caches the result of any async work, such as data that several API calls
|
|
72
|
+
* produce together. For a single fetch response, use `createFetchWithCache`.
|
|
73
|
+
*
|
|
74
|
+
* @param options - The cache, and the function that keeps the runtime alive for background cache writes.
|
|
75
|
+
* @returns A function that returns cached data or runs your callback, and reports the cache status.
|
|
76
|
+
* @publicDocs
|
|
77
|
+
*/
|
|
30
78
|
declare function createRunWithCache({
|
|
31
79
|
cache,
|
|
32
80
|
waitUntil
|
|
@@ -9,11 +9,19 @@ import { createNormalizedCacheStore } from "./store.mjs";
|
|
|
9
9
|
* catches refresh failures, but it should not hide intentional bypasses.
|
|
10
10
|
*/
|
|
11
11
|
var StaleFallbackDisabledError = class extends Error {};
|
|
12
|
+
/**
|
|
13
|
+
* Creates a function that caches the result of any async work, such as data that several API calls
|
|
14
|
+
* produce together. For a single fetch response, use `createFetchWithCache`.
|
|
15
|
+
*
|
|
16
|
+
* @param options - The cache, and the function that keeps the runtime alive for background cache writes.
|
|
17
|
+
* @returns A function that returns cached data or runs your callback, and reports the cache status.
|
|
18
|
+
* @publicDocs
|
|
19
|
+
*/
|
|
12
20
|
function createRunWithCache({ cache, waitUntil }) {
|
|
13
21
|
const store = createNormalizedCacheStore(cache);
|
|
14
22
|
return async function runWithCache(options, run) {
|
|
15
23
|
if (options.strategy.mode === "no-store") return {
|
|
16
|
-
data: (await runAndValidate(run)).data,
|
|
24
|
+
data: (await runAndValidate(run, { background: false })).data,
|
|
17
25
|
cacheStatus: "bypass"
|
|
18
26
|
};
|
|
19
27
|
const cacheKey = await hashCacheKey(options.key);
|
|
@@ -39,7 +47,10 @@ function createRunWithCache({ cache, waitUntil }) {
|
|
|
39
47
|
return runAndMaybeStore(cacheKey, options, run).result;
|
|
40
48
|
};
|
|
41
49
|
function revalidateInBackground(cacheKey, options, run) {
|
|
42
|
-
schedule(runAndMaybeStore(cacheKey, options, run, {
|
|
50
|
+
schedule(runAndMaybeStore(cacheKey, options, run, {
|
|
51
|
+
scheduleCacheWrite: false,
|
|
52
|
+
background: true
|
|
53
|
+
}).complete);
|
|
43
54
|
}
|
|
44
55
|
async function refreshWithStaleFallback(cacheKey, staleValue, options, run) {
|
|
45
56
|
try {
|
|
@@ -52,10 +63,10 @@ function createRunWithCache({ cache, waitUntil }) {
|
|
|
52
63
|
};
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
|
-
function runAndMaybeStore(key, options, run, { scheduleCacheWrite = true } = {}) {
|
|
66
|
+
function runAndMaybeStore(key, options, run, { scheduleCacheWrite = true, background = false } = {}) {
|
|
56
67
|
let storePromise = Promise.resolve();
|
|
57
68
|
const result = Promise.resolve().then(async () => {
|
|
58
|
-
const decision = await runAndValidate(run);
|
|
69
|
+
const decision = await runAndValidate(run, { background });
|
|
59
70
|
if (!shouldStore(options.strategy, decision)) return {
|
|
60
71
|
data: decision.data,
|
|
61
72
|
cacheStatus: "miss"
|
|
@@ -93,8 +104,8 @@ function shouldStore(strategy, decision) {
|
|
|
93
104
|
function shouldFallbackToStale(error) {
|
|
94
105
|
return !(error instanceof StaleFallbackDisabledError);
|
|
95
106
|
}
|
|
96
|
-
async function runAndValidate(run) {
|
|
97
|
-
const decision = await run();
|
|
107
|
+
async function runAndValidate(run, context) {
|
|
108
|
+
const decision = await run(context);
|
|
98
109
|
if (typeof decision !== "object" || decision == null || !("data" in decision) || !("shouldCache" in decision) || typeof decision.shouldCache !== "boolean") throw new TypeError("runWithCache callback must return {data, shouldCache}.");
|
|
99
110
|
return decision;
|
|
100
111
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-with-cache.mjs","names":[],"sources":["../../../src/core/cache/run-with-cache.ts"],"sourcesContent":["import { hashCacheKey, type CacheKey } from \"./key\";\nimport {\n type CacheEnvelope,\n type KeyValueCacheLike,\n type SerializableCacheValue,\n type WebCacheLike,\n createNormalizedCacheStore,\n} from \"./store\";\nimport { NO_STORE, type CachingStrategy, getCacheRetentionTtl } from \"./strategies\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\nexport type WaitUntil = (promise: Promise<unknown>) => void;\n\nexport type CacheInstance = WebCacheLike | KeyValueCacheLike;\n\nexport type CreateRunWithCacheOptions = {\n cache: CacheInstance;\n waitUntil?: WaitUntil;\n};\n\nexport type RunWithCacheOptions = {\n key: CacheKey;\n strategy: CachingStrategy;\n};\n\nexport type CacheDecision<T extends SerializableCacheValue> = {\n /**\n * Must be a JSON-serializable object. The cache store may persist this value\n * through `JSON.stringify`; nested serializability is the caller's contract.\n */\n data: T;\n shouldCache: boolean;\n};\n\nexport type RunWithCacheResult<T extends SerializableCacheValue> = {\n data: T;\n cacheStatus: \"hit\" | \"miss\" | \"bypass\";\n};\n\nexport type RunWithCache = <T extends SerializableCacheValue>(\n options: RunWithCacheOptions,\n run: () => MaybePromise<CacheDecision<T>>,\n) => Promise<RunWithCacheResult<T>>;\n\ntype CacheState = \"fresh\" | \"stale\" | \"stale-if-error\" | \"expired\";\n\n/**\n * `result` resolves as soon as user data is available; `complete` also waits for\n * any cache write so background revalidation can be scheduled as one operation.\n */\ntype CacheOperation<T extends SerializableCacheValue = SerializableCacheValue> = {\n result: Promise<RunWithCacheResult<T>>;\n complete: Promise<void>;\n};\n\ntype RunAndMaybeStoreOptions = {\n scheduleCacheWrite?: boolean;\n};\n\n/**\n * Internal control-flow error for adapters that need to escape the cache\n * pipeline without being interpreted as an origin failure. `staleIfError`\n * catches refresh failures, but it should not hide intentional bypasses.\n */\nexport class StaleFallbackDisabledError extends Error {}\n\nexport function createRunWithCache({ cache, waitUntil }: CreateRunWithCacheOptions): RunWithCache {\n const store = createNormalizedCacheStore(cache);\n\n return async function runWithCache<T extends SerializableCacheValue>(\n options: RunWithCacheOptions,\n run: () => MaybePromise<CacheDecision<T>>,\n ): Promise<RunWithCacheResult<T>> {\n if (options.strategy.mode === NO_STORE) {\n return { data: (await runAndValidate(run)).data, cacheStatus: \"bypass\" };\n }\n\n const cacheKey = await hashCacheKey(options.key);\n\n let cached: CacheEnvelope<T> | undefined;\n\n try {\n cached = await store.get<T>(cacheKey);\n } catch {}\n\n if (cached) {\n const state = getCacheState(cached, options.strategy);\n\n if (state === \"fresh\") return { data: cached.value, cacheStatus: \"hit\" };\n\n if (state === \"stale\") {\n revalidateInBackground(cacheKey, options, run);\n return { data: cached.value, cacheStatus: \"hit\" };\n }\n\n if (state === \"stale-if-error\") {\n return refreshWithStaleFallback(cacheKey, cached.value, options, run);\n }\n }\n\n return runAndMaybeStore(cacheKey, options, run).result;\n };\n\n function revalidateInBackground<T extends SerializableCacheValue>(\n cacheKey: string,\n options: RunWithCacheOptions,\n run: () => MaybePromise<CacheDecision<T>>,\n ) {\n const entry = runAndMaybeStore(cacheKey, options, run, { scheduleCacheWrite: false });\n schedule(entry.complete);\n }\n\n async function refreshWithStaleFallback<T extends SerializableCacheValue>(\n cacheKey: string,\n staleValue: T,\n options: RunWithCacheOptions,\n run: () => MaybePromise<CacheDecision<T>>,\n ): Promise<RunWithCacheResult<T>> {\n try {\n return await runAndMaybeStore(cacheKey, options, run).result;\n } catch (error) {\n if (!shouldFallbackToStale(error)) throw error;\n\n return { data: staleValue, cacheStatus: \"hit\" };\n }\n }\n\n function runAndMaybeStore<T extends SerializableCacheValue>(\n key: string,\n options: RunWithCacheOptions,\n run: () => MaybePromise<CacheDecision<T>>,\n { scheduleCacheWrite = true }: RunAndMaybeStoreOptions = {},\n ): CacheOperation<T> {\n // Assigned inside `result` once the callback has produced cacheable data.\n // `complete` reads the same variable later, so it follows the actual write\n // without making the foreground result wait when waitUntil is available.\n let storePromise: Promise<void> = Promise.resolve();\n\n const result = Promise.resolve().then(async (): Promise<RunWithCacheResult<T>> => {\n const decision = await runAndValidate(run);\n if (!shouldStore(options.strategy, decision)) {\n return { data: decision.data, cacheStatus: \"miss\" };\n }\n\n storePromise = storeDecision(key, options, decision).catch(() => {});\n\n if (waitUntil && scheduleCacheWrite) {\n schedule(storePromise);\n } else if (!waitUntil) {\n await storePromise;\n }\n\n return { data: decision.data, cacheStatus: \"miss\" };\n });\n\n return {\n result,\n complete: result\n .then(\n () => storePromise,\n () => undefined,\n )\n .then(() => undefined),\n };\n }\n\n async function storeDecision<T extends SerializableCacheValue>(\n key: string,\n options: RunWithCacheOptions,\n decision: CacheDecision<T>,\n ) {\n await store.set(\n key,\n {\n version: 1,\n value: decision.data,\n storedAt: Date.now(),\n strategy: options.strategy,\n },\n {\n strategy: options.strategy,\n },\n );\n }\n\n function schedule(promise: Promise<unknown>) {\n try {\n waitUntil?.(promise);\n } catch {}\n }\n}\n\nfunction shouldStore<T extends SerializableCacheValue>(\n strategy: CachingStrategy,\n decision: CacheDecision<T>,\n): boolean {\n return decision.shouldCache && getCacheRetentionTtl(strategy) > 0;\n}\n\nfunction shouldFallbackToStale(error: unknown): boolean {\n return !(error instanceof StaleFallbackDisabledError);\n}\n\nasync function runAndValidate<T extends SerializableCacheValue>(\n run: () => MaybePromise<CacheDecision<T>>,\n): Promise<CacheDecision<T>> {\n const decision = await run();\n\n if (\n typeof decision !== \"object\" ||\n decision == null ||\n !(\"data\" in decision) ||\n !(\"shouldCache\" in decision) ||\n typeof decision.shouldCache !== \"boolean\"\n ) {\n throw new TypeError(\"runWithCache callback must return {data, shouldCache}.\");\n }\n\n return decision;\n}\n\nfunction getCacheState<T extends SerializableCacheValue>(\n envelope: CacheEnvelope<T>,\n strategy: CachingStrategy,\n): CacheState {\n const age = Math.max(0, (Date.now() - envelope.storedAt) / 1000);\n const maxAge = strategy.maxAge ?? 0;\n const staleWhileRevalidate = strategy.staleWhileRevalidate ?? 0;\n const staleIfError = strategy.staleIfError ?? 0;\n\n if (age <= maxAge) return \"fresh\";\n if (age <= maxAge + staleWhileRevalidate) return \"stale\";\n if (age <= maxAge + staleWhileRevalidate + staleIfError) {\n return \"stale-if-error\";\n }\n\n return \"expired\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"run-with-cache.mjs","names":[],"sources":["../../../src/core/cache/run-with-cache.ts"],"sourcesContent":["import { hashCacheKey, type CacheKey } from \"./key\";\nimport {\n type CacheEnvelope,\n type KeyValueCacheLike,\n type SerializableCacheValue,\n type WebCacheLike,\n createNormalizedCacheStore,\n} from \"./store\";\nimport { NO_STORE, type CachingStrategy, getCacheRetentionTtl } from \"./strategies\";\n\n/** A value, or a promise that resolves to the value. */\ntype MaybePromise<T> = T | Promise<T>;\n\n/**\n * Keeps the runtime alive until a background cache write or refresh finishes, such as a worker's `waitUntil` function. Hydrogen ignores errors that the function throws.\n */\nexport type WaitUntil =\n /**\n * @param promise - The background cache write or refresh to wait for.\n * @returns Nothing. Hydrogen ignores the return value.\n */\n (promise: Promise<unknown>) => void;\n\n/** The cache that holds entries, either a Web Cache API cache or a key-value store. */\nexport type CacheInstance = WebCacheLike | KeyValueCacheLike;\n\n/** The cache and the background-work function for `createRunWithCache`. */\nexport type CreateRunWithCacheOptions = {\n /** The cache that holds entries. `createRunWithCache` throws a `TypeError` when the cache is neither a Web Cache API cache nor a key-value store. */\n cache: CacheInstance;\n /**\n * Keeps the runtime alive for background cache writes and refreshes. Without `waitUntil`, each call waits for its cache write before it resolves.\n */\n waitUntil?: WaitUntil;\n};\n\n/** Options for one cached run. */\nexport type RunWithCacheOptions = {\n /**\n * Identifies the cache entry. `runWithCache` throws a `TypeError` for an array key with empty slots or with entries other than strings, numbers, booleans, and `null`.\n */\n key: CacheKey;\n /** Sets how long the result stays fresh, how long the cache serves it stale while refreshing it, and how long the cache serves it stale after an error. */\n strategy: CachingStrategy;\n};\n\n/** The callback's return value, with the data and whether to cache the data. */\nexport type CacheDecision<T extends SerializableCacheValue> = {\n /**\n * The data to return and cache. Web Cache stores save the data as JSON.\n */\n data: T;\n /** Set to `false` to return the data without storing it. */\n shouldCache: boolean;\n};\n\n/** The data, and whether the data came from the cache or the callback. */\nexport type RunWithCacheResult<T extends SerializableCacheValue> = {\n /** The cached data on a hit, or the callback's data otherwise. */\n data: T;\n /** `hit` when the data comes from the cache, `miss` when the callback runs, and `bypass` for a `no-store` strategy. */\n cacheStatus: \"hit\" | \"miss\" | \"bypass\";\n};\n\n/** The context that `runWithCache` passes to its callback. */\nexport type RunWithCacheContext = {\n /** `true` when the callback refreshes a stale entry after the cache already returned the stale value. */\n background: boolean;\n};\n\n// The context is optional so custom runners that call `run()` keep working.\n/** Produces the data for a cached run and decides whether to cache the data. */\ntype RunCallback<T extends SerializableCacheValue> =\n /**\n * @param context - Tells the callback whether it refreshes a stale entry in the background.\n * @returns The data and whether to cache the data, or a promise that resolves to both.\n */\n (context?: RunWithCacheContext) => MaybePromise<CacheDecision<T>>;\n\n/**\n * Returns cached data for the key, or runs the callback and caches the result. Resolves with the\n * data and the cache status.\n */\nexport type RunWithCache =\n /**\n * @param options - The cache key and caching strategy for this call.\n * @param run - Produces the data and decides whether to cache it. Runs whenever the cache has no fresh entry. The context's `background` field is `true` during a stale-while-revalidate refresh.\n * @returns The data and the cache status.\n */\n <T extends SerializableCacheValue>(\n options: RunWithCacheOptions,\n run: RunCallback<T>,\n ) => Promise<RunWithCacheResult<T>>;\n\ntype CacheState = \"fresh\" | \"stale\" | \"stale-if-error\" | \"expired\";\n\n/**\n * `result` resolves as soon as user data is available; `complete` also waits for\n * any cache write so background revalidation can be scheduled as one operation.\n */\ntype CacheOperation<T extends SerializableCacheValue = SerializableCacheValue> = {\n result: Promise<RunWithCacheResult<T>>;\n complete: Promise<void>;\n};\n\ntype RunAndMaybeStoreOptions = {\n scheduleCacheWrite?: boolean;\n background?: boolean;\n};\n\n/**\n * Internal control-flow error for adapters that need to escape the cache\n * pipeline without being interpreted as an origin failure. `staleIfError`\n * catches refresh failures, but it should not hide intentional bypasses.\n */\nexport class StaleFallbackDisabledError extends Error {}\n\n/**\n * Creates a function that caches the result of any async work, such as data that several API calls\n * produce together. For a single fetch response, use `createFetchWithCache`.\n *\n * @param options - The cache, and the function that keeps the runtime alive for background cache writes.\n * @returns A function that returns cached data or runs your callback, and reports the cache status.\n * @publicDocs\n */\nexport function createRunWithCache({ cache, waitUntil }: CreateRunWithCacheOptions): RunWithCache {\n const store = createNormalizedCacheStore(cache);\n\n return async function runWithCache<T extends SerializableCacheValue>(\n options: RunWithCacheOptions,\n run: RunCallback<T>,\n ): Promise<RunWithCacheResult<T>> {\n if (options.strategy.mode === NO_STORE) {\n return {\n data: (await runAndValidate(run, { background: false })).data,\n cacheStatus: \"bypass\",\n };\n }\n\n const cacheKey = await hashCacheKey(options.key);\n\n let cached: CacheEnvelope<T> | undefined;\n\n try {\n cached = await store.get<T>(cacheKey);\n } catch {}\n\n if (cached) {\n const state = getCacheState(cached, options.strategy);\n\n if (state === \"fresh\") return { data: cached.value, cacheStatus: \"hit\" };\n\n if (state === \"stale\") {\n revalidateInBackground(cacheKey, options, run);\n return { data: cached.value, cacheStatus: \"hit\" };\n }\n\n if (state === \"stale-if-error\") {\n return refreshWithStaleFallback(cacheKey, cached.value, options, run);\n }\n }\n\n return runAndMaybeStore(cacheKey, options, run).result;\n };\n\n function revalidateInBackground<T extends SerializableCacheValue>(\n cacheKey: string,\n options: RunWithCacheOptions,\n run: RunCallback<T>,\n ) {\n const entry = runAndMaybeStore(cacheKey, options, run, {\n scheduleCacheWrite: false,\n background: true,\n });\n schedule(entry.complete);\n }\n\n async function refreshWithStaleFallback<T extends SerializableCacheValue>(\n cacheKey: string,\n staleValue: T,\n options: RunWithCacheOptions,\n run: RunCallback<T>,\n ): Promise<RunWithCacheResult<T>> {\n try {\n return await runAndMaybeStore(cacheKey, options, run).result;\n } catch (error) {\n if (!shouldFallbackToStale(error)) throw error;\n\n return { data: staleValue, cacheStatus: \"hit\" };\n }\n }\n\n function runAndMaybeStore<T extends SerializableCacheValue>(\n key: string,\n options: RunWithCacheOptions,\n run: RunCallback<T>,\n { scheduleCacheWrite = true, background = false }: RunAndMaybeStoreOptions = {},\n ): CacheOperation<T> {\n // Assigned inside `result` once the callback has produced cacheable data.\n // `complete` reads the same variable later, so it follows the actual write\n // without making the foreground result wait when waitUntil is available.\n let storePromise: Promise<void> = Promise.resolve();\n\n const result = Promise.resolve().then(async (): Promise<RunWithCacheResult<T>> => {\n const decision = await runAndValidate(run, { background });\n if (!shouldStore(options.strategy, decision)) {\n return { data: decision.data, cacheStatus: \"miss\" };\n }\n\n storePromise = storeDecision(key, options, decision).catch(() => {});\n\n if (waitUntil && scheduleCacheWrite) {\n schedule(storePromise);\n } else if (!waitUntil) {\n await storePromise;\n }\n\n return { data: decision.data, cacheStatus: \"miss\" };\n });\n\n return {\n result,\n complete: result\n .then(\n () => storePromise,\n () => undefined,\n )\n .then(() => undefined),\n };\n }\n\n async function storeDecision<T extends SerializableCacheValue>(\n key: string,\n options: RunWithCacheOptions,\n decision: CacheDecision<T>,\n ) {\n await store.set(\n key,\n {\n version: 1,\n value: decision.data,\n storedAt: Date.now(),\n strategy: options.strategy,\n },\n {\n strategy: options.strategy,\n },\n );\n }\n\n function schedule(promise: Promise<unknown>) {\n try {\n waitUntil?.(promise);\n } catch {}\n }\n}\n\nfunction shouldStore<T extends SerializableCacheValue>(\n strategy: CachingStrategy,\n decision: CacheDecision<T>,\n): boolean {\n return decision.shouldCache && getCacheRetentionTtl(strategy) > 0;\n}\n\nfunction shouldFallbackToStale(error: unknown): boolean {\n return !(error instanceof StaleFallbackDisabledError);\n}\n\nasync function runAndValidate<T extends SerializableCacheValue>(\n run: RunCallback<T>,\n context: RunWithCacheContext,\n): Promise<CacheDecision<T>> {\n const decision = await run(context);\n\n if (\n typeof decision !== \"object\" ||\n decision == null ||\n !(\"data\" in decision) ||\n !(\"shouldCache\" in decision) ||\n typeof decision.shouldCache !== \"boolean\"\n ) {\n throw new TypeError(\"runWithCache callback must return {data, shouldCache}.\");\n }\n\n return decision;\n}\n\nfunction getCacheState<T extends SerializableCacheValue>(\n envelope: CacheEnvelope<T>,\n strategy: CachingStrategy,\n): CacheState {\n const age = Math.max(0, (Date.now() - envelope.storedAt) / 1000);\n const maxAge = strategy.maxAge ?? 0;\n const staleWhileRevalidate = strategy.staleWhileRevalidate ?? 0;\n const staleIfError = strategy.staleIfError ?? 0;\n\n if (age <= maxAge) return \"fresh\";\n if (age <= maxAge + staleWhileRevalidate) return \"stale\";\n if (age <= maxAge + staleWhileRevalidate + staleIfError) {\n return \"stale-if-error\";\n }\n\n return \"expired\";\n}\n\n/**\n * Returns cached data for the key, or runs the callback and caches the result.\n *\n * The callback returns an object with `data` and a boolean `shouldCache`. The function throws a `TypeError` for any other shape. During the `staleWhileRevalidate` window, the function returns the stale data right away and runs the callback in the background. During the `staleIfError` window, the function returns the stale data when the callback throws. A `no-store` strategy runs the callback and skips the cache.\n *\n * @publicDocs\n */\nexport type RunWithCacheForDocs =\n /**\n * @param options - The cache key and caching strategy for this call.\n * @param run - Produces the data and decides whether to cache it. Runs whenever the cache has no fresh entry.\n * @returns The data, and whether the data came from the cache, the callback, or a bypass.\n */\n (\n options: RunWithCacheOptions,\n run: () => MaybePromise<CacheDecision<SerializableCacheValue>>,\n ) => Promise<RunWithCacheResult<SerializableCacheValue>>;\n"],"mappings":"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"}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
//#region src/core/cache/store.d.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* can safely round-trip through `JSON.stringify`.
|
|
3
|
+
* The plain object that you cache. Web Cache stores save the object as JSON. Return data that
|
|
4
|
+
* survives a round trip through `JSON.stringify`.
|
|
6
5
|
*/
|
|
7
6
|
type SerializableCacheValue = Record<string, unknown>;
|
|
7
|
+
/** A cache with the `match()`, `put()`, and `delete()` methods of the Web Cache API, such as a cache from `caches.open()`. */
|
|
8
8
|
type WebCacheLike = Pick<Cache, "match" | "put" | "delete">;
|
|
9
|
+
/** Options that Hydrogen passes when it writes an entry to a key-value store. */
|
|
9
10
|
type KeyValueCacheSetOptions = {
|
|
10
|
-
ttl?: number;
|
|
11
|
+
/** How many seconds to keep the entry. Hydrogen passes the sum of the strategy's `maxAge`, `staleWhileRevalidate`, and `staleIfError`, rounded up. */ttl?: number;
|
|
11
12
|
};
|
|
13
|
+
/** A key-value store with `get()`, `set()`, and an optional `delete()` method. Hydrogen passes each entry to `set()` as an object. Serialize the object in your store when the store saves only strings. */
|
|
12
14
|
type KeyValueCacheLike = {
|
|
13
|
-
get(key: string): unknown | Promise<unknown>;
|
|
14
|
-
set(key: string, value: unknown, options?: KeyValueCacheSetOptions): unknown | Promise<unknown>;
|
|
15
|
+
/** Returns the value under the key. Hydrogen treats a value that it didn't write as a cache miss. */get(key: string): unknown | Promise<unknown>; /** Saves the value under the key. Expire the entry after the `ttl` option, in seconds. */
|
|
16
|
+
set(key: string, value: unknown, options?: KeyValueCacheSetOptions): unknown | Promise<unknown>; /** Removes the value under the key. */
|
|
15
17
|
delete?(key: string): unknown | Promise<unknown>;
|
|
16
18
|
};
|
|
17
19
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.mjs","names":[],"sources":["../../../src/core/cache/store.ts"],"sourcesContent":["import {\n type CachingStrategy,\n getPlatformCacheControlHeader,\n getCacheRetentionTtl,\n} from \"./strategies\";\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"store.mjs","names":[],"sources":["../../../src/core/cache/store.ts"],"sourcesContent":["import {\n type CachingStrategy,\n getPlatformCacheControlHeader,\n getCacheRetentionTtl,\n} from \"./strategies\";\n\n/**\n * The plain object that you cache. Web Cache stores save the object as JSON. Return data that\n * survives a round trip through `JSON.stringify`.\n */\nexport type SerializableCacheValue = Record<string, unknown>;\n\nexport type CacheEnvelope<T extends SerializableCacheValue = SerializableCacheValue> = {\n version: 1;\n value: T;\n storedAt: number;\n strategy: CachingStrategy;\n};\n\n/** A cache with the `match()`, `put()`, and `delete()` methods of the Web Cache API, such as a cache from `caches.open()`. */\nexport type WebCacheLike = Pick<Cache, \"match\" | \"put\" | \"delete\">;\n\n/** Options that Hydrogen passes when it writes an entry to a key-value store. */\nexport type KeyValueCacheSetOptions = {\n /** How many seconds to keep the entry. Hydrogen passes the sum of the strategy's `maxAge`, `staleWhileRevalidate`, and `staleIfError`, rounded up. */\n ttl?: number;\n};\n\n/** A key-value store with `get()`, `set()`, and an optional `delete()` method. Hydrogen passes each entry to `set()` as an object. Serialize the object in your store when the store saves only strings. */\nexport type KeyValueCacheLike = {\n /** Returns the value under the key. Hydrogen treats a value that it didn't write as a cache miss. */\n get(key: string): unknown | Promise<unknown>;\n /** Saves the value under the key. Expire the entry after the `ttl` option, in seconds. */\n set(key: string, value: unknown, options?: KeyValueCacheSetOptions): unknown | Promise<unknown>;\n /** Removes the value under the key. */\n delete?(key: string): unknown | Promise<unknown>;\n};\n\ntype NormalizedCacheStore = {\n get<T extends SerializableCacheValue>(key: string): Promise<CacheEnvelope<T> | undefined>;\n set<T extends SerializableCacheValue>(\n key: string,\n envelope: CacheEnvelope<T>,\n options: NormalizedCacheSetOptions,\n ): Promise<void>;\n delete?(key: string): Promise<void>;\n};\n\ntype NormalizedCacheSetOptions = {\n strategy: CachingStrategy;\n};\n\nexport function createNormalizedCacheStore(\n cache: WebCacheLike | KeyValueCacheLike,\n): NormalizedCacheStore {\n if (isWebCacheLike(cache)) return createWebCacheStore(cache);\n if (isKeyValueCacheLike(cache)) return createKeyValueCacheStore(cache);\n\n throw new TypeError(\"Expected a Web Cache API compatible store or a key/value cache store.\");\n}\n\nfunction createWebCacheStore(cache: WebCacheLike): NormalizedCacheStore {\n return {\n async get<T extends SerializableCacheValue>(key: string) {\n const response = await cache.match(createCacheRequest(key));\n if (!response) return undefined;\n\n const envelope: unknown = JSON.parse(await response.text());\n return isCacheEnvelope<T>(envelope) ? envelope : undefined;\n },\n\n async set<T extends SerializableCacheValue>(\n key: string,\n envelope: CacheEnvelope<T>,\n { strategy }: NormalizedCacheSetOptions,\n ) {\n const response = new Response(JSON.stringify(envelope), {\n headers: {\n \"cache-control\": getPlatformCacheControlHeader(strategy),\n \"content-type\": \"application/json\",\n },\n });\n\n await cache.put(createCacheRequest(key), response);\n },\n\n async delete(key) {\n await cache.delete(createCacheRequest(key));\n },\n };\n}\n\nfunction createKeyValueCacheStore(cache: KeyValueCacheLike): NormalizedCacheStore {\n return {\n async get<T extends SerializableCacheValue>(key: string) {\n const envelope = await cache.get(key);\n return isCacheEnvelope<T>(envelope) ? envelope : undefined;\n },\n\n async set<T extends SerializableCacheValue>(\n key: string,\n envelope: CacheEnvelope<T>,\n { strategy }: NormalizedCacheSetOptions,\n ) {\n const ttl = Math.ceil(getCacheRetentionTtl(strategy));\n\n await cache.set(key, envelope, { ttl });\n },\n\n async delete(key) {\n await cache.delete?.(key);\n },\n };\n}\n\nfunction createCacheRequest(key: string) {\n return new Request(`https://hydrogen.cache/${key}`);\n}\n\nfunction isWebCacheLike(cache: unknown): cache is WebCacheLike {\n return (\n typeof cache === \"object\" &&\n cache != null &&\n \"match\" in cache &&\n \"put\" in cache &&\n typeof cache.match === \"function\" &&\n typeof cache.put === \"function\"\n );\n}\n\nfunction isKeyValueCacheLike(cache: unknown): cache is KeyValueCacheLike {\n return (\n typeof cache === \"object\" &&\n cache != null &&\n \"get\" in cache &&\n \"set\" in cache &&\n typeof cache.get === \"function\" &&\n typeof cache.set === \"function\"\n );\n}\n\nfunction isCacheEnvelope<T extends SerializableCacheValue>(\n value: unknown,\n): value is CacheEnvelope<T> {\n return (\n typeof value === \"object\" &&\n value != null &&\n \"version\" in value &&\n value.version === 1 &&\n \"storedAt\" in value &&\n typeof value.storedAt === \"number\" &&\n // Freshness uses the call-site strategy, but stored envelopes should still\n // look like the current shape so older or malformed entries do not become hits.\n \"strategy\" in value &&\n Boolean(value.strategy) &&\n \"value\" in value &&\n isSerializableCacheValue(value.value)\n );\n}\n\nfunction isSerializableCacheValue(value: unknown): value is SerializableCacheValue {\n return typeof value === \"object\" && value != null && !Array.isArray(value);\n}\n"],"mappings":";;;AAoDA,SAAgB,2BACd,OACsB;AACtB,KAAI,eAAe,MAAM,CAAE,QAAO,oBAAoB,MAAM;AAC5D,KAAI,oBAAoB,MAAM,CAAE,QAAO,yBAAyB,MAAM;AAEtE,OAAM,IAAI,UAAU,wEAAwE;;AAG9F,SAAS,oBAAoB,OAA2C;AACtE,QAAO;EACL,MAAM,IAAsC,KAAa;GACvD,MAAM,WAAW,MAAM,MAAM,MAAM,mBAAmB,IAAI,CAAC;AAC3D,OAAI,CAAC,SAAU,QAAO;GAEtB,MAAM,WAAoB,KAAK,MAAM,MAAM,SAAS,MAAM,CAAC;AAC3D,UAAO,gBAAmB,SAAS,GAAG,WAAW;;EAGnD,MAAM,IACJ,KACA,UACA,EAAE,YACF;GACA,MAAM,WAAW,IAAI,SAAS,KAAK,UAAU,SAAS,EAAE,EACtD,SAAS;IACP,iBAAiB,8BAA8B,SAAS;IACxD,gBAAgB;IACjB,EACF,CAAC;AAEF,SAAM,MAAM,IAAI,mBAAmB,IAAI,EAAE,SAAS;;EAGpD,MAAM,OAAO,KAAK;AAChB,SAAM,MAAM,OAAO,mBAAmB,IAAI,CAAC;;EAE9C;;AAGH,SAAS,yBAAyB,OAAgD;AAChF,QAAO;EACL,MAAM,IAAsC,KAAa;GACvD,MAAM,WAAW,MAAM,MAAM,IAAI,IAAI;AACrC,UAAO,gBAAmB,SAAS,GAAG,WAAW;;EAGnD,MAAM,IACJ,KACA,UACA,EAAE,YACF;GACA,MAAM,MAAM,KAAK,KAAK,qBAAqB,SAAS,CAAC;AAErD,SAAM,MAAM,IAAI,KAAK,UAAU,EAAE,KAAK,CAAC;;EAGzC,MAAM,OAAO,KAAK;AAChB,SAAM,MAAM,SAAS,IAAI;;EAE5B;;AAGH,SAAS,mBAAmB,KAAa;AACvC,QAAO,IAAI,QAAQ,0BAA0B,MAAM;;AAGrD,SAAS,eAAe,OAAuC;AAC7D,QACE,OAAO,UAAU,YACjB,SAAS,QACT,WAAW,SACX,SAAS,SACT,OAAO,MAAM,UAAU,cACvB,OAAO,MAAM,QAAQ;;AAIzB,SAAS,oBAAoB,OAA4C;AACvE,QACE,OAAO,UAAU,YACjB,SAAS,QACT,SAAS,SACT,SAAS,SACT,OAAO,MAAM,QAAQ,cACrB,OAAO,MAAM,QAAQ;;AAIzB,SAAS,gBACP,OAC2B;AAC3B,QACE,OAAO,UAAU,YACjB,SAAS,QACT,aAAa,SACb,MAAM,YAAY,KAClB,cAAc,SACd,OAAO,MAAM,aAAa,YAG1B,cAAc,SACd,QAAQ,MAAM,SAAS,IACvB,WAAW,SACX,yBAAyB,MAAM,MAAM;;AAIzC,SAAS,yBAAyB,OAAiD;AACjF,QAAO,OAAO,UAAU,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,MAAM"}
|
|
@@ -2,29 +2,54 @@
|
|
|
2
2
|
declare const NO_STORE = "no-store";
|
|
3
3
|
declare const PUBLIC = "public";
|
|
4
4
|
declare const PRIVATE = "private";
|
|
5
|
+
/** The cache mode of a strategy, `public`, `private`, or `no-store`. A `no-store` strategy skips the cache. */
|
|
5
6
|
type CacheMode = typeof PUBLIC | typeof PRIVATE | typeof NO_STORE;
|
|
7
|
+
/** The modes that a custom strategy accepts, `public` or `private`. `Cache` throws an error for any other mode. */
|
|
6
8
|
type ExpirableCacheMode = typeof PUBLIC | typeof PRIVATE;
|
|
9
|
+
/**
|
|
10
|
+
* A number of seconds, or an object such as `{ minutes: 10 }`. `Cache` throws an error for a duration that isn't a finite, non-negative number, and rounds fractional totals up to whole seconds.
|
|
11
|
+
*/
|
|
7
12
|
type CacheDuration = number | {
|
|
8
13
|
seconds?: number;
|
|
9
14
|
minutes?: number;
|
|
10
15
|
hours?: number;
|
|
11
16
|
days?: number;
|
|
12
17
|
};
|
|
18
|
+
/** A caching strategy, with every duration in whole seconds. Create a strategy with `Cache`. */
|
|
13
19
|
interface CachingStrategy {
|
|
20
|
+
/** The cache mode. A `no-store` strategy skips the cache. */
|
|
14
21
|
mode?: CacheMode;
|
|
22
|
+
/** How many seconds an entry stays fresh. */
|
|
15
23
|
maxAge?: number;
|
|
24
|
+
/** How many seconds after the fresh window the cache serves a stale entry while it refreshes the entry in the background. */
|
|
16
25
|
staleWhileRevalidate?: number;
|
|
26
|
+
/** How many seconds after the stale window the cache serves a stale entry when the refresh fails. */
|
|
17
27
|
staleIfError?: number;
|
|
18
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Options for a custom caching strategy. A duration that you leave out counts as zero seconds.
|
|
31
|
+
*
|
|
32
|
+
* @publicDocs
|
|
33
|
+
*/
|
|
19
34
|
type CacheOptions = {
|
|
20
|
-
mode?: ExpirableCacheMode;
|
|
21
|
-
maxAge?: CacheDuration;
|
|
35
|
+
/** The cache mode, `public` or `private`. Defaults to `public`. Web Cache stores receive the mode in the `cache-control` header. The Storefront API client throws an error for `private`. */mode?: ExpirableCacheMode; /** How long an entry stays fresh. */
|
|
36
|
+
maxAge?: CacheDuration; /** How long after the fresh window the cache serves a stale entry while it refreshes the entry in the background. */
|
|
22
37
|
staleWhileRevalidate?: CacheDuration;
|
|
38
|
+
/**
|
|
39
|
+
* How long after the stale-while-revalidate window the cache serves a stale entry when a refresh fails.
|
|
40
|
+
*/
|
|
23
41
|
staleIfError?: CacheDuration;
|
|
24
42
|
};
|
|
43
|
+
/** A strategy that skips the cache. */
|
|
25
44
|
type NoStoreStrategy = CachingStrategy & {
|
|
26
45
|
mode: typeof NO_STORE;
|
|
27
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Creates caching strategies for Storefront API queries, cached fetches, and cached runs. Call
|
|
49
|
+
* `Cache` with options for a custom strategy, or use the `none`, `short`, and `long` presets.
|
|
50
|
+
*
|
|
51
|
+
* @publicDocs
|
|
52
|
+
*/
|
|
28
53
|
declare const Cache: typeof createCache & {
|
|
29
54
|
none: typeof cacheNone;
|
|
30
55
|
short: typeof cacheShort;
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
const NO_STORE = "no-store";
|
|
3
3
|
const PUBLIC = "public";
|
|
4
4
|
const PRIVATE = "private";
|
|
5
|
+
/**
|
|
6
|
+
* Creates caching strategies for Storefront API queries, cached fetches, and cached runs. Call
|
|
7
|
+
* `Cache` with options for a custom strategy, or use the `none`, `short`, and `long` presets.
|
|
8
|
+
*
|
|
9
|
+
* @publicDocs
|
|
10
|
+
*/
|
|
5
11
|
const Cache = Object.assign(createCache, {
|
|
6
12
|
none: cacheNone,
|
|
7
13
|
short: cacheShort,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strategies.mjs","names":[],"sources":["../../../src/core/cache/strategies.ts"],"sourcesContent":["export const NO_STORE = \"no-store\";\n\nconst PUBLIC = \"public\";\nconst PRIVATE = \"private\";\n\nexport type CacheMode = typeof PUBLIC | typeof PRIVATE | typeof NO_STORE;\nexport type ExpirableCacheMode = typeof PUBLIC | typeof PRIVATE;\n\nexport type CacheDuration =\n | number\n | {\n seconds?: number;\n minutes?: number;\n hours?: number;\n days?: number;\n };\n\nexport interface CachingStrategy {\n mode?: CacheMode;\n maxAge?: number;\n staleWhileRevalidate?: number;\n staleIfError?: number;\n}\n\nexport type CacheOptions = {\n mode?: ExpirableCacheMode;\n maxAge?: CacheDuration;\n staleWhileRevalidate?: CacheDuration;\n staleIfError?: CacheDuration;\n};\n\ntype NoStoreStrategy = CachingStrategy & {\n mode: typeof NO_STORE;\n};\n\nexport const Cache = Object.assign(createCache, {\n none: cacheNone,\n short: cacheShort,\n long: cacheLong,\n});\n\nexport function getCacheRetentionTtl(strategy: CachingStrategy): number {\n if (strategy.mode === NO_STORE) return 0;\n\n return Math.max(\n 0,\n (strategy.maxAge ?? 0) + (strategy.staleWhileRevalidate ?? 0) + (strategy.staleIfError ?? 0),\n );\n}\n\nexport function getPlatformCacheControlHeader(strategy: CachingStrategy): string {\n const ttl = getCacheRetentionTtl(strategy);\n if (ttl <= 0) return NO_STORE;\n\n return `${strategy.mode ?? PUBLIC}, max-age=${Math.ceil(ttl)}`;\n}\n\nfunction createCache(options: CacheOptions): CachingStrategy {\n return normalizeCacheOptions(options);\n}\n\nfunction cacheNone(): NoStoreStrategy {\n return { mode: NO_STORE };\n}\n\nfunction cacheShort(overrideOptions?: CacheOptions): CachingStrategy {\n return normalizeCacheOptions(\n {\n mode: PUBLIC,\n maxAge: 1,\n staleWhileRevalidate: 9,\n },\n overrideOptions,\n );\n}\n\nfunction cacheLong(overrideOptions?: CacheOptions): CachingStrategy {\n return normalizeCacheOptions(\n {\n mode: PUBLIC,\n maxAge: 3600,\n staleWhileRevalidate: 82800,\n },\n overrideOptions,\n );\n}\n\nfunction normalizeCacheOptions(\n defaults: CacheOptions,\n overrideOptions?: CacheOptions,\n): CachingStrategy;\nfunction normalizeCacheOptions(options: CacheOptions): CachingStrategy;\nfunction normalizeCacheOptions(\n defaultsOrOptions: CacheOptions,\n overrideOptions?: CacheOptions,\n): CachingStrategy {\n const options = {\n ...defaultsOrOptions,\n ...overrideOptions,\n };\n\n return {\n ...normalizeMode(options),\n ...normalizeDurationOption(\"maxAge\", options.maxAge),\n ...normalizeDurationOption(\"staleWhileRevalidate\", options.staleWhileRevalidate),\n ...normalizeDurationOption(\"staleIfError\", options.staleIfError),\n };\n}\n\nfunction normalizeMode(options: CacheOptions): Pick<CachingStrategy, \"mode\"> {\n if (options.mode && options.mode !== PUBLIC && options.mode !== PRIVATE) {\n throw new Error(\"'mode' must be either 'public' or 'private'\");\n }\n\n return { mode: options.mode ?? PUBLIC };\n}\n\nfunction normalizeDurationOption(\n name: \"maxAge\" | \"staleWhileRevalidate\" | \"staleIfError\",\n duration: CacheDuration | undefined,\n): Pick<CachingStrategy, typeof name> {\n if (duration == null) return {};\n\n return { [name]: normalizeDuration(duration) };\n}\n\nfunction normalizeDuration(duration: CacheDuration): number {\n if (typeof duration === \"number\") return validateDuration(duration);\n\n return validateDuration(\n (duration.seconds ?? 0) +\n (duration.minutes ?? 0) * 60 +\n (duration.hours ?? 0) * 3600 +\n (duration.days ?? 0) * 86400,\n );\n}\n\nfunction validateDuration(duration: number): number {\n if (!Number.isFinite(duration) || duration < 0) {\n throw new Error(\"Cache durations must be finite, non-negative numbers.\");\n }\n\n return Math.ceil(duration);\n}\n"],"mappings":";AAAA,MAAa,WAAW;AAExB,MAAM,SAAS;AACf,MAAM,UAAU
|
|
1
|
+
{"version":3,"file":"strategies.mjs","names":[],"sources":["../../../src/core/cache/strategies.ts"],"sourcesContent":["export const NO_STORE = \"no-store\";\n\nconst PUBLIC = \"public\";\nconst PRIVATE = \"private\";\n\n/** The cache mode of a strategy, `public`, `private`, or `no-store`. A `no-store` strategy skips the cache. */\nexport type CacheMode = typeof PUBLIC | typeof PRIVATE | typeof NO_STORE;\n/** The modes that a custom strategy accepts, `public` or `private`. `Cache` throws an error for any other mode. */\nexport type ExpirableCacheMode = typeof PUBLIC | typeof PRIVATE;\n\n/**\n * A number of seconds, or an object such as `{ minutes: 10 }`. `Cache` throws an error for a duration that isn't a finite, non-negative number, and rounds fractional totals up to whole seconds.\n */\nexport type CacheDuration =\n | number\n | {\n seconds?: number;\n minutes?: number;\n hours?: number;\n days?: number;\n };\n\n/** A caching strategy, with every duration in whole seconds. Create a strategy with `Cache`. */\nexport interface CachingStrategy {\n /** The cache mode. A `no-store` strategy skips the cache. */\n mode?: CacheMode;\n /** How many seconds an entry stays fresh. */\n maxAge?: number;\n /** How many seconds after the fresh window the cache serves a stale entry while it refreshes the entry in the background. */\n staleWhileRevalidate?: number;\n /** How many seconds after the stale window the cache serves a stale entry when the refresh fails. */\n staleIfError?: number;\n}\n\n/**\n * Options for a custom caching strategy. A duration that you leave out counts as zero seconds.\n *\n * @publicDocs\n */\nexport type CacheOptions = {\n /** The cache mode, `public` or `private`. Defaults to `public`. Web Cache stores receive the mode in the `cache-control` header. The Storefront API client throws an error for `private`. */\n mode?: ExpirableCacheMode;\n /** How long an entry stays fresh. */\n maxAge?: CacheDuration;\n /** How long after the fresh window the cache serves a stale entry while it refreshes the entry in the background. */\n staleWhileRevalidate?: CacheDuration;\n /**\n * How long after the stale-while-revalidate window the cache serves a stale entry when a refresh fails.\n */\n staleIfError?: CacheDuration;\n};\n\n/** A strategy that skips the cache. */\ntype NoStoreStrategy = CachingStrategy & {\n mode: typeof NO_STORE;\n};\n\n/**\n * Creates caching strategies for Storefront API queries, cached fetches, and cached runs. Call\n * `Cache` with options for a custom strategy, or use the `none`, `short`, and `long` presets.\n *\n * @publicDocs\n */\nexport const Cache = Object.assign(createCache, {\n none: cacheNone,\n short: cacheShort,\n long: cacheLong,\n});\n\n/**\n * Creates caching strategies for Storefront API queries, cached fetches, and cached runs. Call `Cache` with options for a custom strategy, or use the `short`, `long`, and `none` presets.\n *\n * @publicDocs\n */\nexport type CacheForDocs = {\n (options: CacheOptions): CachingStrategy;\n /**\n * Returns a strategy that skips the cache. The preset takes no options.\n */\n none: typeof cacheNone;\n /**\n * Keeps public entries fresh for 1 second, then serves stale entries for 9 more seconds while the cache refreshes them. Pass options to override the mode or either duration, or to add `staleIfError`.\n */\n short: typeof cacheShort;\n /**\n * Keeps public entries fresh for 1 hour, then serves stale entries for 23 more hours while the cache refreshes them. Pass options to override the mode or either duration, or to add `staleIfError`.\n */\n long: typeof cacheLong;\n};\n\nexport function getCacheRetentionTtl(strategy: CachingStrategy): number {\n if (strategy.mode === NO_STORE) return 0;\n\n return Math.max(\n 0,\n (strategy.maxAge ?? 0) + (strategy.staleWhileRevalidate ?? 0) + (strategy.staleIfError ?? 0),\n );\n}\n\nexport function getPlatformCacheControlHeader(strategy: CachingStrategy): string {\n const ttl = getCacheRetentionTtl(strategy);\n if (ttl <= 0) return NO_STORE;\n\n return `${strategy.mode ?? PUBLIC}, max-age=${Math.ceil(ttl)}`;\n}\n\nfunction createCache(options: CacheOptions): CachingStrategy {\n return normalizeCacheOptions(options);\n}\n\nfunction cacheNone(): NoStoreStrategy {\n return { mode: NO_STORE };\n}\n\nfunction cacheShort(overrideOptions?: CacheOptions): CachingStrategy {\n return normalizeCacheOptions(\n {\n mode: PUBLIC,\n maxAge: 1,\n staleWhileRevalidate: 9,\n },\n overrideOptions,\n );\n}\n\nfunction cacheLong(overrideOptions?: CacheOptions): CachingStrategy {\n return normalizeCacheOptions(\n {\n mode: PUBLIC,\n maxAge: 3600,\n staleWhileRevalidate: 82800,\n },\n overrideOptions,\n );\n}\n\nfunction normalizeCacheOptions(\n defaults: CacheOptions,\n overrideOptions?: CacheOptions,\n): CachingStrategy;\nfunction normalizeCacheOptions(options: CacheOptions): CachingStrategy;\nfunction normalizeCacheOptions(\n defaultsOrOptions: CacheOptions,\n overrideOptions?: CacheOptions,\n): CachingStrategy {\n const options = {\n ...defaultsOrOptions,\n ...overrideOptions,\n };\n\n return {\n ...normalizeMode(options),\n ...normalizeDurationOption(\"maxAge\", options.maxAge),\n ...normalizeDurationOption(\"staleWhileRevalidate\", options.staleWhileRevalidate),\n ...normalizeDurationOption(\"staleIfError\", options.staleIfError),\n };\n}\n\nfunction normalizeMode(options: CacheOptions): Pick<CachingStrategy, \"mode\"> {\n if (options.mode && options.mode !== PUBLIC && options.mode !== PRIVATE) {\n throw new Error(\"'mode' must be either 'public' or 'private'\");\n }\n\n return { mode: options.mode ?? PUBLIC };\n}\n\nfunction normalizeDurationOption(\n name: \"maxAge\" | \"staleWhileRevalidate\" | \"staleIfError\",\n duration: CacheDuration | undefined,\n): Pick<CachingStrategy, typeof name> {\n if (duration == null) return {};\n\n return { [name]: normalizeDuration(duration) };\n}\n\nfunction normalizeDuration(duration: CacheDuration): number {\n if (typeof duration === \"number\") return validateDuration(duration);\n\n return validateDuration(\n (duration.seconds ?? 0) +\n (duration.minutes ?? 0) * 60 +\n (duration.hours ?? 0) * 3600 +\n (duration.days ?? 0) * 86400,\n );\n}\n\nfunction validateDuration(duration: number): number {\n if (!Number.isFinite(duration) || duration < 0) {\n throw new Error(\"Cache durations must be finite, non-negative numbers.\");\n }\n\n return Math.ceil(duration);\n}\n"],"mappings":";AAAA,MAAa,WAAW;AAExB,MAAM,SAAS;AACf,MAAM,UAAU;;;;;;;AA4DhB,MAAa,QAAQ,OAAO,OAAO,aAAa;CAC9C,MAAM;CACN,OAAO;CACP,MAAM;CACP,CAAC;AAuBF,SAAgB,qBAAqB,UAAmC;AACtE,KAAI,SAAS,oBAAmB,QAAO;AAEvC,QAAO,KAAK,IACV,IACC,SAAS,UAAU,MAAM,SAAS,wBAAwB,MAAM,SAAS,gBAAgB,GAC3F;;AAGH,SAAgB,8BAA8B,UAAmC;CAC/E,MAAM,MAAM,qBAAqB,SAAS;AAC1C,KAAI,OAAO,EAAG,QAAO;AAErB,QAAO,GAAG,SAAS,QAAQ,OAAO,YAAY,KAAK,KAAK,IAAI;;AAG9D,SAAS,YAAY,SAAwC;AAC3D,QAAO,sBAAsB,QAAQ;;AAGvC,SAAS,YAA6B;AACpC,QAAO,EAAE,MAAM,UAAU;;AAG3B,SAAS,WAAW,iBAAiD;AACnE,QAAO,sBACL;EACE,MAAM;EACN,QAAQ;EACR,sBAAsB;EACvB,EACD,gBACD;;AAGH,SAAS,UAAU,iBAAiD;AAClE,QAAO,sBACL;EACE,MAAM;EACN,QAAQ;EACR,sBAAsB;EACvB,EACD,gBACD;;AAQH,SAAS,sBACP,mBACA,iBACiB;CACjB,MAAM,UAAU;EACd,GAAG;EACH,GAAG;EACJ;AAED,QAAO;EACL,GAAG,cAAc,QAAQ;EACzB,GAAG,wBAAwB,UAAU,QAAQ,OAAO;EACpD,GAAG,wBAAwB,wBAAwB,QAAQ,qBAAqB;EAChF,GAAG,wBAAwB,gBAAgB,QAAQ,aAAa;EACjE;;AAGH,SAAS,cAAc,SAAsD;AAC3E,KAAI,QAAQ,QAAQ,QAAQ,SAAS,UAAU,QAAQ,SAAS,QAC9D,OAAM,IAAI,MAAM,8CAA8C;AAGhE,QAAO,EAAE,MAAM,QAAQ,QAAQ,QAAQ;;AAGzC,SAAS,wBACP,MACA,UACoC;AACpC,KAAI,YAAY,KAAM,QAAO,EAAE;AAE/B,QAAO,GAAG,OAAO,kBAAkB,SAAS,EAAE;;AAGhD,SAAS,kBAAkB,UAAiC;AAC1D,KAAI,OAAO,aAAa,SAAU,QAAO,iBAAiB,SAAS;AAEnE,QAAO,kBACJ,SAAS,WAAW,MAClB,SAAS,WAAW,KAAK,MACzB,SAAS,SAAS,KAAK,QACvB,SAAS,QAAQ,KAAK,MAC1B;;AAGH,SAAS,iBAAiB,UAA0B;AAClD,KAAI,CAAC,OAAO,SAAS,SAAS,IAAI,WAAW,EAC3C,OAAM,IAAI,MAAM,wDAAwD;AAG1E,QAAO,KAAK,KAAK,SAAS"}
|
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
//#region src/core/cart/actions.d.ts
|
|
2
|
-
/** A key-value pair
|
|
2
|
+
/** A custom key-value pair for the cart or a cart line. */
|
|
3
3
|
type CartAttributeInput = {
|
|
4
|
-
key: string;
|
|
4
|
+
/** The attribute name. parseCartRequest throws for an empty name in cart attributes and in form submissions. */key: string; /** The attribute value. */
|
|
5
5
|
value: string;
|
|
6
6
|
};
|
|
7
|
+
/** Input for adding a line to the cart. */
|
|
7
8
|
type CartLineAddInput = {
|
|
8
|
-
/** Storefront API GID of the product variant to add. */merchandiseId: string;
|
|
9
|
-
quantity: number;
|
|
10
|
-
attributes?: CartAttributeInput[]; /**
|
|
9
|
+
/** The Storefront API GID of the product variant to add. */merchandiseId: string; /** Number of units to add. A form submission without a quantity adds `1`. */
|
|
10
|
+
quantity: number; /** Custom key-value attributes to set on the new line. */
|
|
11
|
+
attributes?: CartAttributeInput[]; /** The selling plan GID for a subscription line. */
|
|
11
12
|
sellingPlanId?: string;
|
|
12
13
|
};
|
|
13
|
-
/** Input for updating an existing cart line.
|
|
14
|
+
/** Input for updating an existing cart line. A quantity of `0` removes the line. */
|
|
14
15
|
type CartLineUpdateInput = {
|
|
15
|
-
/** The
|
|
16
|
-
quantity
|
|
17
|
-
attributes
|
|
16
|
+
/** The ID of the cart line to update. */id: string; /** The GID of a product variant to swap the line to. */
|
|
17
|
+
merchandiseId?: string; /** The line's new quantity. */
|
|
18
|
+
quantity: number; /** Custom key-value attributes to set on the line. */
|
|
19
|
+
attributes?: CartAttributeInput[]; /** The selling plan GID to assign to the line for a subscription. */
|
|
18
20
|
sellingPlanId?: string;
|
|
19
21
|
};
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
23
|
+
* A cart change that `parseCartRequest` reads from a JSON or form request.
|
|
22
24
|
*
|
|
23
|
-
*
|
|
24
|
-
* of these variants. The `intent` field determines the Storefront API mutation:
|
|
25
|
+
* The `intent` field names the change and the Storefront API mutation that applies the change:
|
|
25
26
|
*
|
|
26
|
-
* - `
|
|
27
|
-
* - `
|
|
28
|
-
* - `
|
|
29
|
-
* - `
|
|
30
|
-
* - `
|
|
31
|
-
* - `
|
|
32
|
-
* - `
|
|
33
|
-
* - `
|
|
27
|
+
* - `add` adds new lines with `cartLinesAdd`, or creates a cart with `cartCreate` when none exists.
|
|
28
|
+
* - `update` changes quantity, merchandise, or attributes on existing lines with `cartLinesUpdate`.
|
|
29
|
+
* - `remove` removes lines by ID with `cartLinesRemove`.
|
|
30
|
+
* - `discount-update` replaces all discount codes with `cartDiscountCodesUpdate`.
|
|
31
|
+
* - `discount-apply` adds one discount code. The handler reads the current codes, then writes the new list with `cartDiscountCodesUpdate`.
|
|
32
|
+
* - `discount-remove` removes one discount code with the same read-then-write approach.
|
|
33
|
+
* - `attributes-update` sets cart-level attributes with `cartAttributesUpdate`.
|
|
34
|
+
* - `note-update` sets the cart note with `cartNoteUpdate`.
|
|
35
|
+
*
|
|
36
|
+
* @publicDocs
|
|
34
37
|
*/
|
|
35
38
|
type CartAction = {
|
|
36
39
|
intent: "add";
|
|
@@ -57,23 +60,26 @@ type CartAction = {
|
|
|
57
60
|
intent: "note-update";
|
|
58
61
|
note: string;
|
|
59
62
|
};
|
|
63
|
+
/** The cart change and the cart ID that `parseCartRequest` reads from the request body. */
|
|
60
64
|
type ParsedCartRequest = {
|
|
61
|
-
action: CartAction;
|
|
65
|
+
action: CartAction; /** The cart GID from a JSON body, or `null` for a form submission or a JSON body without a cart ID. The function adds the `gid://shopify/Cart/` prefix to a bare cart token. */
|
|
62
66
|
cartId: string | null;
|
|
63
67
|
};
|
|
64
68
|
/**
|
|
65
|
-
*
|
|
69
|
+
* Reads the cart change from a cart request body. Use the function in a custom cart route.
|
|
70
|
+
*
|
|
71
|
+
* The function accepts `application/json`, `application/x-www-form-urlencoded`, and
|
|
72
|
+
* `multipart/form-data` requests. A JSON request can replace every discount code, and a form submission applies or removes one code.
|
|
66
73
|
*
|
|
67
|
-
*
|
|
68
|
-
* `multipart/form-data` content types. JSON and FormData each support a
|
|
69
|
-
* different subset of intents — JSON produces `discount-update` while
|
|
70
|
-
* FormData produces `discount-apply` / `discount-remove`.
|
|
74
|
+
* A form submission always returns a `null` cart ID. Read the cart ID for a form submission with `getCartId`.
|
|
71
75
|
*
|
|
72
|
-
*
|
|
73
|
-
* should fall back to the cart cookie.
|
|
76
|
+
* The function reads the request body, and you can't read the body again afterward. The function throws when the request has an unsupported content type, an unknown intent, or a missing required field, and when a JSON body combines more than one of `lines`, `discountCodes`, `attributes`, and `note`.
|
|
74
77
|
*
|
|
75
|
-
* @
|
|
76
|
-
*
|
|
78
|
+
* @param request The incoming cart request.
|
|
79
|
+
* @returns The cart change and, for a JSON body, the cart ID.
|
|
80
|
+
* @throws If the request has an unsupported content type, an unrecognized intent,
|
|
81
|
+
* or missing required fields, or if a JSON body combines more than one of
|
|
82
|
+
* `lines`, `discountCodes`, `attributes`, and `note`.
|
|
77
83
|
*
|
|
78
84
|
* @example
|
|
79
85
|
* ```ts
|
|
@@ -87,6 +93,7 @@ type ParsedCartRequest = {
|
|
|
87
93
|
* // ...
|
|
88
94
|
* }
|
|
89
95
|
* ```
|
|
96
|
+
* @publicDocs
|
|
90
97
|
*/
|
|
91
98
|
declare function parseCartRequest(request: Request): Promise<ParsedCartRequest>;
|
|
92
99
|
//#endregion
|