@occluze/tether 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
- package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
- package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
- package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
- package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
- package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
- package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
- package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
- package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
- package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
- package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
- package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
- package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
- package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
- package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
- package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
- package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -2
- package/dist/presets/barba.esm.js +19 -23
- package/dist/tether.account-pages.js +320 -365
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +242 -491
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +66 -88
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +36 -33
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +28 -36
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1270 -894
- package/dist/tether.markets.js +73 -90
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +0 -0
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +44 -68
- package/dist/tether.search.min.js +1 -1
- package/dist/types/server/src/shared/account-orders.d.ts +44 -0
- package/dist/types/server/src/shared/account-routes.d.ts +153 -0
- package/dist/types/server/src/shared/api-versions.d.ts +13 -0
- package/dist/types/server/src/shared/error-message.d.ts +2 -0
- package/dist/types/server/src/shared/json.d.ts +25 -0
- package/dist/types/server/src/shared/market-codes.d.ts +16 -0
- package/dist/types/src/account/addresses-view.d.ts +5 -0
- package/dist/types/src/account/answers.d.ts +40 -0
- package/dist/types/src/account/markup.d.ts +19 -0
- package/dist/types/src/account/operations.d.ts +41 -0
- package/dist/types/src/account/order-lines.d.ts +13 -0
- package/dist/types/src/account/order-list.d.ts +12 -0
- package/dist/types/src/account/order-page.d.ts +6 -0
- package/dist/types/src/account/page-answers.d.ts +182 -0
- package/dist/types/src/account/page-events.d.ts +2 -0
- package/dist/types/src/account/server-client.d.ts +60 -0
- package/dist/types/src/account/subscriptions-view.d.ts +2 -0
- package/dist/types/src/account-api.d.ts +18 -0
- package/dist/types/src/account-marks.d.ts +15 -0
- package/dist/types/src/account-pages.d.ts +8 -0
- package/dist/types/src/account.d.ts +64 -0
- package/dist/types/src/amounts.d.ts +12 -0
- package/dist/types/src/analytics-shopify.d.ts +41 -0
- package/dist/types/src/analytics.d.ts +2 -0
- package/dist/types/src/api-version.d.ts +28 -0
- package/dist/types/src/astro.d.ts +42 -0
- package/dist/types/src/cart/cart-shape.d.ts +43 -0
- package/dist/types/src/cart/checkout.d.ts +16 -0
- package/dist/types/src/cart/drawer.d.ts +18 -0
- package/dist/types/src/cart/mutations.d.ts +79 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +182 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +100 -0
- package/dist/types/src/collection.d.ts +25 -0
- package/dist/types/src/config-sections.d.ts +40 -0
- package/dist/types/src/config.d.ts +185 -0
- package/dist/types/src/controls/cart-controls.d.ts +2 -0
- package/dist/types/src/controls/discount.d.ts +2 -0
- package/dist/types/src/controls/extras.d.ts +5 -0
- package/dist/types/src/controls/navigation.d.ts +12 -0
- package/dist/types/src/diagnose-markup.d.ts +19 -0
- package/dist/types/src/diagnose.d.ts +60 -0
- package/dist/types/src/dom.d.ts +75 -0
- package/dist/types/src/extras.d.ts +21 -0
- package/dist/types/src/fragments.d.ts +158 -0
- package/dist/types/src/globals.d.ts +15 -0
- package/dist/types/src/images.d.ts +20 -0
- package/dist/types/src/index.d.ts +167 -0
- package/dist/types/src/initial.d.ts +56 -0
- package/dist/types/src/lazy.d.ts +28 -0
- package/dist/types/src/loader.d.ts +84 -0
- package/dist/types/src/market-codes.d.ts +6 -0
- package/dist/types/src/markets-ui.d.ts +32 -0
- package/dist/types/src/markets.d.ts +72 -0
- package/dist/types/src/markup.d.ts +131 -0
- package/dist/types/src/mounting.d.ts +46 -0
- package/dist/types/src/page-market.d.ts +27 -0
- package/dist/types/src/presets/barba.d.ts +56 -0
- package/dist/types/src/product-events.d.ts +2 -0
- package/dist/types/src/product-render.d.ts +17 -0
- package/dist/types/src/product-shape.d.ts +88 -0
- package/dist/types/src/product-state.d.ts +110 -0
- package/dist/types/src/product.d.ts +66 -0
- package/dist/types/src/repeaters.d.ts +16 -0
- package/dist/types/src/search.d.ts +33 -0
- package/dist/types/src/selling-plans.d.ts +64 -0
- package/dist/types/src/server.d.ts +88 -0
- package/dist/types/src/shopify-values.d.ts +46 -0
- package/dist/types/src/status.d.ts +33 -0
- package/dist/types/src/storage.d.ts +12 -0
- package/dist/types/src/store-html.d.ts +39 -0
- package/dist/types/src/storefront/catalog.d.ts +38 -0
- package/dist/types/src/storefront/json-ld.d.ts +20 -0
- package/dist/types/src/variant-render.d.ts +14 -0
- package/package.json +24 -23
- package/src/account/addresses-view.ts +58 -0
- package/src/account/answers.ts +79 -0
- package/src/account/markup.ts +36 -0
- package/src/account/operations.ts +145 -0
- package/src/account/{order-lines.js → order-lines.ts} +20 -41
- package/src/account/order-list.ts +107 -0
- package/src/account/order-page.ts +137 -0
- package/src/account/page-answers.ts +317 -0
- package/src/account/{page-events.js → page-events.ts} +55 -90
- package/src/account/server-client.ts +183 -0
- package/src/account/{subscriptions-view.js → subscriptions-view.ts} +25 -68
- package/src/account-api.ts +98 -0
- package/src/account-marks.ts +35 -0
- package/src/{account-pages.js → account-pages.ts} +29 -37
- package/src/account.ts +284 -0
- package/src/amounts.ts +64 -0
- package/src/{analytics-shopify.js → analytics-shopify.ts} +112 -107
- package/src/analytics.ts +315 -0
- package/src/{api-version.js → api-version.ts} +25 -31
- package/src/{astro.js → astro.ts} +110 -104
- package/src/cart/cart-shape.ts +25 -0
- package/src/cart/checkout.ts +101 -0
- package/src/cart/{drawer.js → drawer.ts} +18 -31
- package/src/cart/mutations.ts +237 -0
- package/src/cart/{render.js → render.ts} +68 -96
- package/src/cart/store.ts +279 -0
- package/src/{cart.js → cart.ts} +82 -128
- package/src/client.ts +391 -0
- package/src/{collection.js → collection.ts} +30 -38
- package/src/config-sections.ts +284 -0
- package/src/config.ts +480 -0
- package/src/controls/{cart-controls.js → cart-controls.ts} +22 -54
- package/src/controls/{discount.js → discount.ts} +13 -34
- package/src/controls/{extras.js → extras.ts} +15 -24
- package/src/controls/{navigation.js → navigation.ts} +24 -30
- package/src/{diagnose-markup.js → diagnose-markup.ts} +37 -72
- package/src/{diagnose.js → diagnose.ts} +95 -96
- package/src/dom.ts +268 -0
- package/src/{extras.js → extras.ts} +43 -52
- package/src/fragments.ts +261 -0
- package/src/globals.d.ts +15 -0
- package/src/{images.js → images.ts} +30 -43
- package/src/index.ts +415 -0
- package/src/initial.ts +101 -0
- package/src/lazy-iife.ts +197 -0
- package/src/lazy.ts +42 -0
- package/src/{loader.js → loader.ts} +149 -123
- package/src/market-codes.ts +6 -0
- package/src/{markets-ui.js → markets-ui.ts} +91 -68
- package/src/markets.ts +246 -0
- package/src/markup.ts +127 -0
- package/src/{mounting.js → mounting.ts} +88 -101
- package/src/{page-market.js → page-market.ts} +32 -38
- package/src/part-integrity.ts +7 -0
- package/src/parts/account-pages.ts +7 -0
- package/src/parts/account.ts +7 -0
- package/src/parts/analytics.ts +7 -0
- package/src/parts/diagnose.ts +7 -0
- package/src/parts/extras.ts +7 -0
- package/src/parts/markets.ts +7 -0
- package/src/parts/search.ts +7 -0
- package/src/presets/barba.ts +194 -0
- package/src/{product-events.js → product-events.ts} +33 -48
- package/src/{product-render.js → product-render.ts} +38 -77
- package/src/product-shape.ts +49 -0
- package/src/product-state.ts +318 -0
- package/src/{product.js → product.ts} +90 -126
- package/src/{repeaters.js → repeaters.ts} +40 -52
- package/src/{search.js → search.ts} +85 -103
- package/src/selling-plans.ts +145 -0
- package/src/server.ts +211 -0
- package/src/{shopify-values.js → shopify-values.ts} +24 -22
- package/src/{status.js → status.ts} +34 -54
- package/src/storage.ts +34 -0
- package/src/store-html.ts +359 -0
- package/src/storefront/catalog.ts +196 -0
- package/src/storefront/{json-ld.js → json-ld.ts} +45 -68
- package/src/{variant-render.js → variant-render.ts} +24 -50
- package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
- package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
- package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
- package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
- package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
- package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
- package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
- package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
- package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
- package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
- package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
- package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
- package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
- package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
- package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
- package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
- package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
- package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
- package/dist/esm/chunks/search-OU75RNQL.js +0 -1
- package/src/account/addresses-view.js +0 -92
- package/src/account/browser-session.js +0 -359
- package/src/account/markup.js +0 -48
- package/src/account/operations.js +0 -180
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/server-client.js +0 -172
- package/src/account/shared.js +0 -111
- package/src/account/shopify-auth.js +0 -150
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics.js +0 -352
- package/src/cart/checkout.js +0 -97
- package/src/cart/mutations.js +0 -234
- package/src/cart/store.js +0 -326
- package/src/client.js +0 -267
- package/src/config.js +0 -349
- package/src/dom.js +0 -287
- package/src/fragments.js +0 -237
- package/src/index.js +0 -353
- package/src/initial.js +0 -87
- package/src/lazy-iife.js +0 -116
- package/src/lazy.js +0 -39
- package/src/market-codes.js +0 -35
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/parts/account-pages.js +0 -6
- package/src/parts/account.js +0 -6
- package/src/parts/analytics.js +0 -6
- package/src/parts/diagnose.js +0 -6
- package/src/parts/extras.js +0 -6
- package/src/parts/markets.js +0 -6
- package/src/parts/search.js +0 -6
- package/src/presets/barba.js +0 -190
- package/src/product-state.js +0 -318
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/types/astro.d.ts +0 -28
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -577
- package/types/server.d.ts +0 -182
package/src/cart/store.js
DELETED
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The cart on this page: what Shopify last said about it (`CartStatus`), the quantities quick clicks asked for, the
|
|
3
|
-
* customer it belongs to, and the one queue every change goes through. A change of what the page shows goes through
|
|
4
|
-
* here, so the document (`renderCart`) and the listeners (`tether:cart-updated`) always see the store's cart.
|
|
5
|
-
*/
|
|
6
|
-
import { cfg } from '../config.js';
|
|
7
|
-
import { emit } from '../dom.js';
|
|
8
|
-
import { isOpen } from './drawer.js';
|
|
9
|
-
import { renderCart, quantityOf, showSavedCount } from './render.js';
|
|
10
|
-
/** @import { CartWarning } from '../config.js' */
|
|
11
|
-
/** @import { Image } from '../fragments.js' */
|
|
12
|
-
/** @import { Money } from '../shopify-values.js' */
|
|
13
|
-
|
|
14
|
-
/** @typedef {{ key: string, value: string }} Attribute */
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {object} CartVariant A line's `merchandise`: Shopify's `Merchandise` union has only `ProductVariant`.
|
|
17
|
-
* @property {string} id
|
|
18
|
-
* @property {string} title
|
|
19
|
-
* @property {string | null} sku
|
|
20
|
-
* @property {boolean} availableForSale
|
|
21
|
-
* @property {boolean} requiresShipping
|
|
22
|
-
* @property {number | null} [quantityAvailable] Absent when the store's token can't read inventory.
|
|
23
|
-
* @property {Image | null} image
|
|
24
|
-
* @property {Money} price
|
|
25
|
-
* @property {Money | null} compareAtPrice
|
|
26
|
-
* @property {{ name: string, value: string }[]} selectedOptions
|
|
27
|
-
* @property {{ id: string, title: string, handle: string, vendor: string, featuredImage: Image | null }} product
|
|
28
|
-
*/
|
|
29
|
-
/**
|
|
30
|
-
* @typedef {object} LinePlanAllocation
|
|
31
|
-
* @property {{ id: string, name: string, description: string | null, recurringDeliveries: boolean,
|
|
32
|
-
* options: { name: string, value: string }[] }} sellingPlan
|
|
33
|
-
* @property {Money} checkoutChargeAmount
|
|
34
|
-
* @property {{ price: Money, compareAtPrice: Money, perDeliveryPrice: Money }[]} priceAdjustments
|
|
35
|
-
*/
|
|
36
|
-
/**
|
|
37
|
-
* @typedef {object} ShopifyCartLine
|
|
38
|
-
* @property {string} id
|
|
39
|
-
* @property {number} quantity
|
|
40
|
-
* @property {Attribute[]} attributes
|
|
41
|
-
* @property {{ totalAmount: Money, subtotalAmount: Money, amountPerQuantity: Money,
|
|
42
|
-
* compareAtAmountPerQuantity: Money | null }} cost
|
|
43
|
-
* @property {{ discountedAmount: Money }[]} discountAllocations
|
|
44
|
-
* @property {LinePlanAllocation | null} sellingPlanAllocation
|
|
45
|
-
* @property {CartVariant} merchandise
|
|
46
|
-
* @property {{ id: string, quantity: number, merchandise: { id: string, title: string,
|
|
47
|
-
* product: { title: string, handle: string } } }[]} [lineComponents] Only on a bundle (`ComponentizableCartLine`).
|
|
48
|
-
*/
|
|
49
|
-
/**
|
|
50
|
-
* @typedef {object} ShopifyCart The fields of `fragment TetherCart`.
|
|
51
|
-
* @property {string} id
|
|
52
|
-
* @property {string} checkoutUrl
|
|
53
|
-
* @property {number} totalQuantity
|
|
54
|
-
* @property {string | null} note
|
|
55
|
-
* @property {string} createdAt
|
|
56
|
-
* @property {string} updatedAt
|
|
57
|
-
* @property {Attribute[]} attributes
|
|
58
|
-
* @property {{ countryCode: string | null, email: string | null }} buyerIdentity
|
|
59
|
-
* @property {{ code: string, applicable: boolean }[]} discountCodes
|
|
60
|
-
* @property {{ subtotalAmount: Money, totalAmount: Money, checkoutChargeAmount: Money }} cost
|
|
61
|
-
* @property {{ nodes: ShopifyCartLine[] }} lines
|
|
62
|
-
*/
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* @typedef {{ kind: 'unknown' }
|
|
66
|
-
* | { kind: 'empty' }
|
|
67
|
-
* | { kind: 'ready', cart: ShopifyCart }
|
|
68
|
-
* | { kind: 'error', error: unknown, cart: ShopifyCart | null }} CartStatus
|
|
69
|
-
* `unknown` until the saved cart has loaded (the page's own markup shows); `empty` without a cart (none saved, or
|
|
70
|
-
* Shopify no longer has it); `error` after a failed load or change, with the cart as Shopify last sent it.
|
|
71
|
-
*/
|
|
72
|
-
|
|
73
|
-
/** @typedef {{ quantity: number }} PendingQuantity What one quick +/- click asked for; a newer click replaces it. */
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @typedef {object} CustomerLink What the account part sets while a customer is logged in.
|
|
77
|
-
* @property {string | null} accessToken The Customer Account API token: carts created now belong to the customer.
|
|
78
|
-
* @property {(() => Promise<string | null>) | null} beforeCheckout The account's own checkout URL; `null` from it, or
|
|
79
|
-
* no function, uses the cart's.
|
|
80
|
-
*/
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @typedef {object} CartMeta The second argument of a `subscribe` listener.
|
|
84
|
-
* @property {boolean} open
|
|
85
|
-
* @property {number} count
|
|
86
|
-
* @property {boolean} loaded
|
|
87
|
-
* @property {readonly CartWarning[]} warnings
|
|
88
|
-
*/
|
|
89
|
-
|
|
90
|
-
/** @type {CartStatus} */
|
|
91
|
-
let status = { kind: 'unknown' };
|
|
92
|
-
|
|
93
|
-
/** @type {readonly CartWarning[]} Shopify's notes on the last change. */
|
|
94
|
-
let warnings = [];
|
|
95
|
-
|
|
96
|
-
/** @type {Map<string, PendingQuantity>} Line id → what quick +/- clicks asked for, until Shopify has answered. */
|
|
97
|
-
export const pendingQuantities = new Map();
|
|
98
|
-
|
|
99
|
-
/** @type {CustomerLink} */
|
|
100
|
-
export const customer = { accessToken: null, beforeCheckout: null };
|
|
101
|
-
|
|
102
|
-
/** @returns {CartStatus} */
|
|
103
|
-
export const cartStatus = () => status;
|
|
104
|
-
|
|
105
|
-
/** @returns {ShopifyCart | null} The cart as Shopify last sent it. */
|
|
106
|
-
export function currentCart() {
|
|
107
|
-
switch (status.kind) {
|
|
108
|
-
case 'ready':
|
|
109
|
-
case 'error':
|
|
110
|
-
return status.cart;
|
|
111
|
-
case 'unknown':
|
|
112
|
-
case 'empty':
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** @returns {readonly CartWarning[]} */
|
|
118
|
-
export const currentWarnings = () => warnings;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* @param {ShopifyCart | null} cart
|
|
122
|
-
* @returns {CartStatus}
|
|
123
|
-
*/
|
|
124
|
-
const statusOf = (cart) => (cart ? { kind: 'ready', cart } : { kind: 'empty' });
|
|
125
|
-
|
|
126
|
-
const seenKey = () => `${cfg.storageKey}:seen`;
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* localStorage throws in private mode and where the visitor blocks site data: the cart then lasts for this page.
|
|
130
|
-
*
|
|
131
|
-
* @param {string} key
|
|
132
|
-
* @returns {string | null}
|
|
133
|
-
*/
|
|
134
|
-
function readStorageIfAllowed(key) {
|
|
135
|
-
try {
|
|
136
|
-
return localStorage.getItem(key);
|
|
137
|
-
} catch {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Like `readStorageIfAllowed`: without storage, nothing is remembered.
|
|
144
|
-
*
|
|
145
|
-
* @param {string} key
|
|
146
|
-
* @param {string | null} value `null` removes the key.
|
|
147
|
-
*/
|
|
148
|
-
function writeStorageIfAllowed(key, value) {
|
|
149
|
-
try {
|
|
150
|
-
if (value === null) localStorage.removeItem(key);
|
|
151
|
-
else localStorage.setItem(key, value);
|
|
152
|
-
} catch {
|
|
153
|
-
// Private mode or blocked storage: see readStorageIfAllowed.
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** @returns {string | null} The id of the visitor's cart, kept across pages. */
|
|
158
|
-
export const savedCartId = () => readStorageIfAllowed(cfg.storageKey) || null;
|
|
159
|
-
|
|
160
|
-
/** Forgets the saved cart, which Shopify no longer has. */
|
|
161
|
-
export function forgetSavedCart() {
|
|
162
|
-
writeStorageIfAllowed(cfg.storageKey, null);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Keeps a cart Shopify sent without showing it, for an answer a newer change will replace.
|
|
167
|
-
*
|
|
168
|
-
* @param {ShopifyCart | null} cart
|
|
169
|
-
*/
|
|
170
|
-
export function keepQuietly(cart) {
|
|
171
|
-
status = statusOf(cart);
|
|
172
|
-
if (cart) writeStorageIfAllowed(cfg.storageKey, cart.id);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Renders the document with the store's cart. */
|
|
176
|
-
export function render() {
|
|
177
|
-
renderCart({
|
|
178
|
-
cart: currentCart(),
|
|
179
|
-
warnings,
|
|
180
|
-
pending: pendingQuantities,
|
|
181
|
-
failed: status.kind === 'error',
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* @param {string} reason The `reason` of `tether:cart-updated`.
|
|
187
|
-
*/
|
|
188
|
-
export function announce(reason) {
|
|
189
|
-
render();
|
|
190
|
-
emit('cart-updated', { cart: currentCart(), reason });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Keeps a cart Shopify sent after a change, shows it, and tells this page and the visitor's other tabs.
|
|
195
|
-
*
|
|
196
|
-
* @param {ShopifyCart | null} cart
|
|
197
|
-
* @param {string} reason
|
|
198
|
-
* @returns {ShopifyCart | null} `cart`.
|
|
199
|
-
*/
|
|
200
|
-
export function commit(cart, reason) {
|
|
201
|
-
keepQuietly(cart);
|
|
202
|
-
// Other tabs reload the cart on this storage event; the next page shows the count before its cart has loaded.
|
|
203
|
-
writeStorageIfAllowed(seenKey(), `${Date.now()}|${cart?.totalQuantity ?? 0}`);
|
|
204
|
-
announce(reason);
|
|
205
|
-
return cart;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* The outcome of loading the saved cart: the cart, `null` when there is none, or the failure.
|
|
210
|
-
*
|
|
211
|
-
* @param {{ kind: 'loaded', cart: ShopifyCart | null } | { kind: 'failed', error: unknown }} outcome
|
|
212
|
-
*/
|
|
213
|
-
export function settleLoad(outcome) {
|
|
214
|
-
status = outcome.kind === 'loaded' ? statusOf(outcome.cart) : { kind: 'error', error: outcome.error, cart: null };
|
|
215
|
-
announce('load');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Sets or clears the failure without rendering, for `Tether.cart.lastError`, which the public types declare writable.
|
|
220
|
-
*
|
|
221
|
-
* @param {unknown} error `null` or `undefined` clears it.
|
|
222
|
-
*/
|
|
223
|
-
export function setFailure(error) {
|
|
224
|
-
const cart = currentCart();
|
|
225
|
-
status = error == null ? statusOf(cart) : { kind: 'error', error, cart };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Until the saved cart has loaded, the counts show the count it had on the last page instead of the page's 0. */
|
|
229
|
-
export function showLastKnownCount() {
|
|
230
|
-
if (status.kind !== 'unknown') return;
|
|
231
|
-
const count = readStorageIfAllowed(seenKey())?.split('|')[1];
|
|
232
|
-
if (count) showSavedCount(count);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Sets Shopify's notes on the last change: each message once, in the site's words (`warningText`).
|
|
237
|
-
*
|
|
238
|
-
* @param {readonly CartWarning[]} notes
|
|
239
|
-
*/
|
|
240
|
-
export function setWarnings(notes) {
|
|
241
|
-
warnings = notes
|
|
242
|
-
.filter((note, index) => notes.findIndex((other) => other.message === note.message) === index)
|
|
243
|
-
.map((note) => ({ ...note, message: cfg.warningText?.(note) || note.message }));
|
|
244
|
-
if (warnings.length) emit('cart-warning', { warnings });
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** The queue only waits for a task: its result, or failure, reaches the caller through the promise `run` returns. */
|
|
248
|
-
const waitOnly = () => undefined;
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* @returns {<T>(task: () => Promise<T>) => Promise<T>} A runner that starts each task once the one before has
|
|
252
|
-
* settled, whether it succeeded or failed.
|
|
253
|
-
*/
|
|
254
|
-
export function serial() {
|
|
255
|
-
/** @type {Promise<unknown>} */
|
|
256
|
-
let previous = Promise.resolve();
|
|
257
|
-
return (task) => {
|
|
258
|
-
const run = previous.then(task);
|
|
259
|
-
previous = run.then(waitOnly, waitOnly);
|
|
260
|
-
return run;
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const runInOrder = serial();
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Runs a cart change after the ones before it, so quick clicks never race. A failure sets the `error` status (shown by
|
|
268
|
-
* `tether-show="cart-error"` until the next change succeeds), emits `tether:cart-error` and rejects.
|
|
269
|
-
*
|
|
270
|
-
* @template T
|
|
271
|
-
* @param {() => Promise<T>} task
|
|
272
|
-
* @returns {Promise<T>}
|
|
273
|
-
*/
|
|
274
|
-
export function enqueue(task) {
|
|
275
|
-
return runInOrder(task).then(
|
|
276
|
-
(value) => {
|
|
277
|
-
if (status.kind === 'error') {
|
|
278
|
-
status = statusOf(status.cart);
|
|
279
|
-
render();
|
|
280
|
-
}
|
|
281
|
-
return value;
|
|
282
|
-
},
|
|
283
|
-
(/** @type {unknown} */ error) => {
|
|
284
|
-
status = { kind: 'error', error, cart: currentCart() };
|
|
285
|
-
render();
|
|
286
|
-
emit('cart-error', { error });
|
|
287
|
-
throw error;
|
|
288
|
-
},
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* @param {ShopifyCartLine} line
|
|
294
|
-
* @returns {number} The quantity to show for `line`.
|
|
295
|
-
*/
|
|
296
|
-
export const pendingQuantityOf = (line) => quantityOf(line, pendingQuantities);
|
|
297
|
-
|
|
298
|
-
/** @returns {CartMeta} */
|
|
299
|
-
const meta = () => ({
|
|
300
|
-
open: isOpen(),
|
|
301
|
-
count: currentCart()?.totalQuantity ?? 0,
|
|
302
|
-
loaded: status.kind !== 'unknown',
|
|
303
|
-
warnings,
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Calls `listener` now and after every change of the cart or the drawer.
|
|
308
|
-
*
|
|
309
|
-
* @param {(cart: ShopifyCart | null, meta: CartMeta) => void} listener
|
|
310
|
-
* @returns {() => void} Stops listening.
|
|
311
|
-
*/
|
|
312
|
-
export function subscribe(listener) {
|
|
313
|
-
const events = ['cart-updated', 'cart-toggled'].map((name) => `${cfg.prefix}:${name}`);
|
|
314
|
-
const call = () => {
|
|
315
|
-
listener(currentCart(), meta());
|
|
316
|
-
};
|
|
317
|
-
events.forEach((name) => {
|
|
318
|
-
document.addEventListener(name, call);
|
|
319
|
-
});
|
|
320
|
-
call();
|
|
321
|
-
return () => {
|
|
322
|
-
events.forEach((name) => {
|
|
323
|
-
document.removeEventListener(name, call);
|
|
324
|
-
});
|
|
325
|
-
};
|
|
326
|
-
}
|
package/src/client.js
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one Storefront API client: a fetch wrapper that adds the market (`@inContext`), retries what Shopify asks to be
|
|
3
|
-
* retried, and turns every failure into a `TetherError`. The browser (`gql`, also `Tether.query()`) and the server
|
|
4
|
-
* helpers (`request` with their own config) share it.
|
|
5
|
-
*/
|
|
6
|
-
import { cfg, log } from './config.js';
|
|
7
|
-
import { toCountry, toLanguage } from './market-codes.js';
|
|
8
|
-
/** @import { TetherConfig } from './config.js' */
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @typedef {'NOT_FOUND' | 'NETWORK' | 'NOT_CONFIGURED' | 'NOT_LOGGED_IN' | 'NOT_ALLOWED' | 'INVALID'
|
|
12
|
-
* | 'INVALID_COUNTRY' | 'INVALID_DISCOUNT' | 'SESSION_ENDED' | 'TOKEN_FAILED' | 'ERROR' | `HTTP_${number}`}
|
|
13
|
-
* TetherErrorCode
|
|
14
|
-
*/
|
|
15
|
-
/** @typedef {{ message: string, extensions?: { code?: string } }} GraphQLError */
|
|
16
|
-
/** @typedef {{ data?: unknown, errors?: GraphQLError[] }} GraphQLResponse The response shape the GraphQL spec sets. */
|
|
17
|
-
/** @typedef {{ message: string, field?: string[] | null, code?: string | null }} UserError */
|
|
18
|
-
/**
|
|
19
|
-
* @typedef {object} TetherErrorDetails
|
|
20
|
-
* @property {TetherErrorCode} [code] Default `HTTP_<status>` with a status, else `ERROR`.
|
|
21
|
-
* @property {number} [status] The HTTP status of the response that failed.
|
|
22
|
-
* @property {GraphQLError[]} [errors] The GraphQL errors of that response.
|
|
23
|
-
* @property {readonly UserError[]} [userErrors] A mutation's user errors.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
export class TetherError extends Error {
|
|
27
|
-
/**
|
|
28
|
-
* @param {string} message
|
|
29
|
-
* @param {TetherErrorDetails} [details]
|
|
30
|
-
*/
|
|
31
|
-
constructor(message, { status, errors, userErrors, code } = {}) {
|
|
32
|
-
super(message);
|
|
33
|
-
this.name = 'TetherError';
|
|
34
|
-
/** @type {TetherErrorCode} */
|
|
35
|
-
this.code = code ?? (status ? `HTTP_${status}` : 'ERROR');
|
|
36
|
-
/** @type {number | undefined} */
|
|
37
|
-
this.status = status;
|
|
38
|
-
/** @type {GraphQLError[] | undefined} */
|
|
39
|
-
this.errors = errors;
|
|
40
|
-
/** @type {readonly UserError[] | undefined} */
|
|
41
|
-
this.userErrors = userErrors;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* What a request needs from a config: the browser's `cfg`, or a config the server helpers build.
|
|
47
|
-
*
|
|
48
|
-
* @typedef {Pick<TetherConfig, 'domain' | 'token' | 'apiVersion' | 'country' | 'language' | 'maxAge' | 'retries'>
|
|
49
|
-
* & { fetch?: typeof fetch }} StorefrontConfig
|
|
50
|
-
*/
|
|
51
|
-
/** @typedef {{ context?: boolean }} RequestOptions `context: false` sends the query without `@inContext`. */
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @param {Pick<TetherConfig, 'domain' | 'apiVersion'>} [conf]
|
|
55
|
-
* @returns {string} The Storefront GraphQL endpoint of `conf`'s store.
|
|
56
|
-
*/
|
|
57
|
-
export function endpoint(conf = cfg) {
|
|
58
|
-
return `https://${conf.domain}/api/${conf.apiVersion}/graphql.json`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Adds @inContext(country, language) to the first operation in the document, so prices, currency and translations
|
|
63
|
-
* follow the configured market. Operations are always written first, fragments after, so the first match is the
|
|
64
|
-
* operation. A country or language that isn't a valid code is left out.
|
|
65
|
-
*
|
|
66
|
-
* @param {string} query
|
|
67
|
-
* @param {{ country?: string | undefined, language?: string | undefined }} [ctx]
|
|
68
|
-
* @returns {string}
|
|
69
|
-
*/
|
|
70
|
-
export function withContext(query, ctx = cfg) {
|
|
71
|
-
const args = [];
|
|
72
|
-
const country = toCountry(ctx.country);
|
|
73
|
-
const language = toLanguage(ctx.language);
|
|
74
|
-
if (country) args.push(`country: ${country}`);
|
|
75
|
-
if (language) args.push(`language: ${language}`);
|
|
76
|
-
if (!args.length || query.includes('@inContext')) return query;
|
|
77
|
-
|
|
78
|
-
const tag = `@inContext(${args.join(', ')})`;
|
|
79
|
-
// Named (query Name($x: T)), anonymous (query { … }) and shorthand ({ … }) queries.
|
|
80
|
-
if (/^\s*\{/.test(query)) return `query ${tag} ${query.trimStart()}`;
|
|
81
|
-
return query.replace(/^(\s*(?:query|mutation)(?:\s+\w+)?\s*(?:\([^)]*\))?)/, `$1 ${tag}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Backoff after a network error or a 429/5xx: this times 2^attempt. */
|
|
85
|
-
const RETRY_BACKOFF_MS = 300;
|
|
86
|
-
/** Backoff after Shopify's `THROTTLED` error, which asks for a longer wait: this times 2^attempt. */
|
|
87
|
-
const THROTTLE_BACKOFF_MS = 500;
|
|
88
|
-
/** Shopify's status for a request its bot protection rejected; retrying doesn't help. */
|
|
89
|
-
const SECURITY_REJECTION = 430;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* `quantityAvailable` needs the token's `unauthenticated_read_product_inventory` scope; without it Shopify refuses the
|
|
93
|
-
* whole query with an error naming the field. That refusal is learned once per page and store: every later query to
|
|
94
|
-
* the store goes out without the field, so stock levels are unknown and everything else works for one extra request.
|
|
95
|
-
*/
|
|
96
|
-
const INVENTORY_FIELD = 'quantityAvailable';
|
|
97
|
-
|
|
98
|
-
/** @type {Set<string>} The stores (domain, version and token) whose token can't read inventory. */
|
|
99
|
-
const storesWithoutInventory = new Set();
|
|
100
|
-
|
|
101
|
-
/** @param {StorefrontConfig} conf */
|
|
102
|
-
const storeOf = (conf) => `${conf.domain} ${conf.apiVersion} ${conf.token}`;
|
|
103
|
-
|
|
104
|
-
/** @param {number} ms */
|
|
105
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* What a status means, for errors Shopify sends without a message (401/403 have none).
|
|
109
|
-
*
|
|
110
|
-
* @param {number} status
|
|
111
|
-
* @param {StorefrontConfig} conf
|
|
112
|
-
*/
|
|
113
|
-
function statusText(status, conf) {
|
|
114
|
-
if (status === 401 || status === 403) {
|
|
115
|
-
const adminHint = conf.token.startsWith('shpat_') ? ', not an Admin API token (shpat_…)' : '';
|
|
116
|
-
return `Shopify rejected the Storefront token for ${conf.domain} (${status}): use the public Storefront API token (Headless channel)${adminHint}`;
|
|
117
|
-
}
|
|
118
|
-
if (status === 404) {
|
|
119
|
-
return `Store or API version not found (404): check domain ${conf.domain} and apiVersion ${conf.apiVersion}`;
|
|
120
|
-
}
|
|
121
|
-
return `HTTP ${status} from ${conf.domain}`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* @param {Response} res
|
|
126
|
-
* @param {GraphQLError[]} errors
|
|
127
|
-
* @param {StorefrontConfig} conf
|
|
128
|
-
*/
|
|
129
|
-
function graphQLFailure(res, errors, conf) {
|
|
130
|
-
const message = errors
|
|
131
|
-
.map((error) => error.message)
|
|
132
|
-
.filter(Boolean)
|
|
133
|
-
.join(' | ');
|
|
134
|
-
let text = message || 'Storefront API error';
|
|
135
|
-
if (!res.ok) text = res.status === 401 || res.status === 403 || !message ? statusText(res.status, conf) : message;
|
|
136
|
-
return new TetherError(text, { status: res.status, errors });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* @typedef {{ kind: 'data', data: unknown }
|
|
141
|
-
* | { kind: 'retry', backoffMs: number }
|
|
142
|
-
* | { kind: 'inventory-refused' }} Attempt
|
|
143
|
-
*/
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* One request. Failures that retrying can't fix throw.
|
|
147
|
-
*
|
|
148
|
-
* @param {StorefrontConfig} conf
|
|
149
|
-
* @param {string} query
|
|
150
|
-
* @param {Record<string, unknown>} variables
|
|
151
|
-
* @param {boolean} canRetry False on the last attempt: a retryable failure then throws too.
|
|
152
|
-
* @returns {Promise<Attempt>}
|
|
153
|
-
*/
|
|
154
|
-
async function attempt(conf, query, variables, canRetry) {
|
|
155
|
-
let res;
|
|
156
|
-
try {
|
|
157
|
-
res = await (conf.fetch ?? fetch)(endpoint(conf), {
|
|
158
|
-
method: 'POST',
|
|
159
|
-
headers: { 'Content-Type': 'application/json', 'X-Shopify-Storefront-Access-Token': conf.token },
|
|
160
|
-
body: JSON.stringify({ query, variables }),
|
|
161
|
-
});
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (canRetry) return { kind: 'retry', backoffMs: RETRY_BACKOFF_MS };
|
|
164
|
-
throw new TetherError(`Network error: ${error instanceof Error ? error.message : String(error)}`, {
|
|
165
|
-
code: 'NETWORK',
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
if (res.status === SECURITY_REJECTION) {
|
|
169
|
-
throw new TetherError('Shopify security rejection (430)', { status: SECURITY_REJECTION });
|
|
170
|
-
}
|
|
171
|
-
if (canRetry && (res.status === 429 || res.status >= 500)) {
|
|
172
|
-
return { kind: 'retry', backoffMs: RETRY_BACKOFF_MS };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** @type {unknown} */
|
|
176
|
-
let json;
|
|
177
|
-
try {
|
|
178
|
-
json = await res.json();
|
|
179
|
-
} catch {
|
|
180
|
-
const text = res.ok ? `Invalid response (${res.status})` : statusText(res.status, conf);
|
|
181
|
-
throw new TetherError(text, { status: res.status });
|
|
182
|
-
}
|
|
183
|
-
// The Storefront API answers in the shape the GraphQL spec sets, or with an error page that isn't JSON.
|
|
184
|
-
const { data, errors = [] } = /** @type {GraphQLResponse | null} */ (json) ?? {};
|
|
185
|
-
if (errors.length) {
|
|
186
|
-
if (query.includes(INVENTORY_FIELD) && errors.some((error) => error.message.includes(INVENTORY_FIELD))) {
|
|
187
|
-
return { kind: 'inventory-refused' };
|
|
188
|
-
}
|
|
189
|
-
if (canRetry && errors.some((error) => error.extensions?.code === 'THROTTLED')) {
|
|
190
|
-
return { kind: 'retry', backoffMs: THROTTLE_BACKOFF_MS };
|
|
191
|
-
}
|
|
192
|
-
throw graphQLFailure(res, errors, conf);
|
|
193
|
-
}
|
|
194
|
-
if (!res.ok) throw new TetherError(statusText(res.status, conf), { status: res.status });
|
|
195
|
-
return { kind: 'data', data };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* One Storefront API request, for any config: the browser's shared one (`gql`) or one the server helpers build.
|
|
200
|
-
* Queries carry a time bucket so Shopify's cache of an identical query text is at most `maxAge` seconds old, and
|
|
201
|
-
* leave out inventory for a store whose token can't read it (`INVENTORY_FIELD`).
|
|
202
|
-
*
|
|
203
|
-
* @template T The shape of the query's `data`, which the caller's query text decides.
|
|
204
|
-
* @param {StorefrontConfig} conf
|
|
205
|
-
* @param {string} query
|
|
206
|
-
* @param {Record<string, unknown>} [variables]
|
|
207
|
-
* @param {RequestOptions} [options]
|
|
208
|
-
* @returns {Promise<T>}
|
|
209
|
-
* @throws {TetherError} `NOT_CONFIGURED` without a domain or token, `NETWORK` when the store can't be reached,
|
|
210
|
-
* `HTTP_<status>` for an HTTP or GraphQL failure.
|
|
211
|
-
*/
|
|
212
|
-
export async function request(conf, query, variables = {}, { context = true } = {}) {
|
|
213
|
-
if (!conf.domain || !conf.token) {
|
|
214
|
-
throw new TetherError('Tether is not configured: domain and token are required', { code: 'NOT_CONFIGURED' });
|
|
215
|
-
}
|
|
216
|
-
let text = context ? withContext(query, conf) : query;
|
|
217
|
-
if (conf.maxAge > 0 && !/^\s*mutation\b/.test(text)) {
|
|
218
|
-
text += ` #${Math.floor(Date.now() / (conf.maxAge * 1000))}`;
|
|
219
|
-
}
|
|
220
|
-
const store = storeOf(conf);
|
|
221
|
-
let retries = 0;
|
|
222
|
-
for (;;) {
|
|
223
|
-
const sent = storesWithoutInventory.has(store) ? text.replaceAll(INVENTORY_FIELD, '') : text;
|
|
224
|
-
const outcome = await attempt(conf, sent, variables, retries < conf.retries);
|
|
225
|
-
switch (outcome.kind) {
|
|
226
|
-
case 'data':
|
|
227
|
-
// The query text decides the shape of `data`; the caller's `T` states it.
|
|
228
|
-
return /** @type {T} */ (outcome.data);
|
|
229
|
-
case 'inventory-refused':
|
|
230
|
-
log(`the token can't read inventory: ${INVENTORY_FIELD} is left out from now on`);
|
|
231
|
-
storesWithoutInventory.add(store);
|
|
232
|
-
break;
|
|
233
|
-
case 'retry':
|
|
234
|
-
retries += 1;
|
|
235
|
-
log('retry', retries);
|
|
236
|
-
await sleep(outcome.backoffMs * 2 ** retries);
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* `request` with the browser's shared config.
|
|
244
|
-
*
|
|
245
|
-
* @template T
|
|
246
|
-
* @param {string} query
|
|
247
|
-
* @param {Record<string, unknown>} [variables]
|
|
248
|
-
* @param {RequestOptions} [options]
|
|
249
|
-
* @returns {Promise<T>}
|
|
250
|
-
*/
|
|
251
|
-
export const gql = (query, variables, options) => request(cfg, query, variables, options);
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* @template {{ userErrors?: readonly UserError[] | null }} Payload
|
|
255
|
-
* @param {Payload | null | undefined} payload A mutation's payload.
|
|
256
|
-
* @param {string} name The mutation, for the error message.
|
|
257
|
-
* @returns {Payload}
|
|
258
|
-
* @throws {TetherError} `ERROR` on an empty payload or on user errors (in `userErrors`).
|
|
259
|
-
*/
|
|
260
|
-
export function unwrap(payload, name) {
|
|
261
|
-
if (!payload) throw new TetherError(`${name}: empty response`);
|
|
262
|
-
const userErrors = payload.userErrors ?? [];
|
|
263
|
-
if (userErrors.length) {
|
|
264
|
-
throw new TetherError(userErrors.map((error) => error.message).join(' | '), { userErrors });
|
|
265
|
-
}
|
|
266
|
-
return payload;
|
|
267
|
-
}
|