@occluze/tether 0.6.8 → 0.7.1
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 +28 -28
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
- package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
- package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
- package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
- package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
- package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
- package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
- package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
- package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
- package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
- package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
- package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
- package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
- package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
- package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
- package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
- package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
- package/dist/esm/chunks/search-A5DS2IOS.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 +332 -367
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +253 -492
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +82 -89
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +52 -34
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +42 -37
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1361 -935
- package/dist/tether.markets.js +89 -91
- 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 +60 -69
- 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/cart-id.d.ts +2 -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 +85 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +185 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +134 -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 +190 -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 +90 -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 -24
- 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-lines.js +0 -96
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/page-events.js +0 -288
- 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/subscriptions-view.js +0 -158
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account-pages.js +0 -89
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics-shopify.js +0 -241
- package/src/analytics.js +0 -352
- package/src/api-version.js +0 -85
- package/src/astro.js +0 -266
- package/src/cart/checkout.js +0 -97
- package/src/cart/drawer.js +0 -138
- package/src/cart/mutations.js +0 -234
- package/src/cart/render.js +0 -294
- package/src/cart/store.js +0 -326
- package/src/cart.js +0 -391
- package/src/client.js +0 -267
- package/src/collection.js +0 -183
- package/src/config.js +0 -349
- package/src/controls/cart-controls.js +0 -155
- package/src/controls/discount.js +0 -96
- package/src/controls/extras.js +0 -63
- package/src/controls/navigation.js +0 -83
- package/src/diagnose-markup.js +0 -333
- package/src/diagnose.js +0 -295
- package/src/dom.js +0 -287
- package/src/extras.js +0 -145
- package/src/fragments.js +0 -237
- package/src/images.js +0 -104
- 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/loader.js +0 -337
- package/src/market-codes.js +0 -35
- package/src/markets-ui.js +0 -204
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/mounting.js +0 -277
- package/src/page-market.js +0 -116
- 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-events.js +0 -130
- package/src/product-render.js +0 -257
- package/src/product-state.js +0 -318
- package/src/product.js +0 -344
- package/src/repeaters.js +0 -173
- package/src/search.js +0 -323
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/shopify-values.js +0 -89
- package/src/status.js +0 -126
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/src/storefront/json-ld.js +0 -167
- package/src/variant-render.js +0 -165
- 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.js
DELETED
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The cart, as one object: `Tether.cart`. Every change goes through one queue (fast clicks never race) and resolves
|
|
3
|
-
* with the cart Shopify sent back. The state lives in `cart/store.js`, the Cart API calls in `cart/mutations.js`, the
|
|
4
|
-
* drawer in `cart/drawer.js`, the document rendering in `cart/render.js` and the checkout in `cart/checkout.js`.
|
|
5
|
-
*/
|
|
6
|
-
import { cfg, reportError } from './config.js';
|
|
7
|
-
import { TetherError } from './client.js';
|
|
8
|
-
import { emit, toAttributes } from './dom.js';
|
|
9
|
-
import {
|
|
10
|
-
fetchCart,
|
|
11
|
-
isCartGone,
|
|
12
|
-
mutateCart,
|
|
13
|
-
removeLines,
|
|
14
|
-
setLineQuantity,
|
|
15
|
-
toLineInput,
|
|
16
|
-
toLineUpdate,
|
|
17
|
-
updateLines,
|
|
18
|
-
} from './cart/mutations.js';
|
|
19
|
-
import { checkout } from './cart/checkout.js';
|
|
20
|
-
import { drawers, isOpen, openCart, closeCart, toggleCart, trapFocus } from './cart/drawer.js';
|
|
21
|
-
import {
|
|
22
|
-
announce,
|
|
23
|
-
cartStatus,
|
|
24
|
-
commit,
|
|
25
|
-
currentCart,
|
|
26
|
-
currentWarnings,
|
|
27
|
-
customer,
|
|
28
|
-
enqueue,
|
|
29
|
-
forgetSavedCart,
|
|
30
|
-
keepQuietly,
|
|
31
|
-
pendingQuantities,
|
|
32
|
-
pendingQuantityOf,
|
|
33
|
-
render,
|
|
34
|
-
savedCartId,
|
|
35
|
-
setFailure,
|
|
36
|
-
settleLoad,
|
|
37
|
-
showLastKnownCount,
|
|
38
|
-
subscribe,
|
|
39
|
-
} from './cart/store.js';
|
|
40
|
-
/** @import { CartWarning } from './config.js' */
|
|
41
|
-
/** @import { Attributes, LineSpec, CartLineInput, LinePatch } from './cart/mutations.js' */
|
|
42
|
-
/** @import { ShopifyCart, ShopifyCartLine, CustomerLink, CartStatus } from './cart/store.js' */
|
|
43
|
-
|
|
44
|
-
export { trapFocus };
|
|
45
|
-
|
|
46
|
-
/** @returns {readonly ShopifyCartLine[]} */
|
|
47
|
-
const currentLines = () => currentCart()?.lines.nodes ?? [];
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* +1 / -1 for a line. Quick clicks add up: each builds on the quantity the click before asked for, which shows at once
|
|
51
|
-
* (`tether-line="quantity"`, `quantityOf`) until Shopify has answered. Only the latest target is sent; the ones it
|
|
52
|
-
* replaced are skipped.
|
|
53
|
-
*
|
|
54
|
-
* @param {string} lineId
|
|
55
|
-
* @param {number} delta
|
|
56
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
57
|
-
*/
|
|
58
|
-
function changeQuantity(lineId, delta) {
|
|
59
|
-
const line = currentLines().find((candidate) => candidate.id === lineId);
|
|
60
|
-
if (!line) return Promise.resolve(currentCart());
|
|
61
|
-
const target = Math.max(0, pendingQuantityOf(line) + delta);
|
|
62
|
-
const job = { quantity: target };
|
|
63
|
-
pendingQuantities.set(lineId, job);
|
|
64
|
-
announce('pending');
|
|
65
|
-
const isLatest = () => pendingQuantities.get(lineId) === job;
|
|
66
|
-
const sent = enqueue(async () => {
|
|
67
|
-
if (!isLatest()) return currentCart();
|
|
68
|
-
const now = currentLines().find((candidate) => candidate.id === lineId);
|
|
69
|
-
// A + then a -: Shopify already has the quantity.
|
|
70
|
-
if (now?.quantity === target) return currentCart();
|
|
71
|
-
const cart = await setLineQuantity(lineId, target);
|
|
72
|
-
// A newer click is on its way: keep this answer, but show totals and count once that one is in, instead of
|
|
73
|
-
// stepping through every reply.
|
|
74
|
-
if (!isLatest()) {
|
|
75
|
-
keepQuietly(cart);
|
|
76
|
-
return cart;
|
|
77
|
-
}
|
|
78
|
-
return commit(cart, target > 0 ? 'update' : 'remove');
|
|
79
|
-
});
|
|
80
|
-
return sent.finally(() => {
|
|
81
|
-
if (!isLatest()) return;
|
|
82
|
-
pendingQuantities.delete(lineId);
|
|
83
|
-
announce('settled');
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The codes the cart has once a change is its turn in the queue.
|
|
89
|
-
*
|
|
90
|
-
* @param {string[] | string | ((current: string[]) => string[])} codes A function gets the codes as they are by then.
|
|
91
|
-
* @returns {string[]}
|
|
92
|
-
*/
|
|
93
|
-
function discountCodesFrom(codes) {
|
|
94
|
-
const current = (currentCart()?.discountCodes ?? []).map((discount) => discount.code);
|
|
95
|
-
const list = typeof codes === 'function' ? codes(current) : codes;
|
|
96
|
-
return (Array.isArray(list) ? list : [list]).map((code) => code.trim()).filter(Boolean);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @param {Attributes} attributes
|
|
101
|
-
* @returns {{ key: string, value: string }[]} The cart's attributes with `attributes` merged in; `''` removes a key.
|
|
102
|
-
*/
|
|
103
|
-
function mergedAttributes(attributes) {
|
|
104
|
-
const merged = new Map((currentCart()?.attributes ?? []).map((attribute) => [attribute.key, attribute.value]));
|
|
105
|
-
toAttributes(attributes).forEach((attribute) => merged.set(attribute.key, attribute.value));
|
|
106
|
-
return [...merged].filter(([, value]) => value !== '').map(([key, value]) => ({ key, value }));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Loads the saved cart. A cart Shopify no longer has is forgotten; one it can't be asked about (the network, the
|
|
111
|
-
* token) is kept, because it is probably fine.
|
|
112
|
-
*
|
|
113
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
114
|
-
*/
|
|
115
|
-
async function init() {
|
|
116
|
-
const id = savedCartId();
|
|
117
|
-
if (!id) {
|
|
118
|
-
settleLoad({ kind: 'loaded', cart: null });
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
showLastKnownCount();
|
|
122
|
-
try {
|
|
123
|
-
const cart = await fetchCart(id);
|
|
124
|
-
if (!cart) forgetSavedCart();
|
|
125
|
-
settleLoad({ kind: 'loaded', cart });
|
|
126
|
-
} catch (error) {
|
|
127
|
-
reportError(error, 'cart load failed');
|
|
128
|
-
if (isCartGone(error)) forgetSavedCart();
|
|
129
|
-
settleLoad({ kind: 'failed', error });
|
|
130
|
-
}
|
|
131
|
-
return currentCart();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export const Cart = {
|
|
135
|
-
init,
|
|
136
|
-
/** @returns {string | null} The saved cart's id. */
|
|
137
|
-
id: savedCartId,
|
|
138
|
-
|
|
139
|
-
/** @returns {CartStatus} */
|
|
140
|
-
get status() {
|
|
141
|
-
return cartStatus();
|
|
142
|
-
},
|
|
143
|
-
/** @returns {ShopifyCart | null} */
|
|
144
|
-
get state() {
|
|
145
|
-
return currentCart();
|
|
146
|
-
},
|
|
147
|
-
/** @returns {readonly ShopifyCartLine[]} */
|
|
148
|
-
get lines() {
|
|
149
|
-
return currentLines();
|
|
150
|
-
},
|
|
151
|
-
/** @returns {number} */
|
|
152
|
-
get count() {
|
|
153
|
-
return currentCart()?.totalQuantity ?? 0;
|
|
154
|
-
},
|
|
155
|
-
/** @returns {boolean} True once the saved cart, or that there is none, is known. */
|
|
156
|
-
get loaded() {
|
|
157
|
-
return cartStatus().kind !== 'unknown';
|
|
158
|
-
},
|
|
159
|
-
/** @returns {unknown} The failure of the last load or change, until a change succeeds. */
|
|
160
|
-
get lastError() {
|
|
161
|
-
const current = cartStatus();
|
|
162
|
-
return current.kind === 'error' ? current.error : null;
|
|
163
|
-
},
|
|
164
|
-
set lastError(error) {
|
|
165
|
-
setFailure(error);
|
|
166
|
-
},
|
|
167
|
-
/** @returns {readonly CartWarning[]} Shopify's notes on the last change. */
|
|
168
|
-
get warnings() {
|
|
169
|
-
return currentWarnings();
|
|
170
|
-
},
|
|
171
|
-
/** @returns {ReadonlyMap<string, { quantity: number }>} Line id → the quantity quick clicks asked for. */
|
|
172
|
-
get pendingQty() {
|
|
173
|
-
return pendingQuantities;
|
|
174
|
-
},
|
|
175
|
-
|
|
176
|
-
/** @returns {string | null} The logged-in customer's Customer Account API token (set by the account part). */
|
|
177
|
-
get customerAccessToken() {
|
|
178
|
-
return customer.accessToken;
|
|
179
|
-
},
|
|
180
|
-
set customerAccessToken(token) {
|
|
181
|
-
customer.accessToken = token || null;
|
|
182
|
-
},
|
|
183
|
-
/** @returns {(() => Promise<string | null>) | null} The account's own checkout URL (set by the account part). */
|
|
184
|
-
get beforeCheckout() {
|
|
185
|
-
return customer.beforeCheckout;
|
|
186
|
-
},
|
|
187
|
-
set beforeCheckout(fetchUrl) {
|
|
188
|
-
customer.beforeCheckout = typeof fetchUrl === 'function' ? fetchUrl : null;
|
|
189
|
-
},
|
|
190
|
-
/**
|
|
191
|
-
* Links the cart to a logged-in customer, or unlinks it with `null`s. Carts created from now on belong to the
|
|
192
|
-
* customer; `beforeCheckout` gives the checkout URL (`null` from it uses the cart's).
|
|
193
|
-
*
|
|
194
|
-
* @param {Partial<CustomerLink>} link Fields left out keep their value.
|
|
195
|
-
*/
|
|
196
|
-
setCustomer(link) {
|
|
197
|
-
if (link.accessToken !== undefined) customer.accessToken = link.accessToken;
|
|
198
|
-
if (link.beforeCheckout !== undefined) customer.beforeCheckout = link.beforeCheckout;
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
/** Keeps a cart and shows it, as a change does; tests seed the cart with it. */
|
|
202
|
-
_commit: commit,
|
|
203
|
-
render,
|
|
204
|
-
subscribe,
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* `add(variantId, 2)`, `add({ variantId, quantity, sellingPlanId, attributes })`, or `add([line, line])` for
|
|
208
|
-
* bundles in one request. Creates the cart when there is none.
|
|
209
|
-
*
|
|
210
|
-
* @param {string | number | LineSpec | (string | number | LineSpec)[]} input
|
|
211
|
-
* @param {number} [quantity]
|
|
212
|
-
* @param {Omit<LineSpec, 'variantId' | 'quantity'>} [options]
|
|
213
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
214
|
-
*/
|
|
215
|
-
add(input, quantity = 1, options = {}) {
|
|
216
|
-
/** @type {CartLineInput[]} */
|
|
217
|
-
const lines = (Array.isArray(input) ? input : [input]).map((line) => toLineInput(line, quantity, options));
|
|
218
|
-
return enqueue(async () => {
|
|
219
|
-
const cart = await mutateCart({
|
|
220
|
-
name: 'cartLinesAdd',
|
|
221
|
-
args: '$lines: [CartLineInput!]!',
|
|
222
|
-
inner: 'lines: $lines',
|
|
223
|
-
variables: { lines },
|
|
224
|
-
createInput: { lines },
|
|
225
|
-
});
|
|
226
|
-
commit(cart, 'add');
|
|
227
|
-
emit('added', { cart, lines });
|
|
228
|
-
if (cfg.openCartOnAdd) openCart();
|
|
229
|
-
return cart;
|
|
230
|
-
});
|
|
231
|
-
},
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* `updateLine(lineId, 3)` or `updateLine(lineId, { quantity, sellingPlanId, attributes })`.
|
|
235
|
-
*
|
|
236
|
-
* @param {string} lineId
|
|
237
|
-
* @param {number | LinePatch} patch A quantity of 0 or less removes the line.
|
|
238
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
239
|
-
*/
|
|
240
|
-
updateLine(lineId, patch) {
|
|
241
|
-
const quantity = typeof patch === 'number' ? patch : patch.quantity;
|
|
242
|
-
if (quantity !== undefined && quantity <= 0) return Cart.removeLine(lineId);
|
|
243
|
-
const line = toLineUpdate(lineId, patch);
|
|
244
|
-
return enqueue(async () => commit(await updateLines([line]), 'update'));
|
|
245
|
-
},
|
|
246
|
-
|
|
247
|
-
changeQuantity,
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* @param {ShopifyCartLine} line
|
|
251
|
-
* @returns {number} The quantity to show: the one asked for while an update is on its way.
|
|
252
|
-
*/
|
|
253
|
-
quantityOf: pendingQuantityOf,
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* @param {string | string[]} lineIds
|
|
257
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
258
|
-
*/
|
|
259
|
-
removeLine(lineIds) {
|
|
260
|
-
const ids = Array.isArray(lineIds) ? lineIds : [lineIds];
|
|
261
|
-
return enqueue(async () => commit(await removeLines(ids), 'remove'));
|
|
262
|
-
},
|
|
263
|
-
|
|
264
|
-
/** @returns {Promise<ShopifyCart | null>} */
|
|
265
|
-
clear() {
|
|
266
|
-
const ids = currentLines().map((line) => line.id);
|
|
267
|
-
if (!ids.length) return Promise.resolve(currentCart());
|
|
268
|
-
return Cart.removeLine(ids);
|
|
269
|
-
},
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* @param {string | null | undefined} note
|
|
273
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
274
|
-
*/
|
|
275
|
-
setNote(note) {
|
|
276
|
-
const variables = { note: note ?? '' };
|
|
277
|
-
return enqueue(async () =>
|
|
278
|
-
commit(
|
|
279
|
-
await mutateCart({
|
|
280
|
-
name: 'cartNoteUpdate',
|
|
281
|
-
args: '$note: String!',
|
|
282
|
-
inner: 'note: $note',
|
|
283
|
-
variables,
|
|
284
|
-
createInput: variables,
|
|
285
|
-
}),
|
|
286
|
-
'note',
|
|
287
|
-
),
|
|
288
|
-
);
|
|
289
|
-
},
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Cart attributes, merged with the cart's (as it is when the change runs, so quick calls don't drop each other).
|
|
293
|
-
*
|
|
294
|
-
* @param {Attributes} attributes The value `''` removes a key.
|
|
295
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
296
|
-
*/
|
|
297
|
-
setAttributes(attributes) {
|
|
298
|
-
return enqueue(async () => {
|
|
299
|
-
const variables = { attributes: mergedAttributes(attributes) };
|
|
300
|
-
const cart = await mutateCart({
|
|
301
|
-
name: 'cartAttributesUpdate',
|
|
302
|
-
args: '$attributes: [AttributeInput!]!',
|
|
303
|
-
inner: 'attributes: $attributes',
|
|
304
|
-
variables,
|
|
305
|
-
createInput: variables,
|
|
306
|
-
});
|
|
307
|
-
return commit(cart, 'attributes');
|
|
308
|
-
});
|
|
309
|
-
},
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Replaces the discount codes; `[]` removes them all.
|
|
313
|
-
*
|
|
314
|
-
* @param {string[] | string | ((current: string[]) => string[])} codes
|
|
315
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
316
|
-
*/
|
|
317
|
-
setDiscountCodes(codes) {
|
|
318
|
-
return enqueue(async () => {
|
|
319
|
-
const variables = { discountCodes: discountCodesFrom(codes) };
|
|
320
|
-
const cart = await mutateCart({
|
|
321
|
-
name: 'cartDiscountCodesUpdate',
|
|
322
|
-
args: '$discountCodes: [String!]!',
|
|
323
|
-
inner: 'discountCodes: $discountCodes',
|
|
324
|
-
variables,
|
|
325
|
-
createInput: variables,
|
|
326
|
-
});
|
|
327
|
-
return commit(cart, 'discount');
|
|
328
|
-
});
|
|
329
|
-
},
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Adds a code. A code Shopify doesn't accept is removed again, so it doesn't warn on every later change.
|
|
333
|
-
*
|
|
334
|
-
* @param {string} code
|
|
335
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
336
|
-
* @throws {TetherError} `INVALID_DISCOUNT` (rejected) when Shopify doesn't accept the code.
|
|
337
|
-
*/
|
|
338
|
-
async applyDiscount(code) {
|
|
339
|
-
const cart = await Cart.setDiscountCodes((current) => [...new Set([...current, code])]);
|
|
340
|
-
const wanted = code.toLowerCase();
|
|
341
|
-
const applied = (cart?.discountCodes ?? []).find((discount) => discount.code.toLowerCase() === wanted);
|
|
342
|
-
if (applied?.applicable) return cart;
|
|
343
|
-
try {
|
|
344
|
-
await Cart.removeDiscount(code);
|
|
345
|
-
} catch (error) {
|
|
346
|
-
reportError(error, 'removing a discount code Shopify refused');
|
|
347
|
-
}
|
|
348
|
-
throw new TetherError(`Discount code not valid: ${code}`, { code: 'INVALID_DISCOUNT' });
|
|
349
|
-
},
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* @param {string} code
|
|
353
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
354
|
-
*/
|
|
355
|
-
removeDiscount(code) {
|
|
356
|
-
const unwanted = code.toLowerCase();
|
|
357
|
-
return Cart.setDiscountCodes((current) => current.filter((existing) => existing.toLowerCase() !== unwanted));
|
|
358
|
-
},
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Sets who is buying; a country changes the cart's currency and prices to that market's.
|
|
362
|
-
*
|
|
363
|
-
* @param {{ countryCode?: string, email?: string, phone?: string, customerAccessToken?: string | null }} buyerIdentity
|
|
364
|
-
* @param {{ create?: boolean }} [options] `create: false` forgets an expired cart instead of creating an empty one.
|
|
365
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
366
|
-
*/
|
|
367
|
-
setBuyerIdentity(buyerIdentity, { create = true } = {}) {
|
|
368
|
-
return enqueue(async () => {
|
|
369
|
-
const cart = await mutateCart({
|
|
370
|
-
name: 'cartBuyerIdentityUpdate',
|
|
371
|
-
args: '$buyerIdentity: CartBuyerIdentityInput!',
|
|
372
|
-
inner: 'buyerIdentity: $buyerIdentity',
|
|
373
|
-
variables: { buyerIdentity },
|
|
374
|
-
createInput: create ? { buyerIdentity } : null,
|
|
375
|
-
}).catch((/** @type {unknown} */ error) => {
|
|
376
|
-
// mutateCart has forgotten the cart Shopify no longer has; without `create` that leaves no cart.
|
|
377
|
-
if (create || !isCartGone(error)) throw error;
|
|
378
|
-
return null;
|
|
379
|
-
});
|
|
380
|
-
return commit(cart, 'buyer-identity');
|
|
381
|
-
});
|
|
382
|
-
},
|
|
383
|
-
|
|
384
|
-
checkout,
|
|
385
|
-
|
|
386
|
-
drawers,
|
|
387
|
-
isOpen,
|
|
388
|
-
open: openCart,
|
|
389
|
-
close: closeCart,
|
|
390
|
-
toggle: toggleCart,
|
|
391
|
-
};
|
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
|
-
}
|