@vtex/faststore 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/dist/actions.json +181 -0
- package/dist/catalog.json +1673 -484
- package/dist/cli/UnreachableCaseError-Crd168Vu.js +10 -0
- package/dist/cli/UnreachableCaseError-Crd168Vu.js.map +1 -0
- package/dist/cli/cacheControl-CEojH_TM.js +158 -0
- package/dist/cli/cacheControl-CEojH_TM.js.map +1 -0
- package/dist/cli/cachePolicy-m2vASMRW.js +26 -0
- package/dist/cli/cachePolicy-m2vASMRW.js.map +1 -0
- package/dist/cli/computeCoreSchemaHash-DPJZj-qJ.js +10 -0
- package/dist/cli/computeCoreSchemaHash-DPJZj-qJ.js.map +1 -0
- package/dist/cli/default-BCQHcwbL.js +253 -0
- package/dist/cli/default-BCQHcwbL.js.map +1 -0
- package/dist/cli/disableSuggestions-yO9UqiWn.js +21 -0
- package/dist/cli/disableSuggestions-yO9UqiWn.js.map +1 -0
- package/dist/cli/dist-C1JhdHfo.js +2001 -0
- package/dist/cli/dist-C1JhdHfo.js.map +1 -0
- package/dist/cli/graphql-DG9thCDG.js +14589 -0
- package/dist/cli/graphql-DG9thCDG.js.map +1 -0
- package/dist/cli/index.d.ts +350 -0
- package/dist/cli/index.js +35047 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.min-BsIHDfFK.js +613 -0
- package/dist/cli/index.min-BsIHDfFK.js.map +1 -0
- package/dist/cli/inlineTrace-uLgOJ3TH.js +52 -0
- package/dist/cli/inlineTrace-uLgOJ3TH.js.map +1 -0
- package/dist/cli/internalPlugin-DnuwN4_4.js +11 -0
- package/dist/cli/internalPlugin-DnuwN4_4.js.map +1 -0
- package/dist/cli/packageVersion-DQAnIEil.js +6 -0
- package/dist/cli/packageVersion-DQAnIEil.js.map +1 -0
- package/dist/cli/rolldown-runtime-BMI-E3GI.js +44 -0
- package/dist/cli/schemaIsSubgraph-DoGh3OYj.js +17 -0
- package/dist/cli/schemaIsSubgraph-DoGh3OYj.js.map +1 -0
- package/dist/cli/schemaReporting-K-me-3-v.js +199 -0
- package/dist/cli/schemaReporting-K-me-3-v.js.map +1 -0
- package/dist/cli/server-runtime.js +49859 -0
- package/dist/cli/server-runtime.js.map +1 -0
- package/dist/cli/traceTreeBuilder-BIcV2U7b.js +9118 -0
- package/dist/cli/traceTreeBuilder-BIcV2U7b.js.map +1 -0
- package/dist/cli/usageReporting-Bpl8vSZX.js +10201 -0
- package/dist/cli/usageReporting-Bpl8vSZX.js.map +1 -0
- package/dist/cli/v4-CBhFvC49.js +42 -0
- package/dist/cli/v4-CBhFvC49.js.map +1 -0
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -1
- package/dist/components.d.ts +32 -9
- package/dist/components.js +35 -14
- package/dist/content-types.json +93 -0
- package/dist/core/dist/define-component.d.ts +37 -0
- package/dist/core/dist/index.d.ts +2 -2
- package/dist/docs/access-code-sign-in.md +1 -1
- package/dist/docs/account-entry-point.md +1 -1
- package/dist/docs/account-menu-identity.md +160 -0
- package/dist/docs/account-menu.md +106 -23
- package/dist/docs/address-summary.md +24 -3
- package/dist/docs/alert-dialog.md +204 -0
- package/dist/docs/avatar.md +103 -0
- package/dist/docs/billing-address.md +284 -0
- package/dist/docs/card-entry.md +250 -0
- package/dist/docs/cardholder-document.md +142 -0
- package/dist/docs/client-preferences-fieldset.md +148 -0
- package/dist/docs/client-preferences.md +2 -1
- package/dist/docs/client-profile-fieldset.md +184 -0
- package/dist/docs/client-profile.md +1 -1
- package/dist/docs/document-field.md +58 -4
- package/dist/docs/field.md +317 -0
- package/dist/docs/form.md +307 -0
- package/dist/docs/identity-edit.md +6 -6
- package/dist/docs/identity-summary.md +5 -5
- package/dist/docs/installments.md +248 -0
- package/dist/docs/my-account-profile.md +2 -1
- package/dist/docs/order-placed-payment-summary.md +138 -0
- package/dist/docs/payment-method-badges.md +232 -0
- package/dist/docs/payment-methods.md +1 -0
- package/dist/docs/payment.md +218 -0
- package/dist/docs/phone-field.md +58 -4
- package/dist/docs/place-order.md +157 -0
- package/dist/docs/product-shelf.md +20 -13
- package/dist/docs/save-card.md +186 -0
- package/dist/docs/saved-cards.md +218 -0
- package/dist/docs/select.md +465 -0
- package/dist/docs/shipping-address.md +110 -258
- package/dist/index.d.ts +5 -2
- package/dist/internal/client.d.ts +3 -1
- package/dist/internal/client.js +4 -2
- package/dist/internal/content.d.ts +2 -2
- package/dist/internal/server.d.ts +4 -1
- package/dist/internal/server.js +4 -1
- package/dist/matter/access-code-sign-in/access-code-sign-in.js +9 -8
- package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
- package/dist/matter/account-access-gate/account-access-gate.js.map +1 -1
- package/dist/matter/account-entry-point/account-entry-point.js +1 -1
- package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
- package/dist/matter/account-menu/account-menu.d.ts +70 -0
- package/dist/matter/account-menu/account-menu.js +201 -9
- package/dist/matter/account-menu/account-menu.js.map +1 -1
- package/dist/matter/account-menu/sign-out-failure.js +50 -0
- package/dist/matter/account-menu/sign-out-failure.js.map +1 -0
- package/dist/matter/account-menu-identity/account-menu-identity.d.ts +21 -0
- package/dist/matter/account-menu-identity/account-menu-identity.js +88 -0
- package/dist/matter/account-menu-identity/account-menu-identity.js.map +1 -0
- package/dist/matter/billing-address/billing-address.d.ts +165 -0
- package/dist/matter/billing-address/billing-address.js +408 -0
- package/dist/matter/billing-address/billing-address.js.map +1 -0
- package/dist/matter/card-entry/card-capture.d.ts +25 -0
- package/dist/matter/card-entry/card-capture.js +25 -0
- package/dist/matter/card-entry/card-capture.js.map +1 -0
- package/dist/matter/card-entry/card-entry.d.ts +120 -0
- package/dist/matter/card-entry/card-entry.js +833 -0
- package/dist/matter/card-entry/card-entry.js.map +1 -0
- package/dist/matter/cardholder-document/cardholder-document.d.ts +45 -0
- package/dist/matter/cardholder-document/cardholder-document.js +70 -0
- package/dist/matter/cardholder-document/cardholder-document.js.map +1 -0
- package/dist/matter/cart/cart.js +1 -1
- package/dist/matter/client-preferences/client-preferences-fieldset.d.ts +77 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js +299 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences.d.ts +3 -5
- package/dist/matter/client-preferences/client-preferences.js +6 -6
- package/dist/matter/client-preferences/client-preferences.js.map +1 -1
- package/dist/matter/client-profile/client-profile.d.ts +4 -6
- package/dist/matter/client-profile/client-profile.js +9 -6
- package/dist/matter/client-profile/client-profile.js.map +1 -1
- package/dist/matter/client-profile-form/client-profile-fieldset.d.ts +119 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js +488 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -0
- package/dist/matter/identity-edit/identity-edit.js +2 -2
- package/dist/matter/identity-edit/identity-edit.js.map +1 -1
- package/dist/matter/identity-summary/identity-summary.js +3 -2
- package/dist/matter/identity-summary/identity-summary.js.map +1 -1
- package/dist/matter/installments/installments-select.d.ts +42 -0
- package/dist/matter/installments/installments-select.js +167 -0
- package/dist/matter/installments/installments-select.js.map +1 -0
- package/dist/matter/installments/installments.d.ts +58 -0
- package/dist/matter/installments/installments.js +176 -0
- package/dist/matter/installments/installments.js.map +1 -0
- package/dist/matter/installments/use-installments-selection.d.ts +29 -0
- package/dist/matter/installments/use-installments-selection.js +95 -0
- package/dist/matter/installments/use-installments-selection.js.map +1 -0
- package/dist/matter/my-account-profile/my-account-profile.d.ts +3 -0
- package/dist/matter/my-account-profile/my-account-profile.js +6 -5
- package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +63 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +305 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -0
- package/dist/matter/payment/payment-card-capture.d.ts +19 -0
- package/dist/matter/payment/payment-card-capture.js +129 -0
- package/dist/matter/payment/payment-card-capture.js.map +1 -0
- package/dist/matter/payment/payment-controller.d.ts +24 -0
- package/dist/matter/payment/payment-controller.js +108 -0
- package/dist/matter/payment/payment-controller.js.map +1 -0
- package/dist/matter/payment/payment-method-badges.d.ts +57 -0
- package/dist/matter/payment/payment-method-badges.js +192 -0
- package/dist/matter/payment/payment-method-badges.js.map +1 -0
- package/dist/matter/payment/payment.d.ts +89 -0
- package/dist/matter/payment/payment.js +264 -0
- package/dist/matter/payment/payment.js.map +1 -0
- package/dist/matter/payment/use-payment-method-selection.d.ts +302 -0
- package/dist/matter/payment/use-payment-method-selection.js +458 -0
- package/dist/matter/payment/use-payment-method-selection.js.map +1 -0
- package/dist/matter/payment/use-payment-submit-gate.d.ts +111 -0
- package/dist/matter/payment/use-payment-submit-gate.js +144 -0
- package/dist/matter/payment/use-payment-submit-gate.js.map +1 -0
- package/dist/matter/place-order/place-order.d.ts +74 -0
- package/dist/matter/place-order/place-order.js +379 -0
- package/dist/matter/place-order/place-order.js.map +1 -0
- package/dist/matter/product-list/product-list.d.ts +1 -0
- package/dist/matter/product-list/product-list.js.map +1 -1
- package/dist/matter/product-shelf/product-shelf.d.ts +62 -31
- package/dist/matter/product-shelf/product-shelf.js +159 -57
- package/dist/matter/product-shelf/product-shelf.js.map +1 -1
- package/dist/matter/region/region.js +1 -1
- package/dist/matter/region/region.js.map +1 -1
- package/dist/matter/save-card/save-card.d.ts +49 -0
- package/dist/matter/save-card/save-card.js +188 -0
- package/dist/matter/save-card/save-card.js.map +1 -0
- package/dist/matter/saved-cards/saved-cards.d.ts +79 -0
- package/dist/matter/saved-cards/saved-cards.js +388 -0
- package/dist/matter/saved-cards/saved-cards.js.map +1 -0
- package/dist/matter/shipping-address/country-descriptors.js +20 -10
- package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.d.ts +113 -174
- package/dist/matter/shipping-address/shipping-address.js +480 -791
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/variant-selector/variant-selector.js +1 -1
- package/dist/particles/address-summary/address-summary.d.ts +24 -0
- package/dist/particles/address-summary/address-summary.js +83 -2
- package/dist/particles/address-summary/address-summary.js.map +1 -1
- package/dist/particles/alert-dialog/alert-dialog.d.ts +68 -0
- package/dist/particles/alert-dialog/alert-dialog.js +187 -0
- package/dist/particles/alert-dialog/alert-dialog.js.map +1 -0
- package/dist/particles/avatar/avatar.d.ts +17 -0
- package/dist/particles/avatar/avatar.js +65 -0
- package/dist/particles/avatar/avatar.js.map +1 -0
- package/dist/particles/checkbox/checkbox.d.ts +7 -3
- package/dist/particles/checkbox/checkbox.js +4 -1
- package/dist/particles/checkbox/checkbox.js.map +1 -1
- package/dist/particles/document-field/document-field.d.ts +48 -5
- package/dist/particles/document-field/document-field.js +189 -32
- package/dist/particles/document-field/document-field.js.map +1 -1
- package/dist/particles/field/bound-field-registry.js +52 -0
- package/dist/particles/field/bound-field-registry.js.map +1 -0
- package/dist/particles/field/field.d.ts +116 -0
- package/dist/particles/field/field.js +497 -6
- package/dist/particles/field/field.js.map +1 -1
- package/dist/particles/fieldset/fieldset.js +2 -2
- package/dist/particles/fieldset/fieldset.js.map +1 -1
- package/dist/particles/form/form.d.ts +104 -0
- package/dist/particles/form/form.js +485 -0
- package/dist/particles/form/form.js.map +1 -0
- package/dist/particles/phone-field/phone-field.d.ts +38 -10
- package/dist/particles/phone-field/phone-field.js +183 -39
- package/dist/particles/phone-field/phone-field.js.map +1 -1
- package/dist/particles/postal-code-field/postal-code-field.js +2 -2
- package/dist/particles/postal-code-field/postal-code-field.js.map +1 -1
- package/dist/particles/select/field-select.d.ts +43 -0
- package/dist/particles/select/field-select.js +150 -0
- package/dist/particles/select/field-select.js.map +1 -0
- package/dist/particles/select/select.d.ts +2 -0
- package/dist/particles/select/select.js.map +1 -1
- package/dist/particles/shared/compose-refs.js +21 -0
- package/dist/particles/shared/compose-refs.js.map +1 -0
- package/dist/schema.graphql +766 -28
- package/dist/sdk/actions/analytics-action-provider.d.ts +15 -0
- package/dist/sdk/actions/analytics-action-provider.js +19 -0
- package/dist/sdk/actions/analytics-action-provider.js.map +1 -0
- package/dist/sdk/actions/index.d.ts +3 -1
- package/dist/sdk/actions/index.js +3 -1
- package/dist/sdk/actions/payment-action-provider.d.ts +22 -0
- package/dist/sdk/actions/payment-action-provider.js +26 -0
- package/dist/sdk/actions/payment-action-provider.js.map +1 -0
- package/dist/sdk/actions/types.d.ts +6 -2
- package/dist/sdk/analytics/analytics-destination.d.ts +43 -0
- package/dist/sdk/analytics/analytics-destination.js +90 -0
- package/dist/sdk/analytics/analytics-destination.js.map +1 -0
- package/dist/sdk/analytics/use-analytics-actions.d.ts +57 -0
- package/dist/sdk/analytics/use-analytics-actions.js +109 -0
- package/dist/sdk/analytics/use-analytics-actions.js.map +1 -0
- package/dist/sdk/cart/use-cart-actions.js +10 -9
- package/dist/sdk/cart/use-cart-actions.js.map +1 -1
- package/dist/sdk/cart/use-cart-validate-on-open.js +2 -1
- package/dist/sdk/cart/use-cart-validate-on-open.js.map +1 -1
- package/dist/sdk/content/create-content-runtime.d.ts +22 -1
- package/dist/sdk/content/create-content-runtime.js +89 -1
- package/dist/sdk/content/create-content-runtime.js.map +1 -1
- package/dist/sdk/data/empty-content-value.js +16 -0
- package/dist/sdk/data/empty-content-value.js.map +1 -0
- package/dist/sdk/data/entity-resolution-context.js +31 -0
- package/dist/sdk/data/entity-resolution-context.js.map +1 -0
- package/dist/sdk/data/entity-subscriber.js +18 -8
- package/dist/sdk/data/entity-subscriber.js.map +1 -1
- package/dist/sdk/data/hot-data-provider.js +2 -1
- package/dist/sdk/data/hot-data-provider.js.map +1 -1
- package/dist/sdk/data/index.js +2 -1
- package/dist/sdk/data/page-reactive-data-root.d.ts +36 -0
- package/dist/sdk/data/page-reactive-data-root.js +4 -2
- package/dist/sdk/data/page-reactive-data-root.js.map +1 -1
- package/dist/sdk/data/reactive-data-provider.js +7 -3
- package/dist/sdk/data/reactive-data-provider.js.map +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.js +12 -4
- package/dist/sdk/data/reactive-list-item-data-provider.js.map +1 -1
- package/dist/sdk/data/use-reactive-data-settled.js +39 -0
- package/dist/sdk/data/use-reactive-data-settled.js.map +1 -0
- package/dist/sdk/data/use-reactive-entity.js +5 -1
- package/dist/sdk/data/use-reactive-entity.js.map +1 -1
- package/dist/sdk/data/variables-from-content.d.ts +43 -0
- package/dist/sdk/data/variables-from-content.js +57 -0
- package/dist/sdk/data/variables-from-content.js.map +1 -0
- package/dist/sdk/generated/fragment-types.js +2 -0
- package/dist/sdk/generated/fragment-types.js.map +1 -1
- package/dist/sdk/generated/gql.js +2 -1
- package/dist/sdk/generated/gql.js.map +1 -1
- package/dist/sdk/generated/graphql.js +85 -1
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/i18n/messages/en-US.js +3 -0
- package/dist/sdk/i18n/messages/en-US.json +3 -0
- package/dist/sdk/i18n/messages/en-US.json.d.ts +3 -0
- package/dist/sdk/i18n/messages/es-ES.js +3 -0
- package/dist/sdk/i18n/messages/es-ES.json +3 -0
- package/dist/sdk/i18n/messages/pt-BR.js +3 -0
- package/dist/sdk/i18n/messages/pt-BR.json +3 -0
- package/dist/sdk/identity/use-identity-actions.js +8 -6
- package/dist/sdk/identity/use-identity-actions.js.map +1 -1
- package/dist/sdk/payment/gateway-origin.js +35 -0
- package/dist/sdk/payment/gateway-origin.js.map +1 -0
- package/dist/sdk/payment/gateway.d.ts +85 -0
- package/dist/sdk/payment/gateway.js +183 -0
- package/dist/sdk/payment/gateway.js.map +1 -0
- package/dist/sdk/payment/operations.d.ts +52 -0
- package/dist/sdk/payment/operations.js +223 -0
- package/dist/sdk/payment/operations.js.map +1 -0
- package/dist/sdk/payment/security-code-mask.js +71 -0
- package/dist/sdk/payment/security-code-mask.js.map +1 -0
- package/dist/sdk/payment/submission-registry.d.ts +33 -0
- package/dist/sdk/payment/submission-registry.js +58 -0
- package/dist/sdk/payment/submission-registry.js.map +1 -0
- package/dist/sdk/payment/use-payment-actions.d.ts +78 -0
- package/dist/sdk/payment/use-payment-actions.js +276 -0
- package/dist/sdk/payment/use-payment-actions.js.map +1 -0
- package/dist/sdk/payment/use-place-order.d.ts +65 -0
- package/dist/sdk/payment/use-place-order.js +640 -0
- package/dist/sdk/payment/use-place-order.js.map +1 -0
- package/dist/sdk/region/use-region-actions.js +2 -1
- package/dist/sdk/region/use-region-actions.js.map +1 -1
- package/dist/sdk/session/read-server-session.d.ts +21 -0
- package/dist/sdk/session/read-server-session.js +61 -0
- package/dist/sdk/session/read-server-session.js.map +1 -0
- package/dist/sdk/session/use-session-actions.js +5 -4
- package/dist/sdk/session/use-session-actions.js.map +1 -1
- package/dist/sdk/session/use-session-revalidation.js +2 -1
- package/dist/sdk/session/use-session-revalidation.js.map +1 -1
- package/dist/sdk/shipping/use-shipping-actions.js +4 -3
- package/dist/sdk/shipping/use-shipping-actions.js.map +1 -1
- package/dist/utils/loggable-error-name.js +49 -0
- package/dist/utils/loggable-error-name.js.map +1 -0
- package/dist/utils/resolve-authored-copy.js +18 -0
- package/dist/utils/resolve-authored-copy.js.map +1 -0
- package/dist/utils/safe-href.d.ts +11 -0
- package/dist/utils/substitute-template.js +33 -0
- package/dist/utils/substitute-template.js.map +1 -0
- package/package.json +14 -2
- package/src/styles/tailwind.css +34 -0
- package/dist/docs/client-profile-form.md +0 -192
- package/dist/matter/client-profile-form/client-profile-form.d.ts +0 -126
- package/dist/matter/client-profile-form/client-profile-form.js +0 -662
- package/dist/matter/client-profile-form/client-profile-form.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-cart-actions.js","names":[],"sources":["../../../src/sdk/cart/use-cart-actions.ts"],"sourcesContent":["'use client';\n\nimport type { ApolloClient } from '@apollo/client';\nimport { useApolloClient, useMutation } from '@apollo/client/react';\nimport { defineActions } from '@vtex/fs-core';\nimport { useCallback, useMemo } from 'react';\nimport { cartCouponErrorsVar, cartLastAddVar, cartUserErrorsVar, orderFormIdVar } from '../state/cart.js';\nimport {\n CART_COUPON_APPLY,\n CART_COUPON_REMOVE,\n CART_ITEM_ADD,\n CART_ITEM_QUANTITY_DECREASE,\n CART_ITEM_QUANTITY_INCREASE,\n CART_ITEM_REMOVE,\n CART_ITEMS_REMOVE,\n CART_VALIDATE,\n type CartEntity,\n CartFragment,\n type CartItemEntity,\n type CartUserError,\n} from './operations.js';\nimport {\n buildOptimisticDecrease,\n buildOptimisticIncrease,\n buildOptimisticRemove,\n buildOptimisticRemoveItems,\n} from './optimistic.js';\nimport { markAvailabilityFresh, trackCartWrite } from './write-gate.js';\n\n/**\n * The `cart` action namespace — the consolidated, context-free owner of every cart write\n * (docs/storefront-core/reference-free-particle-model.md § The cart write contract, docs/storefront-core/adr-0010-actions-own-the-write-side.md).\n *\n * It replaces `useCartMutations` + the mutation handlers `CartRoot` used to publish through\n * `CartDataContext`. A handler reads the current cart from the normalized Apollo cache (keyed by\n * `orderFormIdVar`), not from component props or React context, so any component anywhere in the\n * tree can dispatch a write through the compiler-injected `$actions.cart.*` prop — the cache is the\n * only meeting point between the read side and the write side (ADR-0010).\n *\n * `userErrors` from every call are published to `cartUserErrorsVar` (cleared at the start of the\n * next call) so `CartError` can surface them without a data context, and also returned to the caller.\n */\n\n/**\n * Thrown by the existing-line handlers (remove / quantity) when invoked before the cart resolves.\n * The first `addItem` mints the cart and hydrates `orderFormIdVar`; until then there is no cached\n * cart entity to read for the optimistic response.\n */\nexport class CartNotReadyError extends Error {\n constructor(message?: string) {\n super(message ?? 'CartNotReadyError: no cart in cache yet; add an item (or load the cart) before mutating a line.');\n this.name = 'CartNotReadyError';\n }\n}\n\nexport type AddItemPayload = Readonly<{\n variantId: string;\n quantity?: number;\n /** Seller global Node ID, e.g. base64(\"Seller:1\"). The server rejects raw ids with INVALID_SELLER_ID. */\n seller: string;\n}>;\n\n/** Existing-line payloads address the line by its opaque cart-item `id`; the handler reads the rest from cache. */\nexport type CartLinePayload = Readonly<{ id: string }>;\n\n/** Batch payloads address several lines by their opaque cart-item `id`s. */\nexport type CartLinesPayload = Readonly<{ ids: readonly string[] }>;\n\nexport type CartQuantityPayload = Readonly<{ id: string; by?: number }>;\n\n/** Coupon writes address the coupon by its `code`; the server resolves the cart from the cookie. */\nexport type CartCouponPayload = Readonly<{ code: string }>;\n\n/**\n * The `cart` namespace surface. Item writes return the resulting `userErrors` (empty on success)\n * and publish them to `cartUserErrorsVar`; coupon writes publish to the dedicated\n * `cartCouponErrorsVar` (docs/storefront-core/reference-free-particle-model.md § The cart write contract).\n */\nexport type CartActions = Readonly<{\n /** Add an item to the cart. Async (no optimistic response). Creates the cart on first add. */\n addItem: (payload: AddItemPayload) => Promise<readonly CartUserError[]>;\n /** Remove a line entirely. Optimistic. */\n removeItem: (payload: CartLinePayload) => Promise<readonly CartUserError[]>;\n /** Remove several lines in one mutation. Optimistic. Ids missing from the cache are skipped. */\n removeItems: (payload: CartLinesPayload) => Promise<readonly CartUserError[]>;\n /** Increase a line's quantity by N (default 1). Optimistic. */\n increaseQuantity: (payload: CartQuantityPayload) => Promise<readonly CartUserError[]>;\n /** Decrease a line's quantity by N (default 1). Optimistic. Removes the line when result <= 0. */\n decreaseQuantity: (payload: CartQuantityPayload) => Promise<readonly CartUserError[]>;\n /** Apply a coupon code. Async (no optimistic). Publishes userErrors to cartCouponErrorsVar. */\n applyCoupon: (payload: CartCouponPayload) => Promise<readonly CartUserError[]>;\n /** Remove a coupon code by code. Async (no optimistic). Publishes userErrors to cartCouponErrorsVar. */\n removeCoupon: (payload: CartCouponPayload) => Promise<readonly CartUserError[]>;\n /**\n * Re-check the active cart against the platform and adopt corrected availability. Async (no\n * optimistic), read-only upstream. Returns userErrors (empty on success) to its caller and stamps\n * the freshness window on success. Unlike the item writes it does NOT publish to\n * `cartUserErrorsVar` — a speculative open-time failure must raise no error banner (Decision 6).\n */\n validate: () => Promise<readonly CartUserError[]>;\n}>;\n\nconst NETWORK_USER_ERROR: CartUserError = {\n code: 'NETWORK_ERROR',\n message: 'Unable to update cart. Please try again.',\n};\n\nfunction publish(errors: readonly CartUserError[]): readonly CartUserError[] {\n cartUserErrorsVar(errors);\n return errors;\n}\n\n/** Coupon-scoped publish — keeps coupon feedback off the shared item-error channel (AD-2). */\nfunction publishCoupon(errors: readonly CartUserError[]): readonly CartUserError[] {\n cartCouponErrorsVar(errors);\n return errors;\n}\n\n/**\n * Publish a write's outcome and, when it came back clean, stamp the availability freshness window —\n * a clean write's payload carries the platform's recomputed cart, which is what lets the\n * validate-on-open call skip (docs/storefront-core/reference-free-particle-model.md § The cart write contract). Used only on paths\n * where a mutation actually reached the server: a `userErrors` response carries `cart: null` and a\n * no-op (unknown line) issues no request, so neither refreshes availability.\n */\nfunction publishWrite(errors: readonly CartUserError[]): readonly CartUserError[] {\n if (errors.length === 0) markAvailabilityFresh();\n return publish(errors);\n}\n\n/** Coupon-channel counterpart of `publishWrite`. */\nfunction publishCouponWrite(errors: readonly CartUserError[]): readonly CartUserError[] {\n if (errors.length === 0) markAvailabilityFresh();\n return publishCoupon(errors);\n}\n\n/**\n * Backfill `orderFormIdVar` from a mutation's returned `cart.id` when local state is still cold.\n * A coupon op can be the first cart write (SSR cold / drawer closed on first load); without this the\n * later `requireLine` paths short-circuit on `orderFormIdVar() === null` and throw `CartNotReadyError`.\n * Mirrors the `addItem` first-add handoff.\n */\nfunction hydrateCartId(cartId: string | null): void {\n if (cartId && orderFormIdVar() === null) orderFormIdVar(cartId);\n}\n\n/**\n * Read the live cart entity from the normalized cache (ADR-0010), keyed by `orderFormIdVar`. Returns\n * null before the cart is minted/loaded. Module-level so it can be shared with the read-only callers\n * that live outside this hook — the pre-checkout gate re-reads the corrected cart this way after\n * `validate` resolves, because Apollo commits the mutation's cache write before the promise settles\n * (docs/storefront-core/reference-cart.md § Validation). The codegen `CartFragment` document embeds its\n * nested fragments, so it is a self-contained read.\n */\nexport function readCartFromCache(client: ApolloClient): CartEntity | null {\n const cartId = orderFormIdVar();\n if (!cartId) return null;\n const cacheId = client.cache.identify({ __typename: 'Cart', id: cartId });\n if (!cacheId) return null;\n return (\n client.cache.readFragment<CartEntity>({ id: cacheId, fragment: CartFragment, fragmentName: 'CartFragment' }) ?? null\n );\n}\n\n/**\n * The `cart` namespace hook. Registered with the first-party action registry via `cartActions`\n * below; the compiler-emitted `CartActionProvider` calls it and threads the narrowed methods into\n * each consumer's `$actions.cart`.\n */\nexport function useCartActions(): CartActions {\n const client = useApolloClient();\n\n const [addMutation] = useMutation(CART_ITEM_ADD);\n const [removeMutation] = useMutation(CART_ITEM_REMOVE);\n const [removeItemsMutation] = useMutation(CART_ITEMS_REMOVE);\n const [increaseMutation] = useMutation(CART_ITEM_QUANTITY_INCREASE);\n const [decreaseMutation] = useMutation(CART_ITEM_QUANTITY_DECREASE);\n const [applyCouponMutation] = useMutation(CART_COUPON_APPLY);\n const [removeCouponMutation] = useMutation(CART_COUPON_REMOVE);\n const [validateMutation] = useMutation(CART_VALIDATE);\n\n const readCart = useCallback((): CartEntity | null => readCartFromCache(client), [client]);\n\n const requireLine = useCallback(\n (id: string): { cart: CartEntity; line: CartItemEntity } | null => {\n const cart = readCart();\n if (!cart) throw new CartNotReadyError();\n const line = cart.items.find((item) => item.id === id);\n if (!line) return null;\n return { cart, line };\n },\n [readCart],\n );\n\n const addItem = useCallback<CartActions['addItem']>(\n async (payload) => {\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n addMutation({\n variables: {\n input: { variantId: payload.variantId, quantity: payload.quantity ?? 1, seller: payload.seller },\n },\n }),\n );\n // Very-first add: the server minted a cart from the empty transport context. Hydrate the\n // var so the existing-line handlers unlock without requiring a cart query.\n if (orderFormIdVar() === null) {\n const newId = result.data?.cartItemAdd?.cart?.id ?? null;\n if (newId) orderFormIdVar(newId);\n }\n const errors = result.data?.cartItemAdd?.userErrors ?? [];\n // Bump the add-success pulse only on the clean branch; the post-add behavior components\n // (CartOpenOnAdd / CartRedirectOnAdd) key their effect on it. The userErrors and\n // network-catch branches leave it untouched (docs/storefront-core/reference-free-particle-model.md § The cart write contract).\n if (errors.length === 0) cartLastAddVar(cartLastAddVar() + 1);\n return publishWrite(errors);\n } catch (error) {\n console.error('[cart.addItem] cartItemAdd failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [addMutation],\n );\n\n const removeItem = useCallback<CartActions['removeItem']>(\n async (payload) => {\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n removeMutation({\n variables: { input: { variantId: found.line.variant.variantId } },\n optimisticResponse: buildOptimisticRemove(found.cart, found.line),\n }),\n );\n return publishWrite(result.data?.cartItemRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeItem] cartItemRemove failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, removeMutation],\n );\n\n const removeItems = useCallback<CartActions['removeItems']>(\n async (payload) => {\n const cart = readCart();\n if (!cart) throw new CartNotReadyError();\n // Client-side tolerance mirroring removeItem's not-found no-op: ids without a cached line\n // are skipped, and the surviving set is deduped before the single mutation.\n const lines = cart.items.filter((item) => payload.ids.includes(item.id));\n if (lines.length === 0) return publish([]);\n const variantIds = [...new Set(lines.map((line) => line.variant.variantId))];\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n removeItemsMutation({\n variables: { input: { variantIds } },\n optimisticResponse: buildOptimisticRemoveItems(cart, lines),\n }),\n );\n return publishWrite(result.data?.cartItemsRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeItems] cartItemsRemove failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [readCart, removeItemsMutation],\n );\n\n const increaseQuantity = useCallback<CartActions['increaseQuantity']>(\n async (payload) => {\n const by = payload.by ?? 1;\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n increaseMutation({\n variables: { input: { variantId: found.line.variant.variantId, quantityToAdd: by } },\n optimisticResponse: buildOptimisticIncrease(found.cart, found.line, by),\n }),\n );\n return publishWrite(result.data?.cartItemQuantityIncrease?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.increaseQuantity] cartItemQuantityIncrease failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, increaseMutation],\n );\n\n const decreaseQuantity = useCallback<CartActions['decreaseQuantity']>(\n async (payload) => {\n const by = payload.by ?? 1;\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n decreaseMutation({\n variables: { input: { variantId: found.line.variant.variantId, quantityToRemove: by } },\n optimisticResponse: buildOptimisticDecrease(found.cart, found.line, by),\n }),\n );\n return publishWrite(result.data?.cartItemQuantityDecrease?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.decreaseQuantity] cartItemQuantityDecrease failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, decreaseMutation],\n );\n\n // Coupons are cart-level: no `requireLine` gate (AD-9). No optimistic response and no client\n // rollback — totals and `isUnmatchedCondition` are server-computed and the server rolls back its\n // own single-coupon rejections; the returned cart (spreading the extended CartFragment)\n // self-updates the cache and every reactive subscriber (docs/storefront-core/reference-free-particle-model.md § The cart write contract).\n const applyCoupon = useCallback<CartActions['applyCoupon']>(\n async (payload) => {\n cartCouponErrorsVar([]);\n try {\n const result = await trackCartWrite(applyCouponMutation({ variables: { input: { code: payload.code } } }));\n hydrateCartId(result.data?.cartCouponApply?.cart?.id ?? null);\n return publishCouponWrite(result.data?.cartCouponApply?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.applyCoupon] cartCouponApply failed', error);\n return publishCoupon([NETWORK_USER_ERROR]);\n }\n },\n [applyCouponMutation],\n );\n\n const removeCoupon = useCallback<CartActions['removeCoupon']>(\n async (payload) => {\n cartCouponErrorsVar([]);\n try {\n const result = await trackCartWrite(removeCouponMutation({ variables: { input: { code: payload.code } } }));\n hydrateCartId(result.data?.cartCouponRemove?.cart?.id ?? null);\n return publishCouponWrite(result.data?.cartCouponRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeCoupon] cartCouponRemove failed', error);\n return publishCoupon([NETWORK_USER_ERROR]);\n }\n },\n [removeCouponMutation],\n );\n\n // Read-only upstream (docs/storefront-core/reference-free-particle-model.md § The cart write contract): re-simulates the cart and returns it\n // with `variant.availability` corrected, spreading CartFragment so the guard, the per-line alert,\n // and the badge re-render off the cache. Deliberately unlike the item writes: it does NOT register\n // as an in-flight write (it would gate itself) and it does NOT publish to cartUserErrorsVar — the\n // open call is speculative and the pre-checkout call is fail-open, so a failure must raise no error\n // banner (Decision 6). It stamps the freshness window on success so open-on-add does not re-check.\n const validate = useCallback<CartActions['validate']>(async () => {\n try {\n const result = await validateMutation();\n // Hydrate as the coupon writes do: the corrected cart is now in the cache, and the pre-checkout\n // gate finds it by `orderFormIdVar`. Without this a cold var would leave the gate reading null\n // and passing a cart it never actually checked.\n hydrateCartId(result.data?.cartValidate?.cart?.id ?? null);\n const errors = result.data?.cartValidate?.userErrors ?? [];\n if (errors.length === 0) markAvailabilityFresh();\n return errors;\n } catch (error) {\n console.error('[cart.validate] cartValidate failed', error);\n return [NETWORK_USER_ERROR];\n }\n }, [validateMutation]);\n\n return useMemo(\n () => ({\n addItem,\n removeItem,\n removeItems,\n increaseQuantity,\n decreaseQuantity,\n applyCoupon,\n removeCoupon,\n validate,\n }),\n [addItem, removeItem, removeItems, increaseQuantity, decreaseQuantity, applyCoupon, removeCoupon, validate],\n );\n}\n\n/**\n * Declares the `cart` action namespace — the single source of truth for its method/payload metadata,\n * runtime provider, and `use` hook (mirrors `defineComponent`). The native catalog builder\n * (`@vtex/faststore-catalog`'s `buildPackageCatalog`) statically reads this call to emit `actions.json`;\n * at runtime `CartActionProvider` calls `useCartActions` directly.\n */\nexport const cartActions = defineActions({\n namespace: 'cart',\n description: 'Mutation-backed writes against the current cart (consolidates useCartMutations / CartRoot).',\n provider: { name: 'CartActionProvider', importPath: '@vtex/faststore/internal/client' },\n methods: {\n addItem: {\n description: 'Add an item to the cart (async strategy). Creates the cart on first add.',\n payload: [\n { name: 'variantId', type: 'string' },\n { name: 'seller', type: 'string' },\n { name: 'quantity', type: 'number', optional: true },\n ],\n },\n removeItem: {\n description: 'Remove a line from the cart (optimistic strategy).',\n payload: [{ name: 'id', type: 'string' }],\n },\n removeItems: {\n description: 'Remove several lines from the cart in one call (optimistic strategy).',\n payload: [{ name: 'ids', type: 'string[]' }],\n },\n increaseQuantity: {\n description: \"Increase a line's quantity by N (default 1, optimistic strategy).\",\n payload: [\n { name: 'id', type: 'string' },\n { name: 'by', type: 'number', optional: true },\n ],\n },\n decreaseQuantity: {\n description: \"Decrease a line's quantity by N (default 1, optimistic). Removes the line at <= 0.\",\n payload: [\n { name: 'id', type: 'string' },\n { name: 'by', type: 'number', optional: true },\n ],\n },\n applyCoupon: {\n description: 'Apply a coupon code to the cart (async strategy). Server recomputes totals.',\n payload: [{ name: 'code', type: 'string' }],\n },\n removeCoupon: {\n description: 'Remove a coupon code from the cart (async strategy). Server recomputes totals.',\n payload: [{ name: 'code', type: 'string' }],\n },\n validate: {\n description:\n 'Re-check the active cart against the platform and adopt corrected availability (async, read-only upstream).',\n payload: [],\n },\n },\n use: useCartActions,\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-cart-actions.js","names":[],"sources":["../../../src/sdk/cart/use-cart-actions.ts"],"sourcesContent":["'use client';\n\nimport type { ApolloClient } from '@apollo/client';\nimport { useApolloClient, useMutation } from '@apollo/client/react';\nimport { defineActions } from '@vtex/fs-core';\nimport { useCallback, useMemo } from 'react';\nimport { loggableErrorName } from '../../utils/loggable-error-name.js';\nimport { cartCouponErrorsVar, cartLastAddVar, cartUserErrorsVar, orderFormIdVar } from '../state/cart.js';\nimport {\n CART_COUPON_APPLY,\n CART_COUPON_REMOVE,\n CART_ITEM_ADD,\n CART_ITEM_QUANTITY_DECREASE,\n CART_ITEM_QUANTITY_INCREASE,\n CART_ITEM_REMOVE,\n CART_ITEMS_REMOVE,\n CART_VALIDATE,\n type CartEntity,\n CartFragment,\n type CartItemEntity,\n type CartUserError,\n} from './operations.js';\nimport {\n buildOptimisticDecrease,\n buildOptimisticIncrease,\n buildOptimisticRemove,\n buildOptimisticRemoveItems,\n} from './optimistic.js';\nimport { markAvailabilityFresh, trackCartWrite } from './write-gate.js';\n\n/**\n * The `cart` action namespace — the consolidated, context-free owner of every cart write\n * (docs/storefront-core/reference-free-particle-model.md § The cart write contract, docs/storefront-core/adr-0010-actions-own-the-write-side.md).\n *\n * It replaces `useCartMutations` + the mutation handlers `CartRoot` used to publish through\n * `CartDataContext`. A handler reads the current cart from the normalized Apollo cache (keyed by\n * `orderFormIdVar`), not from component props or React context, so any component anywhere in the\n * tree can dispatch a write through the compiler-injected `$actions.cart.*` prop — the cache is the\n * only meeting point between the read side and the write side (ADR-0010).\n *\n * `userErrors` from every call are published to `cartUserErrorsVar` (cleared at the start of the\n * next call) so `CartError` can surface them without a data context, and also returned to the caller.\n */\n\n/**\n * Thrown by the existing-line handlers (remove / quantity) when invoked before the cart resolves.\n * The first `addItem` mints the cart and hydrates `orderFormIdVar`; until then there is no cached\n * cart entity to read for the optimistic response.\n */\nexport class CartNotReadyError extends Error {\n constructor(message?: string) {\n super(message ?? 'CartNotReadyError: no cart in cache yet; add an item (or load the cart) before mutating a line.');\n this.name = 'CartNotReadyError';\n }\n}\n\nexport type AddItemPayload = Readonly<{\n variantId: string;\n quantity?: number;\n /** Seller global Node ID, e.g. base64(\"Seller:1\"). The server rejects raw ids with INVALID_SELLER_ID. */\n seller: string;\n}>;\n\n/** Existing-line payloads address the line by its opaque cart-item `id`; the handler reads the rest from cache. */\nexport type CartLinePayload = Readonly<{ id: string }>;\n\n/** Batch payloads address several lines by their opaque cart-item `id`s. */\nexport type CartLinesPayload = Readonly<{ ids: readonly string[] }>;\n\nexport type CartQuantityPayload = Readonly<{ id: string; by?: number }>;\n\n/** Coupon writes address the coupon by its `code`; the server resolves the cart from the cookie. */\nexport type CartCouponPayload = Readonly<{ code: string }>;\n\n/**\n * The `cart` namespace surface. Item writes return the resulting `userErrors` (empty on success)\n * and publish them to `cartUserErrorsVar`; coupon writes publish to the dedicated\n * `cartCouponErrorsVar` (docs/storefront-core/reference-free-particle-model.md § The cart write contract).\n */\nexport type CartActions = Readonly<{\n /** Add an item to the cart. Async (no optimistic response). Creates the cart on first add. */\n addItem: (payload: AddItemPayload) => Promise<readonly CartUserError[]>;\n /** Remove a line entirely. Optimistic. */\n removeItem: (payload: CartLinePayload) => Promise<readonly CartUserError[]>;\n /** Remove several lines in one mutation. Optimistic. Ids missing from the cache are skipped. */\n removeItems: (payload: CartLinesPayload) => Promise<readonly CartUserError[]>;\n /** Increase a line's quantity by N (default 1). Optimistic. */\n increaseQuantity: (payload: CartQuantityPayload) => Promise<readonly CartUserError[]>;\n /** Decrease a line's quantity by N (default 1). Optimistic. Removes the line when result <= 0. */\n decreaseQuantity: (payload: CartQuantityPayload) => Promise<readonly CartUserError[]>;\n /** Apply a coupon code. Async (no optimistic). Publishes userErrors to cartCouponErrorsVar. */\n applyCoupon: (payload: CartCouponPayload) => Promise<readonly CartUserError[]>;\n /** Remove a coupon code by code. Async (no optimistic). Publishes userErrors to cartCouponErrorsVar. */\n removeCoupon: (payload: CartCouponPayload) => Promise<readonly CartUserError[]>;\n /**\n * Re-check the active cart against the platform and adopt corrected availability. Async (no\n * optimistic), read-only upstream. Returns userErrors (empty on success) to its caller and stamps\n * the freshness window on success. Unlike the item writes it does NOT publish to\n * `cartUserErrorsVar` — a speculative open-time failure must raise no error banner (Decision 6).\n */\n validate: () => Promise<readonly CartUserError[]>;\n}>;\n\nconst NETWORK_USER_ERROR: CartUserError = {\n code: 'NETWORK_ERROR',\n message: 'Unable to update cart. Please try again.',\n};\n\nfunction publish(errors: readonly CartUserError[]): readonly CartUserError[] {\n cartUserErrorsVar(errors);\n return errors;\n}\n\n/** Coupon-scoped publish — keeps coupon feedback off the shared item-error channel (AD-2). */\nfunction publishCoupon(errors: readonly CartUserError[]): readonly CartUserError[] {\n cartCouponErrorsVar(errors);\n return errors;\n}\n\n/**\n * Publish a write's outcome and, when it came back clean, stamp the availability freshness window —\n * a clean write's payload carries the platform's recomputed cart, which is what lets the\n * validate-on-open call skip (docs/storefront-core/reference-free-particle-model.md § The cart write contract). Used only on paths\n * where a mutation actually reached the server: a `userErrors` response carries `cart: null` and a\n * no-op (unknown line) issues no request, so neither refreshes availability.\n */\nfunction publishWrite(errors: readonly CartUserError[]): readonly CartUserError[] {\n if (errors.length === 0) markAvailabilityFresh();\n return publish(errors);\n}\n\n/** Coupon-channel counterpart of `publishWrite`. */\nfunction publishCouponWrite(errors: readonly CartUserError[]): readonly CartUserError[] {\n if (errors.length === 0) markAvailabilityFresh();\n return publishCoupon(errors);\n}\n\n/**\n * Backfill `orderFormIdVar` from a mutation's returned `cart.id` when local state is still cold.\n * A coupon op can be the first cart write (SSR cold / drawer closed on first load); without this the\n * later `requireLine` paths short-circuit on `orderFormIdVar() === null` and throw `CartNotReadyError`.\n * Mirrors the `addItem` first-add handoff.\n */\nfunction hydrateCartId(cartId: string | null): void {\n if (cartId && orderFormIdVar() === null) orderFormIdVar(cartId);\n}\n\n/**\n * Read the live cart entity from the normalized cache (ADR-0010), keyed by `orderFormIdVar`. Returns\n * null before the cart is minted/loaded. Module-level so it can be shared with the read-only callers\n * that live outside this hook — the pre-checkout gate re-reads the corrected cart this way after\n * `validate` resolves, because Apollo commits the mutation's cache write before the promise settles\n * (docs/storefront-core/reference-cart.md § Validation). The codegen `CartFragment` document embeds its\n * nested fragments, so it is a self-contained read.\n */\nexport function readCartFromCache(client: ApolloClient): CartEntity | null {\n const cartId = orderFormIdVar();\n if (!cartId) return null;\n const cacheId = client.cache.identify({ __typename: 'Cart', id: cartId });\n if (!cacheId) return null;\n return (\n client.cache.readFragment<CartEntity>({ id: cacheId, fragment: CartFragment, fragmentName: 'CartFragment' }) ?? null\n );\n}\n\n/**\n * The `cart` namespace hook. Registered with the first-party action registry via `cartActions`\n * below; the compiler-emitted `CartActionProvider` calls it and threads the narrowed methods into\n * each consumer's `$actions.cart`.\n */\nexport function useCartActions(): CartActions {\n const client = useApolloClient();\n\n const [addMutation] = useMutation(CART_ITEM_ADD);\n const [removeMutation] = useMutation(CART_ITEM_REMOVE);\n const [removeItemsMutation] = useMutation(CART_ITEMS_REMOVE);\n const [increaseMutation] = useMutation(CART_ITEM_QUANTITY_INCREASE);\n const [decreaseMutation] = useMutation(CART_ITEM_QUANTITY_DECREASE);\n const [applyCouponMutation] = useMutation(CART_COUPON_APPLY);\n const [removeCouponMutation] = useMutation(CART_COUPON_REMOVE);\n const [validateMutation] = useMutation(CART_VALIDATE);\n\n const readCart = useCallback((): CartEntity | null => readCartFromCache(client), [client]);\n\n const requireLine = useCallback(\n (id: string): { cart: CartEntity; line: CartItemEntity } | null => {\n const cart = readCart();\n if (!cart) throw new CartNotReadyError();\n const line = cart.items.find((item) => item.id === id);\n if (!line) return null;\n return { cart, line };\n },\n [readCart],\n );\n\n const addItem = useCallback<CartActions['addItem']>(\n async (payload) => {\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n addMutation({\n variables: {\n input: { variantId: payload.variantId, quantity: payload.quantity ?? 1, seller: payload.seller },\n },\n }),\n );\n // Very-first add: the server minted a cart from the empty transport context. Hydrate the\n // var so the existing-line handlers unlock without requiring a cart query.\n if (orderFormIdVar() === null) {\n const newId = result.data?.cartItemAdd?.cart?.id ?? null;\n if (newId) orderFormIdVar(newId);\n }\n const errors = result.data?.cartItemAdd?.userErrors ?? [];\n // Bump the add-success pulse only on the clean branch; the post-add behavior components\n // (CartOpenOnAdd / CartRedirectOnAdd) key their effect on it. The userErrors and\n // network-catch branches leave it untouched (docs/storefront-core/reference-free-particle-model.md § The cart write contract).\n if (errors.length === 0) cartLastAddVar(cartLastAddVar() + 1);\n return publishWrite(errors);\n } catch (error) {\n console.error('[cart.addItem] cartItemAdd failed', loggableErrorName(error));\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [addMutation],\n );\n\n const removeItem = useCallback<CartActions['removeItem']>(\n async (payload) => {\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n removeMutation({\n variables: { input: { variantId: found.line.variant.variantId } },\n optimisticResponse: buildOptimisticRemove(found.cart, found.line),\n }),\n );\n return publishWrite(result.data?.cartItemRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeItem] cartItemRemove failed', loggableErrorName(error));\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, removeMutation],\n );\n\n const removeItems = useCallback<CartActions['removeItems']>(\n async (payload) => {\n const cart = readCart();\n if (!cart) throw new CartNotReadyError();\n // Client-side tolerance mirroring removeItem's not-found no-op: ids without a cached line\n // are skipped, and the surviving set is deduped before the single mutation.\n const lines = cart.items.filter((item) => payload.ids.includes(item.id));\n if (lines.length === 0) return publish([]);\n const variantIds = [...new Set(lines.map((line) => line.variant.variantId))];\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n removeItemsMutation({\n variables: { input: { variantIds } },\n optimisticResponse: buildOptimisticRemoveItems(cart, lines),\n }),\n );\n return publishWrite(result.data?.cartItemsRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeItems] cartItemsRemove failed', loggableErrorName(error));\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [readCart, removeItemsMutation],\n );\n\n const increaseQuantity = useCallback<CartActions['increaseQuantity']>(\n async (payload) => {\n const by = payload.by ?? 1;\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n increaseMutation({\n variables: { input: { variantId: found.line.variant.variantId, quantityToAdd: by } },\n optimisticResponse: buildOptimisticIncrease(found.cart, found.line, by),\n }),\n );\n return publishWrite(result.data?.cartItemQuantityIncrease?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.increaseQuantity] cartItemQuantityIncrease failed', loggableErrorName(error));\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, increaseMutation],\n );\n\n const decreaseQuantity = useCallback<CartActions['decreaseQuantity']>(\n async (payload) => {\n const by = payload.by ?? 1;\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n decreaseMutation({\n variables: { input: { variantId: found.line.variant.variantId, quantityToRemove: by } },\n optimisticResponse: buildOptimisticDecrease(found.cart, found.line, by),\n }),\n );\n return publishWrite(result.data?.cartItemQuantityDecrease?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.decreaseQuantity] cartItemQuantityDecrease failed', loggableErrorName(error));\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, decreaseMutation],\n );\n\n // Coupons are cart-level: no `requireLine` gate (AD-9). No optimistic response and no client\n // rollback — totals and `isUnmatchedCondition` are server-computed and the server rolls back its\n // own single-coupon rejections; the returned cart (spreading the extended CartFragment)\n // self-updates the cache and every reactive subscriber (docs/storefront-core/reference-free-particle-model.md § The cart write contract).\n const applyCoupon = useCallback<CartActions['applyCoupon']>(\n async (payload) => {\n cartCouponErrorsVar([]);\n try {\n const result = await trackCartWrite(applyCouponMutation({ variables: { input: { code: payload.code } } }));\n hydrateCartId(result.data?.cartCouponApply?.cart?.id ?? null);\n return publishCouponWrite(result.data?.cartCouponApply?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.applyCoupon] cartCouponApply failed', loggableErrorName(error));\n return publishCoupon([NETWORK_USER_ERROR]);\n }\n },\n [applyCouponMutation],\n );\n\n const removeCoupon = useCallback<CartActions['removeCoupon']>(\n async (payload) => {\n cartCouponErrorsVar([]);\n try {\n const result = await trackCartWrite(removeCouponMutation({ variables: { input: { code: payload.code } } }));\n hydrateCartId(result.data?.cartCouponRemove?.cart?.id ?? null);\n return publishCouponWrite(result.data?.cartCouponRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeCoupon] cartCouponRemove failed', loggableErrorName(error));\n return publishCoupon([NETWORK_USER_ERROR]);\n }\n },\n [removeCouponMutation],\n );\n\n // Read-only upstream (docs/storefront-core/reference-free-particle-model.md § The cart write contract): re-simulates the cart and returns it\n // with `variant.availability` corrected, spreading CartFragment so the guard, the per-line alert,\n // and the badge re-render off the cache. Deliberately unlike the item writes: it does NOT register\n // as an in-flight write (it would gate itself) and it does NOT publish to cartUserErrorsVar — the\n // open call is speculative and the pre-checkout call is fail-open, so a failure must raise no error\n // banner (Decision 6). It stamps the freshness window on success so open-on-add does not re-check.\n const validate = useCallback<CartActions['validate']>(async () => {\n try {\n const result = await validateMutation();\n // Hydrate as the coupon writes do: the corrected cart is now in the cache, and the pre-checkout\n // gate finds it by `orderFormIdVar`. Without this a cold var would leave the gate reading null\n // and passing a cart it never actually checked.\n hydrateCartId(result.data?.cartValidate?.cart?.id ?? null);\n const errors = result.data?.cartValidate?.userErrors ?? [];\n if (errors.length === 0) markAvailabilityFresh();\n return errors;\n } catch (error) {\n console.error('[cart.validate] cartValidate failed', loggableErrorName(error));\n return [NETWORK_USER_ERROR];\n }\n }, [validateMutation]);\n\n return useMemo(\n () => ({\n addItem,\n removeItem,\n removeItems,\n increaseQuantity,\n decreaseQuantity,\n applyCoupon,\n removeCoupon,\n validate,\n }),\n [addItem, removeItem, removeItems, increaseQuantity, decreaseQuantity, applyCoupon, removeCoupon, validate],\n );\n}\n\n/**\n * Declares the `cart` action namespace — the single source of truth for its method/payload metadata,\n * runtime provider, and `use` hook (mirrors `defineComponent`). The native catalog builder\n * (`@vtex/faststore-catalog`'s `buildPackageCatalog`) statically reads this call to emit `actions.json`;\n * at runtime `CartActionProvider` calls `useCartActions` directly.\n */\nexport const cartActions = defineActions({\n namespace: 'cart',\n description: 'Mutation-backed writes against the current cart (consolidates useCartMutations / CartRoot).',\n provider: { name: 'CartActionProvider', importPath: '@vtex/faststore/internal/client' },\n methods: {\n addItem: {\n description: 'Add an item to the cart (async strategy). Creates the cart on first add.',\n payload: [\n { name: 'variantId', type: 'string' },\n { name: 'seller', type: 'string' },\n { name: 'quantity', type: 'number', optional: true },\n ],\n },\n removeItem: {\n description: 'Remove a line from the cart (optimistic strategy).',\n payload: [{ name: 'id', type: 'string' }],\n },\n removeItems: {\n description: 'Remove several lines from the cart in one call (optimistic strategy).',\n payload: [{ name: 'ids', type: 'string[]' }],\n },\n increaseQuantity: {\n description: \"Increase a line's quantity by N (default 1, optimistic strategy).\",\n payload: [\n { name: 'id', type: 'string' },\n { name: 'by', type: 'number', optional: true },\n ],\n },\n decreaseQuantity: {\n description: \"Decrease a line's quantity by N (default 1, optimistic). Removes the line at <= 0.\",\n payload: [\n { name: 'id', type: 'string' },\n { name: 'by', type: 'number', optional: true },\n ],\n },\n applyCoupon: {\n description: 'Apply a coupon code to the cart (async strategy). Server recomputes totals.',\n payload: [{ name: 'code', type: 'string' }],\n },\n removeCoupon: {\n description: 'Remove a coupon code from the cart (async strategy). Server recomputes totals.',\n payload: [{ name: 'code', type: 'string' }],\n },\n validate: {\n description:\n 'Re-check the active cart against the platform and adopt corrected availability (async, read-only upstream).',\n payload: [],\n },\n },\n use: useCartActions,\n});\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { loggableErrorName } from "../../utils/loggable-error-name.js";
|
|
2
3
|
import { cartAvailabilityFreshAtVar, cartWritesInFlightVar, miniCartOpenVar } from "../state/cart.js";
|
|
3
4
|
import { readCartFromCache } from "./use-cart-actions.js";
|
|
4
5
|
import { useApolloClient, useReactiveVar } from "@apollo/client/react";
|
|
@@ -34,7 +35,7 @@ function useCartValidateOnOpen(validate) {
|
|
|
34
35
|
if (!cart || cart.items.length === 0) return;
|
|
35
36
|
pending.current = true;
|
|
36
37
|
Promise.resolve(validate()).catch((error) => {
|
|
37
|
-
console.error("[cart.validateOnOpen] validate rejected", error);
|
|
38
|
+
console.error("[cart.validateOnOpen] validate rejected", loggableErrorName(error));
|
|
38
39
|
}).finally(() => {
|
|
39
40
|
pending.current = false;
|
|
40
41
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-cart-validate-on-open.js","names":[],"sources":["../../../src/sdk/cart/use-cart-validate-on-open.ts"],"sourcesContent":["'use client';\n\nimport { useApolloClient, useReactiveVar } from '@apollo/client/react';\nimport { useEffect, useRef } from 'react';\nimport {\n CART_VALIDATE_STALENESS_MS,\n cartAvailabilityFreshAtVar,\n cartWritesInFlightVar,\n miniCartOpenVar,\n} from '../state/cart.js';\nimport { readCartFromCache } from './use-cart-actions.js';\n\n/**\n * Runs stock validation when the mini-cart overlay opens (docs/storefront-core/reference-cart.md § Validation).\n *\n * `miniCartOpenVar` is the overlay's single source of truth — the add-pulse open, the header trigger,\n * and manual close all route through it — so watching its closed→open edge covers every way the\n * drawer opens. The call is bounded and cooperative: it fires at most once per transition, and skips\n * when there is nothing to gain — no validate action wired, a cart write in flight, availability\n * refreshed within the staleness window (which is why open-on-add does not re-validate), or an empty\n * cart. `validate` is the compiler-injected `$actions.cart.validate`; when it is absent the hook is a\n * no-op, so a store without the action wired pays nothing.\n *\n * A dispatch is reserved synchronously with a ref latch, cleared when the call settles. Freshness is\n * only stamped once `validate` resolves, so a close-and-reopen during that window would otherwise slip\n * past the staleness guard and fire a second call; the latch closes that gap.\n */\nexport function useCartValidateOnOpen(validate: (() => Promise<unknown>) | undefined): void {\n const client = useApolloClient();\n const open = useReactiveVar(miniCartOpenVar);\n const wasOpen = useRef(open);\n const pending = useRef(false);\n\n useEffect(() => {\n const opened = open && !wasOpen.current;\n wasOpen.current = open;\n if (!opened || !validate || pending.current) return;\n // A write in flight (or one that just settled, inside the window) already refreshed availability.\n if (cartWritesInFlightVar() > 0) return;\n if (Date.now() - cartAvailabilityFreshAtVar() < CART_VALIDATE_STALENESS_MS) return;\n // Nothing to validate on an empty cart, and T1 would only round-trip a single read for it.\n const cart = readCartFromCache(client);\n if (!cart || cart.items.length === 0) return;\n pending.current = true;\n // The current wiring never rejects (useCartActions().validate catches and resolves with\n // [NETWORK_USER_ERROR]); this guards a differently-injected validate so a rejection cannot\n // escape as an unhandled promise rejection.\n void Promise.resolve(validate())\n .catch((error) => {\n console.error('[cart.validateOnOpen] validate rejected', error);\n })\n .finally(() => {\n pending.current = false;\n });\n }, [open, validate, client]);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-cart-validate-on-open.js","names":[],"sources":["../../../src/sdk/cart/use-cart-validate-on-open.ts"],"sourcesContent":["'use client';\n\nimport { useApolloClient, useReactiveVar } from '@apollo/client/react';\nimport { useEffect, useRef } from 'react';\nimport { loggableErrorName } from '../../utils/loggable-error-name.js';\nimport {\n CART_VALIDATE_STALENESS_MS,\n cartAvailabilityFreshAtVar,\n cartWritesInFlightVar,\n miniCartOpenVar,\n} from '../state/cart.js';\nimport { readCartFromCache } from './use-cart-actions.js';\n\n/**\n * Runs stock validation when the mini-cart overlay opens (docs/storefront-core/reference-cart.md § Validation).\n *\n * `miniCartOpenVar` is the overlay's single source of truth — the add-pulse open, the header trigger,\n * and manual close all route through it — so watching its closed→open edge covers every way the\n * drawer opens. The call is bounded and cooperative: it fires at most once per transition, and skips\n * when there is nothing to gain — no validate action wired, a cart write in flight, availability\n * refreshed within the staleness window (which is why open-on-add does not re-validate), or an empty\n * cart. `validate` is the compiler-injected `$actions.cart.validate`; when it is absent the hook is a\n * no-op, so a store without the action wired pays nothing.\n *\n * A dispatch is reserved synchronously with a ref latch, cleared when the call settles. Freshness is\n * only stamped once `validate` resolves, so a close-and-reopen during that window would otherwise slip\n * past the staleness guard and fire a second call; the latch closes that gap.\n */\nexport function useCartValidateOnOpen(validate: (() => Promise<unknown>) | undefined): void {\n const client = useApolloClient();\n const open = useReactiveVar(miniCartOpenVar);\n const wasOpen = useRef(open);\n const pending = useRef(false);\n\n useEffect(() => {\n const opened = open && !wasOpen.current;\n wasOpen.current = open;\n if (!opened || !validate || pending.current) return;\n // A write in flight (or one that just settled, inside the window) already refreshed availability.\n if (cartWritesInFlightVar() > 0) return;\n if (Date.now() - cartAvailabilityFreshAtVar() < CART_VALIDATE_STALENESS_MS) return;\n // Nothing to validate on an empty cart, and T1 would only round-trip a single read for it.\n const cart = readCartFromCache(client);\n if (!cart || cart.items.length === 0) return;\n pending.current = true;\n // The current wiring never rejects (useCartActions().validate catches and resolves with\n // [NETWORK_USER_ERROR]); this guards a differently-injected validate so a rejection cannot\n // escape as an unhandled promise rejection.\n void Promise.resolve(validate())\n .catch((error) => {\n console.error('[cart.validateOnOpen] validate rejected', loggableErrorName(error));\n })\n .finally(() => {\n pending.current = false;\n });\n }, [open, validate, client]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,sBAAsB,UAAsD;CAC1F,MAAM,SAAS,gBAAgB;CAC/B,MAAM,OAAO,eAAe,eAAe;CAC3C,MAAM,UAAU,OAAO,IAAI;CAC3B,MAAM,UAAU,OAAO,KAAK;CAE5B,gBAAgB;EACd,MAAM,SAAS,QAAQ,CAAC,QAAQ;EAChC,QAAQ,UAAU;EAClB,IAAI,CAAC,UAAU,CAAC,YAAY,QAAQ,SAAS;EAE7C,IAAI,sBAAsB,IAAI,GAAG;EACjC,IAAI,KAAK,IAAI,IAAI,2BAA2B,IAAA,KAAgC;EAE5E,MAAM,OAAO,kBAAkB,MAAM;EACrC,IAAI,CAAC,QAAQ,KAAK,MAAM,WAAW,GAAG;EACtC,QAAQ,UAAU;EAIlB,QAAa,QAAQ,SAAS,CAAC,CAAC,CAC7B,OAAO,UAAU;GAChB,QAAQ,MAAM,2CAA2C,kBAAkB,KAAK,CAAC;EACnF,CAAC,CAAC,CACD,cAAc;GACb,QAAQ,UAAU;EACpB,CAAC;CACL,GAAG;EAAC;EAAM;EAAU;CAAM,CAAC;AAC7B"}
|
|
@@ -33,8 +33,28 @@ export type ContentEntryBoundaryProps = {
|
|
|
33
33
|
value: unknown;
|
|
34
34
|
children: ReactNode;
|
|
35
35
|
};
|
|
36
|
+
/** The coordinates of one item read. Object arguments, so two strings cannot be swapped at the call site. */
|
|
37
|
+
export type ReadContentItemArgs = {
|
|
38
|
+
contentTypeId: string;
|
|
39
|
+
slug: string;
|
|
40
|
+
locale: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* One item read. `found` is the resolved entry. `not-found` is HTTP 404 only. `unavailable` is every
|
|
44
|
+
* other failure with nothing stored. There is no `invalid` arm: a 200 JSON object is the blob.
|
|
45
|
+
*/
|
|
46
|
+
export type ContentItemResult = {
|
|
47
|
+
status: 'found';
|
|
48
|
+
blob: Record<string, unknown>;
|
|
49
|
+
} | {
|
|
50
|
+
status: 'not-found';
|
|
51
|
+
} | {
|
|
52
|
+
status: 'unavailable';
|
|
53
|
+
reason: string;
|
|
54
|
+
};
|
|
36
55
|
export type ContentRuntime = {
|
|
37
56
|
readContentSlices: (args: ReadContentSlicesArgs) => Promise<ContentSlices>;
|
|
57
|
+
readContentItem: (args: ReadContentItemArgs) => Promise<ContentItemResult>;
|
|
38
58
|
ContentEntryBoundary: FC<ContentEntryBoundaryProps>;
|
|
39
59
|
};
|
|
40
60
|
/**
|
|
@@ -61,7 +81,8 @@ export type ContentEntryResult = {
|
|
|
61
81
|
* `ContentEntryBoundary` the compiler wraps around each content-backed element. One read per scope
|
|
62
82
|
* yields every element's `$content` value keyed by `element.id`, so a page issues at most two Data
|
|
63
83
|
* Plane reads (its own entry plus its layout's) regardless of content-backed component count. Draft
|
|
64
|
-
* Mode switches
|
|
84
|
+
* Mode switches that composite read to Control Plane's preview endpoint through a server-only
|
|
85
|
+
* credential. `readContentItem` reads one entry by slug and does not consult Draft Mode.
|
|
65
86
|
*/
|
|
66
87
|
export declare function createContentRuntime(config: ContentRuntimeConfig): ContentRuntime;
|
|
67
88
|
//#endregion
|
|
@@ -48,7 +48,8 @@ const PREVIEW_COOKIE = "__fs_content_preview";
|
|
|
48
48
|
* `ContentEntryBoundary` the compiler wraps around each content-backed element. One read per scope
|
|
49
49
|
* yields every element's `$content` value keyed by `element.id`, so a page issues at most two Data
|
|
50
50
|
* Plane reads (its own entry plus its layout's) regardless of content-backed component count. Draft
|
|
51
|
-
* Mode switches
|
|
51
|
+
* Mode switches that composite read to Control Plane's preview endpoint through a server-only
|
|
52
|
+
* credential. `readContentItem` reads one entry by slug and does not consult Draft Mode.
|
|
52
53
|
*/
|
|
53
54
|
function createContentRuntime(config) {
|
|
54
55
|
const lastGood = /* @__PURE__ */ new Map();
|
|
@@ -119,6 +120,37 @@ function createContentRuntime(config) {
|
|
|
119
120
|
};
|
|
120
121
|
}
|
|
121
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The by-slug read plus the same fallback. Not a parameter of `readPublished`: the key, the URL,
|
|
125
|
+
* and the result taxonomy differ, and folding them together would put a slug on the composite log
|
|
126
|
+
* line. A `not-found` returns before the map is read or written.
|
|
127
|
+
*/
|
|
128
|
+
async function readPublishedItem(contentTypeId, slug, locale) {
|
|
129
|
+
const key = `item\u0000${contentTypeId}\u0000${slug}\u0000${locale}`;
|
|
130
|
+
try {
|
|
131
|
+
const result = await readItemBySlug(config.account, config.storeId, locale, contentTypeId, slug);
|
|
132
|
+
if (result.status === "found") retainBlob(key, result.blob, {
|
|
133
|
+
contentTypeId,
|
|
134
|
+
slug,
|
|
135
|
+
locale
|
|
136
|
+
});
|
|
137
|
+
return { result };
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const reason = transientReason(error);
|
|
140
|
+
const stored = lastGood.get(key);
|
|
141
|
+
if (stored === void 0) return { result: {
|
|
142
|
+
status: "unavailable",
|
|
143
|
+
reason
|
|
144
|
+
} };
|
|
145
|
+
return {
|
|
146
|
+
result: {
|
|
147
|
+
status: "found",
|
|
148
|
+
blob: stored
|
|
149
|
+
},
|
|
150
|
+
staleReason: reason
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
122
154
|
const read = cache(async (contentTypeId, entryId, locale) => {
|
|
123
155
|
await connection();
|
|
124
156
|
const { isEnabled: draft } = await draftMode();
|
|
@@ -175,8 +207,27 @@ function createContentRuntime(config) {
|
|
|
175
207
|
});
|
|
176
208
|
return slices;
|
|
177
209
|
}
|
|
210
|
+
const readItem = cache(async (contentTypeId, slug, locale) => {
|
|
211
|
+
await connection();
|
|
212
|
+
const { result, staleReason } = await readPublishedItem(contentTypeId, slug, locale);
|
|
213
|
+
const stale = staleReason !== void 0;
|
|
214
|
+
const { level, event } = stale ? STALE_READ_EVENT : READ_EVENTS[result.status];
|
|
215
|
+
const reason = staleReason ?? (result.status === "unavailable" ? result.reason : void 0);
|
|
216
|
+
if (!(!stale && result.status === "found" && process.env.NODE_ENV === "production")) logEvent(level, event, {
|
|
217
|
+
contentTypeId,
|
|
218
|
+
slug,
|
|
219
|
+
locale,
|
|
220
|
+
source: "data-plane",
|
|
221
|
+
...reason === void 0 ? {} : { reason }
|
|
222
|
+
});
|
|
223
|
+
return result;
|
|
224
|
+
});
|
|
225
|
+
async function readContentItem({ contentTypeId, slug, locale }) {
|
|
226
|
+
return readItem(contentTypeId, slug, locale);
|
|
227
|
+
}
|
|
178
228
|
return {
|
|
179
229
|
readContentSlices,
|
|
230
|
+
readContentItem,
|
|
180
231
|
ContentEntryBoundary
|
|
181
232
|
};
|
|
182
233
|
}
|
|
@@ -231,6 +282,32 @@ async function readPublishedEntry(account, storeId, locale, contentTypeId, entry
|
|
|
231
282
|
};
|
|
232
283
|
}
|
|
233
284
|
/**
|
|
285
|
+
* One entry from the Data Plane by slug. A 404 resolves `not-found` before `json()`. Any other
|
|
286
|
+
* non-OK status, a non-JSON body, or a non-object body throws, and `readPublishedItem` decides
|
|
287
|
+
* between the stored blob and `unavailable`. A 200 object is `found` with no field-schema check.
|
|
288
|
+
* `locale_metadata` is the platform envelope (it carries an inline SVG) and this path has no slicer,
|
|
289
|
+
* so that own property is removed before the blob is returned or retained. No other key is dropped.
|
|
290
|
+
* A list body is not an object, so it throws rather than resolving `not-found`.
|
|
291
|
+
*/
|
|
292
|
+
async function readItemBySlug(account, storeId, locale, contentTypeId, slug) {
|
|
293
|
+
const url = buildDataPlaneSlugUrl(account, storeId, locale, contentTypeId, slug);
|
|
294
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(ENTRY_REQUEST_TIMEOUT_MS) });
|
|
295
|
+
if (response.status === 404) return { status: "not-found" };
|
|
296
|
+
if (!response.ok) throw new Error(`content upstream status ${response.status}`);
|
|
297
|
+
let payload;
|
|
298
|
+
try {
|
|
299
|
+
payload = await response.json();
|
|
300
|
+
} catch {
|
|
301
|
+
throw new Error("content response is not JSON");
|
|
302
|
+
}
|
|
303
|
+
if (!isRecord(payload)) throw new Error("content response is not an object");
|
|
304
|
+
if (Object.hasOwn(payload, "locale_metadata")) delete payload.locale_metadata;
|
|
305
|
+
return {
|
|
306
|
+
status: "found",
|
|
307
|
+
blob: payload
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
234
311
|
* The Draft Mode read: the same composite entry resolved through Control Plane's preview endpoint
|
|
235
312
|
* with a server-only credential, bypassing all caches. The branch comes from a SIGNED preview
|
|
236
313
|
* context cookie whose HMAC is re-verified here against the broker secret — a tampered cookie fails
|
|
@@ -330,6 +407,17 @@ function buildDataPlaneUrl(account, storeId, locale, contentTypeId, entryId) {
|
|
|
330
407
|
entryId
|
|
331
408
|
].map(encodeURIComponent).join("/")}?locale=${encodeURIComponent(locale)}`;
|
|
332
409
|
}
|
|
410
|
+
/** Same origin and `locale` query as `buildDataPlaneUrl`. The slug is one segment: it is not split on `/`. */
|
|
411
|
+
function buildDataPlaneSlugUrl(account, storeId, locale, contentTypeId, slug) {
|
|
412
|
+
return `https://${account}.vtexcommercestable.com.br/api/content-platform/data/${[
|
|
413
|
+
account,
|
|
414
|
+
storeId,
|
|
415
|
+
contentTypeId,
|
|
416
|
+
"entries",
|
|
417
|
+
"slug",
|
|
418
|
+
slug
|
|
419
|
+
].map(encodeURIComponent).join("/")}?locale=${encodeURIComponent(locale)}`;
|
|
420
|
+
}
|
|
333
421
|
/**
|
|
334
422
|
* Slices one element's value out of the composite blob by its `element.id` and validates it against
|
|
335
423
|
* that element's per-component model. Omits only this component on a missing key, a non-object
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-content-runtime.js","names":[],"sources":["../../../src/sdk/content/create-content-runtime.tsx"],"sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { ContentPropertySchema, ContentTypeDefinition } from '@vtex/fs-core';\nimport { cookies, draftMode } from 'next/headers';\nimport { connection } from 'next/server';\nimport { cache, type FC, type ReactNode } from 'react';\n\nexport type ContentRuntimeConfig = {\n account: string;\n storeId: string;\n /** Canonical per-component models, verbatim from the aggregated content catalog, keyed by id. */\n contentTypes: Record<string, ContentTypeDefinition>;\n};\n\n/** The coordinates of one composite read. `locale` arrives per call, so one build serves many. */\nexport type ReadContentSlicesArgs = {\n contentTypeId: string;\n entryId: string;\n locale: string;\n /** `element.id → per-component model id` for every content-backed element in the scope. */\n elements: Record<string, string>;\n};\n\n/**\n * The scope's slices, keyed by `element.id`. A key is absent when its element must be omitted — a\n * missing key, a shape violation, or a whole-entry failure.\n *\n * The value type is unchecked on purpose: the generated page forwards it to a component whose\n * `$content` is `ContentValue<that component's model>`, a type that exists only in the component\n * package, and an index signature cannot satisfy a model's required properties. Stated once here\n * rather than cast at every emitted prop; the read below validates every value against its model.\n */\nexport type ContentSlices = {\n // biome-ignore lint/suspicious/noExplicitAny: the model's type lives in the component package, not here.\n [elementId: string]: any;\n};\n\nexport type ContentEntryBoundaryProps = {\n /** This element's slice, read at the top of the scope. `undefined` omits the component. */\n value: unknown;\n children: ReactNode;\n};\n\nexport type ContentRuntime = {\n readContentSlices: (args: ReadContentSlicesArgs) => Promise<ContentSlices>;\n ContentEntryBoundary: FC<ContentEntryBoundaryProps>;\n};\n\n/** The signed preview context the Draft Mode route stores; re-verified here before use. */\ntype PreviewClaims = {\n account: string;\n storeId: string;\n branch: string;\n /** Millisecond epoch the token was minted / expires; a captured cookie stops working past `exp`. */\n iat: number;\n exp: number;\n};\n\n/**\n * Internal result taxonomy: a read either finds a validated composite blob or fails for a definite,\n * loggable reason. A transient upstream failure resolves `found` from the last-good store below when\n * that store holds the entry, and degrades to `unavailable` — omitting the affected components —\n * only when it does not.\n */\nexport type ContentEntryResult =\n | { status: 'found'; blob: Record<string, unknown> }\n | { status: 'not-found' }\n | { status: 'unavailable'; reason: string }\n | { status: 'invalid'; reason: string };\n\n/**\n * One published read's outcome. `staleReason` is present only when the fresh read failed and the\n * store covered it, and the result is then an ordinary `found`: no consumer has a fifth renderable\n * status to learn, and only the log line tells a served blob from a fresh one.\n */\ntype PublishedRead = { result: ContentEntryResult; staleReason?: string };\n\nconst ENTRY_REQUEST_TIMEOUT_MS = 5000;\n\n/**\n * Retention ceilings for the last-good store, both estimated rather than measured: no production\n * store exists yet to size a real composite against, and the example schemas in this repo are demo\n * content, not authored pages, so neither is evidence of real scale.\n *\n * `MAX_RETAINED_BLOB_BYTES` bounds one entry. Ordinary editorial text (a title, a body, an image URL,\n * an alt text) stays in the hundreds of bytes to a few kilobytes per element regardless of how many\n * elements a page composes — even the largest composed layout in this repo (103 elements) does not\n * read as megabytes if every element stays plain text. The realistic way to exceed this is a single\n * field holding embedded binary — a base64 image pasted into a text field instead of uploaded through\n * the media widget — which is exactly the shape this guards against, independent of element count.\n *\n * `MAX_RETAINED_TOTAL_BYTES` bounds the whole store, and is the one that actually matters: the\n * per-blob cap alone does not bound total memory, because it multiplies by key count, and key count is\n * not small — a large store's pages and layouts, times its locales, is plausibly a few hundred keys.\n * A few hundred keys at a few megabytes each already exceeds a typical serverless heap (512 MB–1 GB)\n * on the per-blob cap alone; ~64 MB is a small slice of that heap while still holding many ordinary\n * pages at once.\n *\n * Both numbers count serialized JSON text (`blobByteLength`), not retained heap: a parsed object\n * typically costs more than its JSON text — V8 pays per-property and per-string object overhead that\n * the text itself does not — so the real footprint at either ceiling is larger than the number on the\n * tin, plausibly by a small multiple. Both ceilings still leave headroom against a 512 MB–1 GB heap at\n * that multiple; sizing them as retained heap instead would need a runtime memory measurement this\n * module has no cheap way to take.\n *\n * Both numbers are round and generous on purpose, not derived. `content.retention.skipped` below is\n * the real calibration path: once a production store exists, its log volume says whether either\n * ceiling needs to move.\n */\nconst MAX_RETAINED_BLOB_BYTES = 1_000_000;\nconst MAX_RETAINED_TOTAL_BYTES = 64_000_000;\n\n/** Server-only preview credential, supplied at deployment time; never reaches the browser. */\nconst PREVIEW_TOKEN_ENV = 'CONTENT_PLATFORM_PREVIEW_TOKEN';\n/** Shared broker secret used to verify the signed preview context cookie. */\nconst PREVIEW_SECRET_ENV = 'CONTENT_PREVIEW_SECRET';\n/** The signed preview context cookie the Draft Mode route sets. */\nconst PREVIEW_COOKIE = '__fs_content_preview';\n\n/**\n * Creates the one server-side content read surface for a store: a `React.cache`-deduped\n * `readContentSlices` the generated page or layout awaits once at the top, and the\n * `ContentEntryBoundary` the compiler wraps around each content-backed element. One read per scope\n * yields every element's `$content` value keyed by `element.id`, so a page issues at most two Data\n * Plane reads (its own entry plus its layout's) regardless of content-backed component count. Draft\n * Mode switches the same read to Control Plane's preview endpoint through a server-only credential.\n */\nexport function createContentRuntime(config: ContentRuntimeConfig): ContentRuntime {\n // The blob of the most recent successful published read, per entry, so a failing Data Plane\n // renders the last content that worked instead of holes. Not `React.cache`, whose lifetime is the\n // request and so shorter than the failure it has to cover, and not module scope, which every\n // runtime in the process would share — two of them on the same coordinates would then serve each\n // other's content. The generated consumer holds the runtime in a module-scope `const`, so the\n // closure lives as long as the process either way.\n //\n // `contentTypeId` and `entryId` are compiler-emitted literals for the life of the deployment (a\n // dynamic route gets one entry, not one per slug), so today's key domain is small: pages plus\n // layouts, times locales. That is an invariant of the generated caller, not of this function —\n // nothing here stops a future one from passing a request-derived value instead, and the moment it\n // does, this map grows with traffic rather than with the store's page count.\n const lastGood = new Map<string, Record<string, unknown>>();\n let retainedBytes = 0;\n\n /**\n * Whether a successful read's blob replaces what `key` already holds, weighed against both\n * ceilings. Skipping never deletes: whatever `key` held before, if anything, is left exactly as it\n * was — only the update is declined, so a rare oversized read cannot erase a smaller last-good value\n * that a future failure could still use.\n */\n function retainBlob(\n key: string,\n blob: Record<string, unknown>,\n fields: { contentTypeId: string; entryId: string; locale: string },\n ): void {\n const size = blobByteLength(blob);\n if (size > MAX_RETAINED_BLOB_BYTES) {\n logEvent('warn', 'content.retention.skipped', { ...fields, bytes: size, reason: 'blob-too-large' });\n return;\n }\n const existing = lastGood.get(key);\n const existingSize = existing ? blobByteLength(existing) : 0;\n if (retainedBytes - existingSize + size > MAX_RETAINED_TOTAL_BYTES) {\n logEvent('warn', 'content.retention.skipped', { ...fields, bytes: size, reason: 'total-cap-reached' });\n return;\n }\n lastGood.set(key, blob);\n retainedBytes += size - existingSize;\n }\n\n /**\n * The published read plus the fallback that covers it. The upstream is always called and the store\n * is read only in the `catch`, which is what makes this a fallback and not a cache: a publish is\n * visible on the very next request. A success ordinarily overwrites the entry (see `retainBlob` for\n * the two cases where it does not), a `not-found` leaves it alone so unpublishing still unpublishes,\n * and nothing is ever removed or expired by age.\n *\n * Failure is structural — whatever leaves `readPublishedEntry` as a throw — rather than a status\n * table, so a 403 or a 429 serves the stored blob, and a failure mode added there later is covered\n * without touching this.\n */\n async function readPublished(contentTypeId: string, entryId: string, locale: string): Promise<PublishedRead> {\n // NUL appears in no content-type id, entry id, or BCP-47 tag, so two distinct triples cannot\n // collide. Keyed on the same three values the memoisation below uses, so the two layers never\n // disagree about what \"the same read\" is.\n const key = `${contentTypeId}\\u0000${entryId}\\u0000${locale}`;\n try {\n const result = await readPublishedEntry(config.account, config.storeId, locale, contentTypeId, entryId);\n if (result.status === 'found') {\n retainBlob(key, result.blob, { contentTypeId, entryId, locale });\n }\n return { result };\n } catch (error) {\n const reason = transientReason(error);\n const stored = lastGood.get(key);\n // Nothing stored: omit the affected components while the rest of the route renders.\n if (stored === undefined) return { result: { status: 'unavailable', reason } };\n return { result: { status: 'found', blob: stored }, staleReason: reason };\n }\n }\n\n // React.cache collapses same-entry awaits within one request: N boundaries reading the same\n // composite entry share one upstream read and one read-log line. That is per-request memoisation\n // only — nothing is reused across requests — so it is `readPublished` above, not this, that\n // carries a value across the failure it covers. Either way a read never throws a route error.\n const read = cache(async (contentTypeId: string, entryId: string, locale: string): Promise<ContentEntryResult> => {\n // Content is read per request, never at build time. Nothing else on a content-only page is\n // request-dependent — `draftMode()` prerenders as disabled and an uncached `fetch` does not bail\n // out on its own — so without this the entry is captured by a static prerender and the store\n // stops tracking the CMS until its next deploy. `'use cache'` used to hold the route open with a\n // five-minute revalidate; removing it for a platform that does not stream took that with it.\n // Outside the fallback's try, which lives in `readPublished`, so the bail-out it throws during\n // static generation is never swallowed as a transient failure.\n await connection();\n const { isEnabled: draft } = await draftMode();\n let result: ContentEntryResult;\n let staleReason: string | undefined;\n if (draft) {\n // The preview branch neither reads the store nor writes it: an unpublished blob must not\n // outlive the request that asked for it, and a draft must never cover a published failure.\n result = await readPreviewEntry(config, contentTypeId, entryId, locale);\n } else {\n ({ result, staleReason } = await readPublished(contentTypeId, entryId, locale));\n }\n // A served blob is `found`, so the status alone would report it as a success.\n const stale = staleReason !== undefined;\n const { level, event } = stale ? STALE_READ_EVENT : READ_EVENTS[result.status];\n const reason =\n staleReason ?? (result.status === 'unavailable' || result.status === 'invalid' ? result.reason : undefined);\n // The success line is the cheapest evidence of the ≤2-reads-per-page guarantee, and its only\n // cost is production volume: nothing caches this read, so it would emit once or twice per page\n // view and bury the rare failure line. Suppressed there, kept everywhere the count is checked —\n // and the gate covers a genuine success only, so a covered outage stays audible in production.\n const suppressed = !stale && result.status === 'found' && process.env.NODE_ENV === 'production';\n if (!suppressed) {\n logEvent(level, event, {\n contentTypeId,\n entryId,\n locale,\n source: draft ? 'control-plane' : 'data-plane',\n ...(reason === undefined ? {} : { reason }),\n });\n }\n return result;\n });\n\n /**\n * The scope's one read, awaited before render: the composite entry, sliced into one `$content`\n * value per content-backed element. The values reach the components as literal props, so they are\n * part of the server render by construction — a `cloneElement` from a client-side provider lands\n * only on hydration, which is what made server HTML and client DOM disagree.\n *\n * The public shape takes one object, so a call site cannot silently swap two same-typed positional\n * arguments; `React.cache` keys on the positional ones underneath.\n */\n async function readContentSlices({\n contentTypeId,\n entryId,\n locale,\n elements,\n }: ReadContentSlicesArgs): Promise<ContentSlices> {\n const result = await read(contentTypeId, entryId, locale);\n if (result.status !== 'found') {\n // A whole-entry failure (404, invalid, or an upstream failure with nothing stored for the\n // entry) omits every component sharing it: no key is present, so every boundary renders\n // nothing. A failure the last-good store covered never arrives here — it arrives as `found`.\n return emptySlices();\n }\n const slices = emptySlices();\n const requested = Object.entries(elements);\n const omissions: ElementOmission[] = [];\n for (const [elementId, modelId] of requested) {\n const slice = sliceContent(result.blob, elementId, ownValue(config.contentTypes, modelId));\n if (slice.status === 'omitted') {\n // A missing or invalid key within an otherwise-valid entry omits only this one component.\n omissions.push({ elementId, modelId, reason: slice.reason });\n continue;\n }\n slices[elementId] = slice.content;\n }\n if (omissions.length > 0) {\n // One line per call rather than one per element: the operational fact is the ratio, and\n // `omitted === total` is what tells a blank scope from a partial one. The level says only that\n // the page rendered, so it stays `warn` at every ratio.\n logEvent('warn', 'content.elements.omitted', {\n contentTypeId,\n entryId,\n locale,\n omitted: omissions.length,\n total: requested.length,\n omissions,\n });\n }\n return slices;\n }\n\n return { readContentSlices, ContentEntryBoundary };\n}\n\n/**\n * The slice map, with no prototype. An element id is a schema string, so `__proto__` is a name the\n * map can be asked for; on a plain object the lookup would inherit `Object.prototype` and a boundary\n * would render an element whose `$content` is that object rather than omitting it. The failure path\n * returns this too — an entry that 404s must answer `undefined` for every key, including that one.\n */\nfunction emptySlices(): ContentSlices {\n return Object.create(null) as ContentSlices;\n}\n\n/**\n * Renders its element only when that element's slice is present — the omission rule the content RFC\n * states. It no longer delivers the value (the page passes `$content` as a literal prop), so it\n * clones nothing and what wraps it no longer decides whether content arrives.\n *\n * Module scope, not a closure over `createContentRuntime`: it reads nothing from the runtime's\n * configuration, so every store shares one component identity.\n */\nfunction ContentEntryBoundary({ value, children }: ContentEntryBoundaryProps): ReactNode {\n return value === undefined ? null : children;\n}\n\n/**\n * The published read: one composite entry from the Data Plane, live on every request. A definitive\n * 404 resolves a `not-found` sentinel; a transient failure (network, timeout, 5xx, malformed body)\n * throws, and `readPublished` decides between the stored blob and an omit.\n *\n * Nothing caches it. This read sat under `'use cache'` until Cache Components was removed for a\n * platform that does not stream, and the five-minute `cacheLife` and 24-hour `expire` went with it —\n * a transient failure used to throw inside the cache and Next kept serving the last-good entry for\n * up to a day. What replaced that is the last-good store, which is a fallback and not a cache: it\n * never shortens or skips this read, so the happy path is unchanged and a publish lands on the very\n * next request. The page cache, the two cache tags this read emitted and the tag-based revalidation\n * route they fed are still gone, and still owed by whatever restores caching.\n */\nasync function readPublishedEntry(\n account: string,\n storeId: string,\n locale: string,\n contentTypeId: string,\n entryId: string,\n): Promise<ContentEntryResult> {\n const url = buildDataPlaneUrl(account, storeId, locale, contentTypeId, entryId);\n const response = await fetch(url, { signal: AbortSignal.timeout(ENTRY_REQUEST_TIMEOUT_MS) });\n\n if (response.status === 404) return { status: 'not-found' };\n if (!response.ok) throw new Error(`content upstream status ${response.status}`);\n\n let payload: unknown;\n try {\n payload = await response.json();\n } catch {\n throw new Error('content response is not JSON');\n }\n if (!isRecord(payload)) throw new Error('content response is not an object');\n\n return { status: 'found', blob: payload };\n}\n\n/**\n * The Draft Mode read: the same composite entry resolved through Control Plane's preview endpoint\n * with a server-only credential, bypassing all caches. The branch comes from a SIGNED preview\n * context cookie whose HMAC is re-verified here against the broker secret — a tampered cookie fails\n * closed, so a valid session cannot read an unauthorized branch's drafts. The credential never\n * enters browser code, logs, URLs, or props. Uncached, so its sentinels are never persisted.\n */\nasync function readPreviewEntry(\n config: ContentRuntimeConfig,\n contentTypeId: string,\n entryId: string,\n locale: string,\n): Promise<ContentEntryResult> {\n const token = process.env[PREVIEW_TOKEN_ENV];\n if (!token) return { status: 'unavailable', reason: 'preview credential unavailable' };\n const secret = process.env[PREVIEW_SECRET_ENV];\n if (!secret) return { status: 'unavailable', reason: 'preview secret unavailable' };\n\n const store = await cookies();\n const raw = store.get(PREVIEW_COOKIE)?.value;\n const claims = raw ? verifyPreviewContext(raw, secret) : null;\n if (!claims || claims.account !== config.account || claims.storeId !== config.storeId || !claims.branch) {\n return { status: 'unavailable', reason: 'invalid preview context' };\n }\n\n const { account, storeId } = config;\n const path = [account, storeId, contentTypeId, 'entries', 'preview', entryId].map(encodeURIComponent).join('/');\n const url = `https://${account}.vtexcommercestable.com.br/api/content-platform/manage/${path}?branchId=${encodeURIComponent(claims.branch)}&locale=${encodeURIComponent(locale)}`;\n return fetchPreview(url, token);\n}\n\nasync function fetchPreview(url: string, token: string): Promise<ContentEntryResult> {\n let response: Response;\n try {\n response = await fetch(url, {\n cache: 'no-store',\n headers: { Authorization: `Bearer ${token}` },\n signal: AbortSignal.timeout(ENTRY_REQUEST_TIMEOUT_MS),\n });\n } catch (error) {\n return { status: 'unavailable', reason: isTimeoutError(error) ? 'timeout' : 'network error' };\n }\n if (response.status === 404) return { status: 'not-found' };\n if (!response.ok) return { status: 'unavailable', reason: `upstream status ${response.status}` };\n let payload: unknown;\n try {\n payload = await response.json();\n } catch {\n return { status: 'invalid', reason: 'response body is not JSON' };\n }\n if (!isRecord(payload)) return { status: 'invalid', reason: 'response body is not an object' };\n return { status: 'found', blob: payload };\n}\n\n/**\n * Verifies a signed preview context cookie (`base64url(claims).base64url(hmac)`) against the broker\n * secret with a constant-time comparison, returning the claims only when the signature holds and the\n * token has not expired — a captured cookie fails closed once past `exp`.\n */\nfunction verifyPreviewContext(raw: string, secret: string): PreviewClaims | null {\n const [body, signature] = raw.split('.');\n if (!body || !signature) return null;\n const expected = createHmac('sha256', secret).update(body).digest('base64url');\n const given = Buffer.from(signature, 'utf8');\n const want = Buffer.from(expected, 'utf8');\n if (given.length !== want.length || !timingSafeEqual(given, want)) return null;\n try {\n const claims = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as PreviewClaims;\n if (typeof claims.branch !== 'string' || !Number.isFinite(claims.exp) || Date.now() >= claims.exp) {\n return null;\n }\n return claims;\n } catch {\n return null;\n }\n}\n\nfunction buildDataPlaneUrl(\n account: string,\n storeId: string,\n locale: string,\n contentTypeId: string,\n entryId: string,\n): string {\n const path = [account, storeId, contentTypeId, 'entries', entryId].map(encodeURIComponent).join('/');\n return `https://${account}.vtexcommercestable.com.br/api/content-platform/data/${path}?locale=${encodeURIComponent(locale)}`;\n}\n\ntype ContentSliceResult =\n | { status: 'sliced'; content: Record<string, unknown> }\n | { status: 'omitted'; reason: string };\n\n/**\n * Slices one element's value out of the composite blob by its `element.id` and validates it against\n * that element's per-component model. Omits only this component on a missing key, a non-object\n * value, an unknown model, or a shape violation — keyword constraints are the preflight's job.\n * Failing closed on an unknown model keeps an unvalidated blob from reaching `$content` (which would\n * crash the route as a React child).\n */\nfunction sliceContent(\n blob: Record<string, unknown>,\n elementId: string,\n model: ContentTypeDefinition | undefined,\n): ContentSliceResult {\n const raw = ownValue(blob, elementId);\n if (!isRecord(raw)) return { status: 'omitted', reason: 'missing or non-object value' };\n if (!model) return { status: 'omitted', reason: 'unknown model' };\n\n const content = projectObject(model.schema.properties, raw);\n const violation = findShapeViolation(model, content);\n if (violation !== null) return { status: 'omitted', reason: violation };\n return { status: 'sliced', content };\n}\n\n/** A closed object shape, at the root or one level down. */\ntype ClosedObjectShape = {\n required?: readonly string[];\n properties: Record<string, ContentPropertySchema>;\n};\n\n/**\n * Keeps declared keys only, at every level the vocabulary admits, so the injected value matches the\n * `ContentValue` type the component was compiled against. A value of the wrong shape passes through\n * untouched for the validator below to reject.\n */\nfunction projectObject(\n properties: Record<string, ContentPropertySchema>,\n raw: Record<string, unknown>,\n): Record<string, unknown> {\n const projected: Record<string, unknown> = {};\n for (const [key, property] of Object.entries(properties)) {\n const value = ownValue(raw, key);\n if (value === undefined) continue;\n if (property.type === 'array') {\n projected[key] = Array.isArray(value)\n ? value.map((member) => (isRecord(member) ? projectObject(property.items.properties, member) : member))\n : value;\n } else if (property.type === 'object') {\n projected[key] = isRecord(value) ? projectObject(property.properties, value) : value;\n } else {\n projected[key] = value;\n }\n }\n return projected;\n}\n\n/** Shape-level validation only (required presence, primitive type, enum membership), at every level. */\nfunction findShapeViolation(model: ContentTypeDefinition, content: Record<string, unknown>): string | null {\n return findObjectViolation('', model.schema, content);\n}\n\nfunction findObjectViolation(path: string, shape: ClosedObjectShape, value: Record<string, unknown>): string | null {\n for (const key of shape.required ?? []) {\n if (ownValue(value, key) === undefined) return `missing required property \"${joinPath(path, key)}\"`;\n }\n for (const [key, property] of Object.entries(shape.properties)) {\n const member = ownValue(value, key);\n if (member === undefined) continue;\n const violation = findPropertyViolation(joinPath(path, key), property, member);\n if (violation !== null) return violation;\n }\n return null;\n}\n\nfunction findPropertyViolation(path: string, property: ContentPropertySchema, value: unknown): string | null {\n if (property.type === 'array') {\n if (!Array.isArray(value)) return `property \"${path}\" does not match type \"array\"`;\n for (const [index, member] of value.entries()) {\n const itemPath = `${path}[${index}]`;\n if (!isRecord(member)) return `property \"${itemPath}\" does not match type \"object\"`;\n const violation = findObjectViolation(itemPath, property.items, member);\n if (violation !== null) return violation;\n }\n // minItems and maxItems are author-time bounds; the read path does not enforce them.\n return null;\n }\n if (property.type === 'object') {\n if (!isRecord(value)) return `property \"${path}\" does not match type \"object\"`;\n return findObjectViolation(path, property, value);\n }\n if (!matchesScalarType(property.type, value)) return `property \"${path}\" does not match type \"${property.type}\"`;\n if (property.enum && !property.enum.includes(value as string | number)) {\n return `property \"${path}\" is not an allowed enum value`;\n }\n return null;\n}\n\nfunction joinPath(path: string, key: string): string {\n return path.length === 0 ? key : `${path}.${key}`;\n}\n\nfunction matchesScalarType(type: 'string' | 'number' | 'integer' | 'boolean', value: unknown): boolean {\n switch (type) {\n case 'string':\n return typeof value === 'string';\n case 'boolean':\n return typeof value === 'boolean';\n case 'number':\n return typeof value === 'number' && Number.isFinite(value);\n case 'integer':\n return typeof value === 'number' && Number.isInteger(value);\n }\n}\n\ntype LogLevel = 'error' | 'warn' | 'info';\n\n/** The closed vocabulary a log consumer discriminates on. Nothing else is ever emitted. */\ntype ContentEvent =\n | 'content.read.ok'\n | 'content.read.stale'\n | 'content.read.missing'\n | 'content.read.invalid'\n | 'content.read.failed'\n | 'content.elements.omitted'\n | 'content.retention.skipped';\n\n/** One omitted element, as the aggregate line reports it. `reason` is a path, never a value. */\ntype ElementOmission = { elementId: string; modelId: string; reason: string };\n\n/**\n * Severity as a function of the read's status, and nothing else: `unavailable` means the runtime\n * could not read, which is the on-call signal; `not-found` and `invalid` mean the content is broken\n * while the runtime works. `source` is a payload field, so a preview read that cannot resolve is a\n * failure like any other. `invalid` is reachable from the preview branch only — the published path\n * throws on a bad body, which is why the store covers that case.\n */\nconst READ_EVENTS: Record<ContentEntryResult['status'], { level: LogLevel; event: ContentEvent }> = {\n found: { level: 'info', event: 'content.read.ok' },\n 'not-found': { level: 'warn', event: 'content.read.missing' },\n invalid: { level: 'warn', event: 'content.read.invalid' },\n unavailable: { level: 'error', event: 'content.read.failed' },\n};\n\n/**\n * The one line the status does not decide: the read failed and the stored blob covered it. `warn`\n * because the page rendered, which keeps `error` meaning exactly \"the shopper is seeing holes\". A\n * covered outage never escalates on its own, so alerting on one means filtering this event name.\n */\nconst STALE_READ_EVENT: { level: LogLevel; event: ContentEvent } = { level: 'warn', event: 'content.read.stale' };\n\n/**\n * The runtime's one log line: a constant message plus a payload whose `event` is the sole event\n * discriminator, so rewording prose cannot break a consumer. Nothing here ever carries a content\n * value, a preview token, or the broker secret.\n *\n * `entryId` is high-cardinality and stays log-only. Feeding these events to a metrics pipeline —\n * which no part of this runtime has today — means dropping or hashing it at that boundary; the six\n * event names are the bounded dimension set such a pipeline would inherit.\n */\nfunction logEvent(level: LogLevel, event: ContentEvent, fields: Record<string, unknown>): void {\n // `event` spreads last so a `fields` key of the same name cannot displace the discriminator every\n // consumer keys on. `fields` is a `Record<string, unknown>`, so nothing in the types would object.\n console[level]('[content-runtime]', { ...fields, event });\n}\n\nfunction transientReason(error: unknown): string {\n if (isTimeoutError(error)) return 'timeout';\n return error instanceof Error ? error.message : 'network error';\n}\n\n/**\n * The exact UTF-8 byte count of the serialized blob, not the JS string's UTF-16 code-unit count\n * (`.length`). Non-ASCII text — accented Latin, common in Portuguese-language content, at 2 bytes per\n * code unit, or CJK at 3 — makes those two numbers diverge, and a check against `.length` would let a\n * blob past both ceilings while genuinely over them in bytes.\n */\nfunction blobByteLength(blob: Record<string, unknown>): number {\n return Buffer.byteLength(JSON.stringify(blob), 'utf8');\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Own-property read. Every object keyed by an authored id here comes from `JSON.parse` or a plain\n * literal, so it carries `Object.prototype`: a key named `__proto__` or `constructor` would resolve\n * to something inherited instead of to `undefined`, and the absent-key branch would never be taken.\n */\nfunction ownValue<T>(source: Record<string, T>, key: string): T | undefined {\n return Object.hasOwn(source, key) ? source[key] : undefined;\n}\n\n/** Property check instead of `instanceof`: `AbortSignal.timeout` rejects with a `DOMException`. */\nfunction isTimeoutError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return false;\n const { name } = error as { name?: unknown };\n return name === 'TimeoutError' || name === 'AbortError';\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-content-runtime.js","names":[],"sources":["../../../src/sdk/content/create-content-runtime.tsx"],"sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { ContentPropertySchema, ContentTypeDefinition } from '@vtex/fs-core';\nimport { cookies, draftMode } from 'next/headers';\nimport { connection } from 'next/server';\nimport { cache, type FC, type ReactNode } from 'react';\n\nexport type ContentRuntimeConfig = {\n account: string;\n storeId: string;\n /** Canonical per-component models, verbatim from the aggregated content catalog, keyed by id. */\n contentTypes: Record<string, ContentTypeDefinition>;\n};\n\n/** The coordinates of one composite read. `locale` arrives per call, so one build serves many. */\nexport type ReadContentSlicesArgs = {\n contentTypeId: string;\n entryId: string;\n locale: string;\n /** `element.id → per-component model id` for every content-backed element in the scope. */\n elements: Record<string, string>;\n};\n\n/**\n * The scope's slices, keyed by `element.id`. A key is absent when its element must be omitted — a\n * missing key, a shape violation, or a whole-entry failure.\n *\n * The value type is unchecked on purpose: the generated page forwards it to a component whose\n * `$content` is `ContentValue<that component's model>`, a type that exists only in the component\n * package, and an index signature cannot satisfy a model's required properties. Stated once here\n * rather than cast at every emitted prop; the read below validates every value against its model.\n */\nexport type ContentSlices = {\n // biome-ignore lint/suspicious/noExplicitAny: the model's type lives in the component package, not here.\n [elementId: string]: any;\n};\n\nexport type ContentEntryBoundaryProps = {\n /** This element's slice, read at the top of the scope. `undefined` omits the component. */\n value: unknown;\n children: ReactNode;\n};\n\n/** The coordinates of one item read. Object arguments, so two strings cannot be swapped at the call site. */\nexport type ReadContentItemArgs = {\n contentTypeId: string;\n slug: string;\n locale: string;\n};\n\n/**\n * One item read. `found` is the resolved entry. `not-found` is HTTP 404 only. `unavailable` is every\n * other failure with nothing stored. There is no `invalid` arm: a 200 JSON object is the blob.\n */\nexport type ContentItemResult =\n | { status: 'found'; blob: Record<string, unknown> }\n | { status: 'not-found' }\n | { status: 'unavailable'; reason: string };\n\nexport type ContentRuntime = {\n readContentSlices: (args: ReadContentSlicesArgs) => Promise<ContentSlices>;\n readContentItem: (args: ReadContentItemArgs) => Promise<ContentItemResult>;\n ContentEntryBoundary: FC<ContentEntryBoundaryProps>;\n};\n\n/** The signed preview context the Draft Mode route stores; re-verified here before use. */\ntype PreviewClaims = {\n account: string;\n storeId: string;\n branch: string;\n /** Millisecond epoch the token was minted / expires; a captured cookie stops working past `exp`. */\n iat: number;\n exp: number;\n};\n\n/**\n * Internal result taxonomy: a read either finds a validated composite blob or fails for a definite,\n * loggable reason. A transient upstream failure resolves `found` from the last-good store below when\n * that store holds the entry, and degrades to `unavailable` — omitting the affected components —\n * only when it does not.\n */\nexport type ContentEntryResult =\n | { status: 'found'; blob: Record<string, unknown> }\n | { status: 'not-found' }\n | { status: 'unavailable'; reason: string }\n | { status: 'invalid'; reason: string };\n\n/**\n * One published read's outcome. `staleReason` is present only when the fresh read failed and the\n * store covered it, and the result is then an ordinary `found`: no consumer has a fifth renderable\n * status to learn, and only the log line tells a served blob from a fresh one.\n */\ntype PublishedRead = { result: ContentEntryResult; staleReason?: string };\n\n/** Same shape as `PublishedRead`, on the item taxonomy (no `invalid` arm). */\ntype PublishedItemRead = { result: ContentItemResult; staleReason?: string };\n\nconst ENTRY_REQUEST_TIMEOUT_MS = 5000;\n\n/**\n * Retention ceilings for the last-good store, both estimated rather than measured: no production\n * store exists yet to size a real composite against, and the example schemas in this repo are demo\n * content, not authored pages, so neither is evidence of real scale.\n *\n * `MAX_RETAINED_BLOB_BYTES` bounds one entry. Ordinary editorial text (a title, a body, an image URL,\n * an alt text) stays in the hundreds of bytes to a few kilobytes per element regardless of how many\n * elements a page composes — even the largest composed layout in this repo (103 elements) does not\n * read as megabytes if every element stays plain text. The realistic way to exceed this is a single\n * field holding embedded binary — a base64 image pasted into a text field instead of uploaded through\n * the media widget — which is exactly the shape this guards against, independent of element count.\n *\n * `MAX_RETAINED_TOTAL_BYTES` bounds the whole store, and is the one that actually matters: the\n * per-blob cap alone does not bound total memory, because it multiplies by key count, and key count is\n * not small — a large store's pages and layouts, times its locales, is plausibly a few hundred keys.\n * A few hundred keys at a few megabytes each already exceeds a typical serverless heap (512 MB–1 GB)\n * on the per-blob cap alone; ~64 MB is a small slice of that heap while still holding many ordinary\n * pages at once.\n *\n * Both numbers count serialized JSON text (`blobByteLength`), not retained heap: a parsed object\n * typically costs more than its JSON text — V8 pays per-property and per-string object overhead that\n * the text itself does not — so the real footprint at either ceiling is larger than the number on the\n * tin, plausibly by a small multiple. Both ceilings still leave headroom against a 512 MB–1 GB heap at\n * that multiple; sizing them as retained heap instead would need a runtime memory measurement this\n * module has no cheap way to take.\n *\n * Both numbers are round and generous on purpose, not derived. `content.retention.skipped` below is\n * the real calibration path: once a production store exists, its log volume says whether either\n * ceiling needs to move.\n */\nconst MAX_RETAINED_BLOB_BYTES = 1_000_000;\nconst MAX_RETAINED_TOTAL_BYTES = 64_000_000;\n\n/** Server-only preview credential, supplied at deployment time; never reaches the browser. */\nconst PREVIEW_TOKEN_ENV = 'CONTENT_PLATFORM_PREVIEW_TOKEN';\n/** Shared broker secret used to verify the signed preview context cookie. */\nconst PREVIEW_SECRET_ENV = 'CONTENT_PREVIEW_SECRET';\n/** The signed preview context cookie the Draft Mode route sets. */\nconst PREVIEW_COOKIE = '__fs_content_preview';\n\n/**\n * Creates the one server-side content read surface for a store: a `React.cache`-deduped\n * `readContentSlices` the generated page or layout awaits once at the top, and the\n * `ContentEntryBoundary` the compiler wraps around each content-backed element. One read per scope\n * yields every element's `$content` value keyed by `element.id`, so a page issues at most two Data\n * Plane reads (its own entry plus its layout's) regardless of content-backed component count. Draft\n * Mode switches that composite read to Control Plane's preview endpoint through a server-only\n * credential. `readContentItem` reads one entry by slug and does not consult Draft Mode.\n */\nexport function createContentRuntime(config: ContentRuntimeConfig): ContentRuntime {\n // The blob of the most recent successful published read, per entry, so a failing Data Plane\n // renders the last content that worked instead of holes. Not `React.cache`, whose lifetime is the\n // request and so shorter than the failure it has to cover, and not module scope, which every\n // runtime in the process would share — two of them on the same coordinates would then serve each\n // other's content. The generated consumer holds the runtime in a module-scope `const`, so the\n // closure lives as long as the process either way.\n //\n // `contentTypeId` and `entryId` are compiler-emitted literals for the life of the deployment (a\n // dynamic route gets one entry, not one per slug), so today's key domain is small: pages plus\n // layouts, times locales. That is an invariant of the generated caller, not of this function —\n // nothing here stops a future one from passing a request-derived value instead, and the moment it\n // does, this map grows with traffic rather than with the store's page count.\n //\n // Item reads share this map and both ceilings. Their key starts with `item` and a NUL, then the\n // content type, slug, and locale, so it cannot equal a composite key even when the slug equals an\n // entry id. A slug is request-derived, so that part of the map grows with distinct items.\n const lastGood = new Map<string, Record<string, unknown>>();\n let retainedBytes = 0;\n\n type RetainFields =\n | { contentTypeId: string; entryId: string; locale: string }\n | { contentTypeId: string; slug: string; locale: string };\n\n /**\n * Whether a successful read's blob replaces what `key` already holds, weighed against both\n * ceilings. Skipping never deletes: whatever `key` held before, if anything, is left exactly as it\n * was — only the update is declined, so a rare oversized read cannot erase a smaller last-good value\n * that a future failure could still use.\n */\n function retainBlob(key: string, blob: Record<string, unknown>, fields: RetainFields): void {\n const size = blobByteLength(blob);\n if (size > MAX_RETAINED_BLOB_BYTES) {\n logEvent('warn', 'content.retention.skipped', { ...fields, bytes: size, reason: 'blob-too-large' });\n return;\n }\n const existing = lastGood.get(key);\n const existingSize = existing ? blobByteLength(existing) : 0;\n if (retainedBytes - existingSize + size > MAX_RETAINED_TOTAL_BYTES) {\n logEvent('warn', 'content.retention.skipped', { ...fields, bytes: size, reason: 'total-cap-reached' });\n return;\n }\n lastGood.set(key, blob);\n retainedBytes += size - existingSize;\n }\n\n /**\n * The published read plus the fallback that covers it. The upstream is always called and the store\n * is read only in the `catch`, which is what makes this a fallback and not a cache: a publish is\n * visible on the very next request. A success ordinarily overwrites the entry (see `retainBlob` for\n * the two cases where it does not), a `not-found` leaves it alone so unpublishing still unpublishes,\n * and nothing is ever removed or expired by age.\n *\n * Failure is structural — whatever leaves `readPublishedEntry` as a throw — rather than a status\n * table, so a 403 or a 429 serves the stored blob, and a failure mode added there later is covered\n * without touching this.\n */\n async function readPublished(contentTypeId: string, entryId: string, locale: string): Promise<PublishedRead> {\n // NUL appears in no content-type id, entry id, or BCP-47 tag, so two distinct triples cannot\n // collide. Keyed on the same three values the memoisation below uses, so the two layers never\n // disagree about what \"the same read\" is.\n const key = `${contentTypeId}\\u0000${entryId}\\u0000${locale}`;\n try {\n const result = await readPublishedEntry(config.account, config.storeId, locale, contentTypeId, entryId);\n if (result.status === 'found') {\n retainBlob(key, result.blob, { contentTypeId, entryId, locale });\n }\n return { result };\n } catch (error) {\n const reason = transientReason(error);\n const stored = lastGood.get(key);\n // Nothing stored: omit the affected components while the rest of the route renders.\n if (stored === undefined) return { result: { status: 'unavailable', reason } };\n return { result: { status: 'found', blob: stored }, staleReason: reason };\n }\n }\n\n /**\n * The by-slug read plus the same fallback. Not a parameter of `readPublished`: the key, the URL,\n * and the result taxonomy differ, and folding them together would put a slug on the composite log\n * line. A `not-found` returns before the map is read or written.\n */\n async function readPublishedItem(contentTypeId: string, slug: string, locale: string): Promise<PublishedItemRead> {\n // The `item` prefix is what keeps this off `${contentTypeId}\\u0000${entryId}\\u0000${locale}`.\n const key = `item\\u0000${contentTypeId}\\u0000${slug}\\u0000${locale}`;\n try {\n const result = await readItemBySlug(config.account, config.storeId, locale, contentTypeId, slug);\n if (result.status === 'found') {\n retainBlob(key, result.blob, { contentTypeId, slug, locale });\n }\n return { result };\n } catch (error) {\n const reason = transientReason(error);\n const stored = lastGood.get(key);\n if (stored === undefined) return { result: { status: 'unavailable', reason } };\n return { result: { status: 'found', blob: stored }, staleReason: reason };\n }\n }\n\n // React.cache collapses same-entry awaits within one request: N boundaries reading the same\n // composite entry share one upstream read and one read-log line. That is per-request memoisation\n // only — nothing is reused across requests — so it is `readPublished` above, not this, that\n // carries a value across the failure it covers. Either way a read never throws a route error.\n const read = cache(async (contentTypeId: string, entryId: string, locale: string): Promise<ContentEntryResult> => {\n // Content is read per request, never at build time. Nothing else on a content-only page is\n // request-dependent — `draftMode()` prerenders as disabled and an uncached `fetch` does not bail\n // out on its own — so without this the entry is captured by a static prerender and the store\n // stops tracking the CMS until its next deploy. `'use cache'` used to hold the route open with a\n // five-minute revalidate; removing it for a platform that does not stream took that with it.\n // Outside the fallback's try, which lives in `readPublished`, so the bail-out it throws during\n // static generation is never swallowed as a transient failure.\n await connection();\n const { isEnabled: draft } = await draftMode();\n let result: ContentEntryResult;\n let staleReason: string | undefined;\n if (draft) {\n // The preview branch neither reads the store nor writes it: an unpublished blob must not\n // outlive the request that asked for it, and a draft must never cover a published failure.\n result = await readPreviewEntry(config, contentTypeId, entryId, locale);\n } else {\n ({ result, staleReason } = await readPublished(contentTypeId, entryId, locale));\n }\n // A served blob is `found`, so the status alone would report it as a success.\n const stale = staleReason !== undefined;\n const { level, event } = stale ? STALE_READ_EVENT : READ_EVENTS[result.status];\n const reason =\n staleReason ?? (result.status === 'unavailable' || result.status === 'invalid' ? result.reason : undefined);\n // The success line is the cheapest evidence of the ≤2-reads-per-page guarantee, and its only\n // cost is production volume: nothing caches this read, so it would emit once or twice per page\n // view and bury the rare failure line. Suppressed there, kept everywhere the count is checked —\n // and the gate covers a genuine success only, so a covered outage stays audible in production.\n const suppressed = !stale && result.status === 'found' && process.env.NODE_ENV === 'production';\n if (!suppressed) {\n logEvent(level, event, {\n contentTypeId,\n entryId,\n locale,\n source: draft ? 'control-plane' : 'data-plane',\n ...(reason === undefined ? {} : { reason }),\n });\n }\n return result;\n });\n\n /**\n * The scope's one read, awaited before render: the composite entry, sliced into one `$content`\n * value per content-backed element. The values reach the components as literal props, so they are\n * part of the server render by construction — a `cloneElement` from a client-side provider lands\n * only on hydration, which is what made server HTML and client DOM disagree.\n *\n * The public shape takes one object, so a call site cannot silently swap two same-typed positional\n * arguments; `React.cache` keys on the positional ones underneath.\n */\n async function readContentSlices({\n contentTypeId,\n entryId,\n locale,\n elements,\n }: ReadContentSlicesArgs): Promise<ContentSlices> {\n const result = await read(contentTypeId, entryId, locale);\n if (result.status !== 'found') {\n // A whole-entry failure (404, invalid, or an upstream failure with nothing stored for the\n // entry) omits every component sharing it: no key is present, so every boundary renders\n // nothing. A failure the last-good store covered never arrives here — it arrives as `found`.\n return emptySlices();\n }\n const slices = emptySlices();\n const requested = Object.entries(elements);\n const omissions: ElementOmission[] = [];\n for (const [elementId, modelId] of requested) {\n const slice = sliceContent(result.blob, elementId, ownValue(config.contentTypes, modelId));\n if (slice.status === 'omitted') {\n // A missing or invalid key within an otherwise-valid entry omits only this one component.\n omissions.push({ elementId, modelId, reason: slice.reason });\n continue;\n }\n slices[elementId] = slice.content;\n }\n if (omissions.length > 0) {\n // One line per call rather than one per element: the operational fact is the ratio, and\n // `omitted === total` is what tells a blank scope from a partial one. The level says only that\n // the page rendered, so it stays `warn` at every ratio.\n logEvent('warn', 'content.elements.omitted', {\n contentTypeId,\n entryId,\n locale,\n omitted: omissions.length,\n total: requested.length,\n omissions,\n });\n }\n return slices;\n }\n\n // A separate cache from `read`. Positional key is content type, slug, locale. `connection()` is\n // outside `readPublishedItem`'s try, so a prerender bail-out propagates unlogged. Draft Mode is\n // not a branch this read has: there is no slug preview route, and a Data Plane `branch` query is\n // ignored by the platform.\n const readItem = cache(async (contentTypeId: string, slug: string, locale: string): Promise<ContentItemResult> => {\n await connection();\n const { result, staleReason } = await readPublishedItem(contentTypeId, slug, locale);\n const stale = staleReason !== undefined;\n const { level, event } = stale ? STALE_READ_EVENT : READ_EVENTS[result.status];\n const reason = staleReason ?? (result.status === 'unavailable' ? result.reason : undefined);\n const suppressed = !stale && result.status === 'found' && process.env.NODE_ENV === 'production';\n if (!suppressed) {\n logEvent(level, event, {\n contentTypeId,\n slug,\n locale,\n source: 'data-plane',\n ...(reason === undefined ? {} : { reason }),\n });\n }\n return result;\n });\n\n async function readContentItem({ contentTypeId, slug, locale }: ReadContentItemArgs): Promise<ContentItemResult> {\n return readItem(contentTypeId, slug, locale);\n }\n\n return { readContentSlices, readContentItem, ContentEntryBoundary };\n}\n\n/**\n * The slice map, with no prototype. An element id is a schema string, so `__proto__` is a name the\n * map can be asked for; on a plain object the lookup would inherit `Object.prototype` and a boundary\n * would render an element whose `$content` is that object rather than omitting it. The failure path\n * returns this too — an entry that 404s must answer `undefined` for every key, including that one.\n */\nfunction emptySlices(): ContentSlices {\n return Object.create(null) as ContentSlices;\n}\n\n/**\n * Renders its element only when that element's slice is present — the omission rule the content RFC\n * states. It no longer delivers the value (the page passes `$content` as a literal prop), so it\n * clones nothing and what wraps it no longer decides whether content arrives.\n *\n * Module scope, not a closure over `createContentRuntime`: it reads nothing from the runtime's\n * configuration, so every store shares one component identity.\n */\nfunction ContentEntryBoundary({ value, children }: ContentEntryBoundaryProps): ReactNode {\n return value === undefined ? null : children;\n}\n\n/**\n * The published read: one composite entry from the Data Plane, live on every request. A definitive\n * 404 resolves a `not-found` sentinel; a transient failure (network, timeout, 5xx, malformed body)\n * throws, and `readPublished` decides between the stored blob and an omit.\n *\n * Nothing caches it. This read sat under `'use cache'` until Cache Components was removed for a\n * platform that does not stream, and the five-minute `cacheLife` and 24-hour `expire` went with it —\n * a transient failure used to throw inside the cache and Next kept serving the last-good entry for\n * up to a day. What replaced that is the last-good store, which is a fallback and not a cache: it\n * never shortens or skips this read, so the happy path is unchanged and a publish lands on the very\n * next request. The page cache, the two cache tags this read emitted and the tag-based revalidation\n * route they fed are still gone, and still owed by whatever restores caching.\n */\nasync function readPublishedEntry(\n account: string,\n storeId: string,\n locale: string,\n contentTypeId: string,\n entryId: string,\n): Promise<ContentEntryResult> {\n const url = buildDataPlaneUrl(account, storeId, locale, contentTypeId, entryId);\n const response = await fetch(url, { signal: AbortSignal.timeout(ENTRY_REQUEST_TIMEOUT_MS) });\n\n if (response.status === 404) return { status: 'not-found' };\n if (!response.ok) throw new Error(`content upstream status ${response.status}`);\n\n let payload: unknown;\n try {\n payload = await response.json();\n } catch {\n throw new Error('content response is not JSON');\n }\n if (!isRecord(payload)) throw new Error('content response is not an object');\n\n return { status: 'found', blob: payload };\n}\n\n/**\n * One entry from the Data Plane by slug. A 404 resolves `not-found` before `json()`. Any other\n * non-OK status, a non-JSON body, or a non-object body throws, and `readPublishedItem` decides\n * between the stored blob and `unavailable`. A 200 object is `found` with no field-schema check.\n * `locale_metadata` is the platform envelope (it carries an inline SVG) and this path has no slicer,\n * so that own property is removed before the blob is returned or retained. No other key is dropped.\n * A list body is not an object, so it throws rather than resolving `not-found`.\n */\nasync function readItemBySlug(\n account: string,\n storeId: string,\n locale: string,\n contentTypeId: string,\n slug: string,\n): Promise<ContentItemResult> {\n const url = buildDataPlaneSlugUrl(account, storeId, locale, contentTypeId, slug);\n const response = await fetch(url, { signal: AbortSignal.timeout(ENTRY_REQUEST_TIMEOUT_MS) });\n\n if (response.status === 404) return { status: 'not-found' };\n if (!response.ok) throw new Error(`content upstream status ${response.status}`);\n\n let payload: unknown;\n try {\n payload = await response.json();\n } catch {\n throw new Error('content response is not JSON');\n }\n if (!isRecord(payload)) throw new Error('content response is not an object');\n if (Object.hasOwn(payload, 'locale_metadata')) {\n delete payload.locale_metadata;\n }\n\n return { status: 'found', blob: payload };\n}\n\n/**\n * The Draft Mode read: the same composite entry resolved through Control Plane's preview endpoint\n * with a server-only credential, bypassing all caches. The branch comes from a SIGNED preview\n * context cookie whose HMAC is re-verified here against the broker secret — a tampered cookie fails\n * closed, so a valid session cannot read an unauthorized branch's drafts. The credential never\n * enters browser code, logs, URLs, or props. Uncached, so its sentinels are never persisted.\n */\nasync function readPreviewEntry(\n config: ContentRuntimeConfig,\n contentTypeId: string,\n entryId: string,\n locale: string,\n): Promise<ContentEntryResult> {\n const token = process.env[PREVIEW_TOKEN_ENV];\n if (!token) return { status: 'unavailable', reason: 'preview credential unavailable' };\n const secret = process.env[PREVIEW_SECRET_ENV];\n if (!secret) return { status: 'unavailable', reason: 'preview secret unavailable' };\n\n const store = await cookies();\n const raw = store.get(PREVIEW_COOKIE)?.value;\n const claims = raw ? verifyPreviewContext(raw, secret) : null;\n if (!claims || claims.account !== config.account || claims.storeId !== config.storeId || !claims.branch) {\n return { status: 'unavailable', reason: 'invalid preview context' };\n }\n\n const { account, storeId } = config;\n const path = [account, storeId, contentTypeId, 'entries', 'preview', entryId].map(encodeURIComponent).join('/');\n const url = `https://${account}.vtexcommercestable.com.br/api/content-platform/manage/${path}?branchId=${encodeURIComponent(claims.branch)}&locale=${encodeURIComponent(locale)}`;\n return fetchPreview(url, token);\n}\n\nasync function fetchPreview(url: string, token: string): Promise<ContentEntryResult> {\n let response: Response;\n try {\n response = await fetch(url, {\n cache: 'no-store',\n headers: { Authorization: `Bearer ${token}` },\n signal: AbortSignal.timeout(ENTRY_REQUEST_TIMEOUT_MS),\n });\n } catch (error) {\n return { status: 'unavailable', reason: isTimeoutError(error) ? 'timeout' : 'network error' };\n }\n if (response.status === 404) return { status: 'not-found' };\n if (!response.ok) return { status: 'unavailable', reason: `upstream status ${response.status}` };\n let payload: unknown;\n try {\n payload = await response.json();\n } catch {\n return { status: 'invalid', reason: 'response body is not JSON' };\n }\n if (!isRecord(payload)) return { status: 'invalid', reason: 'response body is not an object' };\n return { status: 'found', blob: payload };\n}\n\n/**\n * Verifies a signed preview context cookie (`base64url(claims).base64url(hmac)`) against the broker\n * secret with a constant-time comparison, returning the claims only when the signature holds and the\n * token has not expired — a captured cookie fails closed once past `exp`.\n */\nfunction verifyPreviewContext(raw: string, secret: string): PreviewClaims | null {\n const [body, signature] = raw.split('.');\n if (!body || !signature) return null;\n const expected = createHmac('sha256', secret).update(body).digest('base64url');\n const given = Buffer.from(signature, 'utf8');\n const want = Buffer.from(expected, 'utf8');\n if (given.length !== want.length || !timingSafeEqual(given, want)) return null;\n try {\n const claims = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as PreviewClaims;\n if (typeof claims.branch !== 'string' || !Number.isFinite(claims.exp) || Date.now() >= claims.exp) {\n return null;\n }\n return claims;\n } catch {\n return null;\n }\n}\n\nfunction buildDataPlaneUrl(\n account: string,\n storeId: string,\n locale: string,\n contentTypeId: string,\n entryId: string,\n): string {\n const path = [account, storeId, contentTypeId, 'entries', entryId].map(encodeURIComponent).join('/');\n return `https://${account}.vtexcommercestable.com.br/api/content-platform/data/${path}?locale=${encodeURIComponent(locale)}`;\n}\n\n/** Same origin and `locale` query as `buildDataPlaneUrl`. The slug is one segment: it is not split on `/`. */\nfunction buildDataPlaneSlugUrl(\n account: string,\n storeId: string,\n locale: string,\n contentTypeId: string,\n slug: string,\n): string {\n const path = [account, storeId, contentTypeId, 'entries', 'slug', slug].map(encodeURIComponent).join('/');\n return `https://${account}.vtexcommercestable.com.br/api/content-platform/data/${path}?locale=${encodeURIComponent(locale)}`;\n}\n\ntype ContentSliceResult =\n | { status: 'sliced'; content: Record<string, unknown> }\n | { status: 'omitted'; reason: string };\n\n/**\n * Slices one element's value out of the composite blob by its `element.id` and validates it against\n * that element's per-component model. Omits only this component on a missing key, a non-object\n * value, an unknown model, or a shape violation — keyword constraints are the preflight's job.\n * Failing closed on an unknown model keeps an unvalidated blob from reaching `$content` (which would\n * crash the route as a React child).\n */\nfunction sliceContent(\n blob: Record<string, unknown>,\n elementId: string,\n model: ContentTypeDefinition | undefined,\n): ContentSliceResult {\n const raw = ownValue(blob, elementId);\n if (!isRecord(raw)) return { status: 'omitted', reason: 'missing or non-object value' };\n if (!model) return { status: 'omitted', reason: 'unknown model' };\n\n const content = projectObject(model.schema.properties, raw);\n const violation = findShapeViolation(model, content);\n if (violation !== null) return { status: 'omitted', reason: violation };\n return { status: 'sliced', content };\n}\n\n/** A closed object shape, at the root or one level down. */\ntype ClosedObjectShape = {\n required?: readonly string[];\n properties: Record<string, ContentPropertySchema>;\n};\n\n/**\n * Keeps declared keys only, at every level the vocabulary admits, so the injected value matches the\n * `ContentValue` type the component was compiled against. A value of the wrong shape passes through\n * untouched for the validator below to reject.\n */\nfunction projectObject(\n properties: Record<string, ContentPropertySchema>,\n raw: Record<string, unknown>,\n): Record<string, unknown> {\n const projected: Record<string, unknown> = {};\n for (const [key, property] of Object.entries(properties)) {\n const value = ownValue(raw, key);\n if (value === undefined) continue;\n if (property.type === 'array') {\n projected[key] = Array.isArray(value)\n ? value.map((member) => (isRecord(member) ? projectObject(property.items.properties, member) : member))\n : value;\n } else if (property.type === 'object') {\n projected[key] = isRecord(value) ? projectObject(property.properties, value) : value;\n } else {\n projected[key] = value;\n }\n }\n return projected;\n}\n\n/** Shape-level validation only (required presence, primitive type, enum membership), at every level. */\nfunction findShapeViolation(model: ContentTypeDefinition, content: Record<string, unknown>): string | null {\n return findObjectViolation('', model.schema, content);\n}\n\nfunction findObjectViolation(path: string, shape: ClosedObjectShape, value: Record<string, unknown>): string | null {\n for (const key of shape.required ?? []) {\n if (ownValue(value, key) === undefined) return `missing required property \"${joinPath(path, key)}\"`;\n }\n for (const [key, property] of Object.entries(shape.properties)) {\n const member = ownValue(value, key);\n if (member === undefined) continue;\n const violation = findPropertyViolation(joinPath(path, key), property, member);\n if (violation !== null) return violation;\n }\n return null;\n}\n\nfunction findPropertyViolation(path: string, property: ContentPropertySchema, value: unknown): string | null {\n if (property.type === 'array') {\n if (!Array.isArray(value)) return `property \"${path}\" does not match type \"array\"`;\n for (const [index, member] of value.entries()) {\n const itemPath = `${path}[${index}]`;\n if (!isRecord(member)) return `property \"${itemPath}\" does not match type \"object\"`;\n const violation = findObjectViolation(itemPath, property.items, member);\n if (violation !== null) return violation;\n }\n // minItems and maxItems are author-time bounds; the read path does not enforce them.\n return null;\n }\n if (property.type === 'object') {\n if (!isRecord(value)) return `property \"${path}\" does not match type \"object\"`;\n return findObjectViolation(path, property, value);\n }\n if (!matchesScalarType(property.type, value)) return `property \"${path}\" does not match type \"${property.type}\"`;\n if (property.enum && !property.enum.includes(value as string | number)) {\n return `property \"${path}\" is not an allowed enum value`;\n }\n return null;\n}\n\nfunction joinPath(path: string, key: string): string {\n return path.length === 0 ? key : `${path}.${key}`;\n}\n\nfunction matchesScalarType(type: 'string' | 'number' | 'integer' | 'boolean', value: unknown): boolean {\n switch (type) {\n case 'string':\n return typeof value === 'string';\n case 'boolean':\n return typeof value === 'boolean';\n case 'number':\n return typeof value === 'number' && Number.isFinite(value);\n case 'integer':\n return typeof value === 'number' && Number.isInteger(value);\n }\n}\n\ntype LogLevel = 'error' | 'warn' | 'info';\n\n/** The closed vocabulary a log consumer discriminates on. Nothing else is ever emitted. */\ntype ContentEvent =\n | 'content.read.ok'\n | 'content.read.stale'\n | 'content.read.missing'\n | 'content.read.invalid'\n | 'content.read.failed'\n | 'content.elements.omitted'\n | 'content.retention.skipped';\n\n/** One omitted element, as the aggregate line reports it. `reason` is a path, never a value. */\ntype ElementOmission = { elementId: string; modelId: string; reason: string };\n\n/**\n * Severity as a function of the read's status, and nothing else: `unavailable` means the runtime\n * could not read, which is the on-call signal; `not-found` and `invalid` mean the content is broken\n * while the runtime works. `source` is a payload field, so a preview read that cannot resolve is a\n * failure like any other. `invalid` is reachable from the preview branch only — the published path\n * throws on a bad body, which is why the store covers that case.\n */\nconst READ_EVENTS: Record<ContentEntryResult['status'], { level: LogLevel; event: ContentEvent }> = {\n found: { level: 'info', event: 'content.read.ok' },\n 'not-found': { level: 'warn', event: 'content.read.missing' },\n invalid: { level: 'warn', event: 'content.read.invalid' },\n unavailable: { level: 'error', event: 'content.read.failed' },\n};\n\n/**\n * The one line the status does not decide: the read failed and the stored blob covered it. `warn`\n * because the page rendered, which keeps `error` meaning exactly \"the shopper is seeing holes\". A\n * covered outage never escalates on its own, so alerting on one means filtering this event name.\n */\nconst STALE_READ_EVENT: { level: LogLevel; event: ContentEvent } = { level: 'warn', event: 'content.read.stale' };\n\n/**\n * The runtime's one log line: a constant message plus a payload whose `event` is the sole event\n * discriminator, so rewording prose cannot break a consumer. Nothing here ever carries a content\n * value, a preview token, or the broker secret.\n *\n * `entryId` is high-cardinality and stays log-only. Feeding these events to a metrics pipeline —\n * which no part of this runtime has today — means dropping or hashing it at that boundary; the six\n * event names are the bounded dimension set such a pipeline would inherit.\n */\nfunction logEvent(level: LogLevel, event: ContentEvent, fields: Record<string, unknown>): void {\n // `event` spreads last so a `fields` key of the same name cannot displace the discriminator every\n // consumer keys on. `fields` is a `Record<string, unknown>`, so nothing in the types would object.\n console[level]('[content-runtime]', { ...fields, event });\n}\n\nfunction transientReason(error: unknown): string {\n if (isTimeoutError(error)) return 'timeout';\n return error instanceof Error ? error.message : 'network error';\n}\n\n/**\n * The exact UTF-8 byte count of the serialized blob, not the JS string's UTF-16 code-unit count\n * (`.length`). Non-ASCII text — accented Latin, common in Portuguese-language content, at 2 bytes per\n * code unit, or CJK at 3 — makes those two numbers diverge, and a check against `.length` would let a\n * blob past both ceilings while genuinely over them in bytes.\n */\nfunction blobByteLength(blob: Record<string, unknown>): number {\n return Buffer.byteLength(JSON.stringify(blob), 'utf8');\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Own-property read. Every object keyed by an authored id here comes from `JSON.parse` or a plain\n * literal, so it carries `Object.prototype`: a key named `__proto__` or `constructor` would resolve\n * to something inherited instead of to `undefined`, and the absent-key branch would never be taken.\n */\nfunction ownValue<T>(source: Record<string, T>, key: string): T | undefined {\n return Object.hasOwn(source, key) ? source[key] : undefined;\n}\n\n/** Property check instead of `instanceof`: `AbortSignal.timeout` rejects with a `DOMException`. */\nfunction isTimeoutError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return false;\n const { name } = error as { name?: unknown };\n return name === 'TimeoutError' || name === 'AbortError';\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/sdk/data/empty-content-value.ts
|
|
2
|
+
/**
|
|
3
|
+
* The emptiness rule shared by the SDK helper that binds fragment variables and by the components
|
|
4
|
+
* that pick between a content field and the prop of the same name: `undefined`, `null`, `""`, a
|
|
5
|
+
* whitespace-only string and `[]` mean the merchant left the field unfilled. Trimming only decides
|
|
6
|
+
* emptiness; the value itself is sent and rendered as authored (content-models reference).
|
|
7
|
+
*/
|
|
8
|
+
function isEmptyContentValue(value) {
|
|
9
|
+
if (value === void 0 || value === null) return true;
|
|
10
|
+
if (typeof value === "string") return value.trim() === "";
|
|
11
|
+
return Array.isArray(value) && value.length === 0;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { isEmptyContentValue };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=empty-content-value.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"empty-content-value.js","names":[],"sources":["../../../src/sdk/data/empty-content-value.ts"],"sourcesContent":["/**\n * The emptiness rule shared by the SDK helper that binds fragment variables and by the components\n * that pick between a content field and the prop of the same name: `undefined`, `null`, `\"\"`, a\n * whitespace-only string and `[]` mean the merchant left the field unfilled. Trimming only decides\n * emptiness; the value itself is sent and rendered as authored (content-models reference).\n */\nexport function isEmptyContentValue(value: unknown): boolean {\n if (value === undefined || value === null) return true;\n if (typeof value === 'string') return value.trim() === '';\n return Array.isArray(value) && value.length === 0;\n}\n"],"mappings":";;;;;;;AAMA,SAAgB,oBAAoB,OAAyB;CAC3D,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,OAAO;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO,MAAM,KAAK,MAAM;CACvD,OAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW;AAClD"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
//#region src/sdk/data/entity-resolution-context.ts
|
|
4
|
+
/**
|
|
5
|
+
* Is the `$data` reaching this subtree the cache's own answer for the entity the page query
|
|
6
|
+
* currently points at, or the server-rendered `fallbackData` standing in for one?
|
|
7
|
+
*
|
|
8
|
+
* `DataOverrideContext` carries the slice and cannot carry this: a fallback slice and a resolved
|
|
9
|
+
* one are the same shape, and an entity whose saved values are genuinely absent looks exactly like
|
|
10
|
+
* an entity whose cache read regressed. Only `useReactiveEntity` knows which of the two it just
|
|
11
|
+
* handed down, so it is the one that publishes here — see its `resolved` return.
|
|
12
|
+
*
|
|
13
|
+
* Default `true`: with no reactive provider above, nothing is outstanding. Storybook, unit tests and
|
|
14
|
+
* any tree handing `$data` over as a plain prop read the default and gate nothing, exactly as they
|
|
15
|
+
* do with `useReactiveDataSettled`.
|
|
16
|
+
*/
|
|
17
|
+
const EntityResolutionContext = createContext(true);
|
|
18
|
+
/**
|
|
19
|
+
* Every entity slice composed into this subtree's `$data` is the cache's own answer.
|
|
20
|
+
*
|
|
21
|
+
* Unlike `useReactiveDataSettled`, this is not latched: it re-arms on its own whenever the entity
|
|
22
|
+
* ref moves or its cache read stops being complete, which is the whole point — a latched page-level
|
|
23
|
+
* signal cannot tell a form that the data under it has gone back to being a placeholder.
|
|
24
|
+
*/
|
|
25
|
+
function useEntityDataResolved() {
|
|
26
|
+
return useContext(EntityResolutionContext);
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { EntityResolutionContext, useEntityDataResolved };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=entity-resolution-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-resolution-context.js","names":[],"sources":["../../../src/sdk/data/entity-resolution-context.ts"],"sourcesContent":["'use client';\n\nimport { createContext, useContext } from 'react';\n\n/**\n * Is the `$data` reaching this subtree the cache's own answer for the entity the page query\n * currently points at, or the server-rendered `fallbackData` standing in for one?\n *\n * `DataOverrideContext` carries the slice and cannot carry this: a fallback slice and a resolved\n * one are the same shape, and an entity whose saved values are genuinely absent looks exactly like\n * an entity whose cache read regressed. Only `useReactiveEntity` knows which of the two it just\n * handed down, so it is the one that publishes here — see its `resolved` return.\n *\n * Default `true`: with no reactive provider above, nothing is outstanding. Storybook, unit tests and\n * any tree handing `$data` over as a plain prop read the default and gate nothing, exactly as they\n * do with `useReactiveDataSettled`.\n */\nexport const EntityResolutionContext = createContext<boolean>(true);\n\n/**\n * Every entity slice composed into this subtree's `$data` is the cache's own answer.\n *\n * Unlike `useReactiveDataSettled`, this is not latched: it re-arms on its own whenever the entity\n * ref moves or its cache read stops being complete, which is the whole point — a latched page-level\n * signal cannot tell a form that the data under it has gone back to being a placeholder.\n */\nexport function useEntityDataResolved(): boolean {\n return useContext(EntityResolutionContext);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAa,0BAA0B,cAAuB,IAAI;;;;;;;;AASlE,SAAgB,wBAAiC;CAC/C,OAAO,WAAW,uBAAuB;AAC3C"}
|