@doswiftly/storefront-sdk 4.3.0 → 4.5.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.
Files changed (96) hide show
  1. package/README.md +6 -14
  2. package/dist/core/cart/types.d.ts +53 -20
  3. package/dist/core/cart/types.d.ts.map +1 -1
  4. package/dist/core/cart/types.js +3 -0
  5. package/dist/core/image.d.ts +4 -46
  6. package/dist/core/image.d.ts.map +1 -1
  7. package/dist/core/image.js +4 -65
  8. package/dist/core/index.d.ts +1 -1
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/index.js +0 -2
  11. package/dist/core/operations/cart.d.ts +15 -9
  12. package/dist/core/operations/cart.d.ts.map +1 -1
  13. package/dist/core/operations/cart.js +130 -58
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/package.json +9 -4
  17. package/src/__tests__/contract/storefront-api.contract.test.ts +0 -450
  18. package/src/__tests__/unit/auth-client.test.ts +0 -210
  19. package/src/__tests__/unit/bot-protection.test.ts +0 -461
  20. package/src/__tests__/unit/cart-client.test.ts +0 -233
  21. package/src/__tests__/unit/cart-store.test.ts +0 -349
  22. package/src/__tests__/unit/create-client.test.ts +0 -356
  23. package/src/__tests__/unit/helpers.test.ts +0 -377
  24. package/src/__tests__/unit/middleware.test.ts +0 -374
  25. package/src/__tests__/unit/test-helpers.ts +0 -103
  26. package/src/core/auth/auth-client.ts +0 -123
  27. package/src/core/auth/cookie-config.ts +0 -23
  28. package/src/core/auth/handlers.ts +0 -168
  29. package/src/core/auth/routes.ts +0 -26
  30. package/src/core/auth/token-client.ts +0 -51
  31. package/src/core/auth/types.ts +0 -54
  32. package/src/core/bot-protection/abstract-manager.ts +0 -185
  33. package/src/core/bot-protection/create-manager.ts +0 -37
  34. package/src/core/bot-protection/eucaptcha-manager.ts +0 -88
  35. package/src/core/bot-protection/fallback-manager.ts +0 -43
  36. package/src/core/bot-protection/turnstile-manager.ts +0 -92
  37. package/src/core/bot-protection/types/eucaptcha.d.ts +0 -28
  38. package/src/core/bot-protection/types/turnstile.d.ts +0 -33
  39. package/src/core/cache.ts +0 -102
  40. package/src/core/cart/cart-client.ts +0 -150
  41. package/src/core/cart/cookie-config.ts +0 -13
  42. package/src/core/cart/types.ts +0 -104
  43. package/src/core/client/compose.ts +0 -15
  44. package/src/core/client/create-client.ts +0 -129
  45. package/src/core/client/dedupe.ts +0 -19
  46. package/src/core/client/execute.ts +0 -70
  47. package/src/core/client/hash.ts +0 -21
  48. package/src/core/client/operation-name.ts +0 -12
  49. package/src/core/client/types.ts +0 -171
  50. package/src/core/currency/cookie-config.ts +0 -13
  51. package/src/core/errors.ts +0 -67
  52. package/src/core/format.ts +0 -254
  53. package/src/core/helpers/assert-no-user-errors.ts +0 -21
  54. package/src/core/helpers/normalize-connection.ts +0 -48
  55. package/src/core/helpers/sanitize-html.ts +0 -42
  56. package/src/core/image.ts +0 -103
  57. package/src/core/index.ts +0 -180
  58. package/src/core/language/cookie-config.ts +0 -13
  59. package/src/core/middleware/auth.ts +0 -27
  60. package/src/core/middleware/bot-protection.ts +0 -140
  61. package/src/core/middleware/currency.ts +0 -27
  62. package/src/core/middleware/errors.ts +0 -86
  63. package/src/core/middleware/language.ts +0 -30
  64. package/src/core/middleware/retry.ts +0 -75
  65. package/src/core/middleware/timeout.ts +0 -61
  66. package/src/core/operations/auth.ts +0 -123
  67. package/src/core/operations/cart.ts +0 -185
  68. package/src/index.ts +0 -25
  69. package/src/react/bot-protection/bot-protection-context.ts +0 -17
  70. package/src/react/bot-protection/bot-protection-widget.tsx +0 -46
  71. package/src/react/cookies.ts +0 -89
  72. package/src/react/helpers/create-store-context.ts +0 -56
  73. package/src/react/hooks/use-auth.ts +0 -218
  74. package/src/react/hooks/use-bot-protection.ts +0 -31
  75. package/src/react/hooks/use-cart-manager.ts +0 -236
  76. package/src/react/hooks/use-currency.ts +0 -23
  77. package/src/react/hooks/use-debounced-value.ts +0 -30
  78. package/src/react/hooks/use-hydrated.ts +0 -20
  79. package/src/react/hooks/use-storefront-client.ts +0 -12
  80. package/src/react/index.ts +0 -71
  81. package/src/react/providers/currency-provider.tsx +0 -30
  82. package/src/react/providers/language-provider.tsx +0 -34
  83. package/src/react/providers/storefront-client-provider.tsx +0 -107
  84. package/src/react/providers/storefront-provider.tsx +0 -99
  85. package/src/react/server/get-storefront-client.ts +0 -60
  86. package/src/react/server/index.ts +0 -1
  87. package/src/react/stores/auth.store.ts +0 -112
  88. package/src/react/stores/cart.context.ts +0 -10
  89. package/src/react/stores/cart.store.ts +0 -254
  90. package/src/react/stores/currency.store.ts +0 -93
  91. package/src/react/stores/index.ts +0 -17
  92. package/src/react/stores/language.store.ts +0 -90
  93. package/src/react/stores/store-context.tsx +0 -103
  94. package/src/react/types/shop-config.ts +0 -22
  95. package/tsconfig.json +0 -20
  96. package/vitest.config.ts +0 -14
@@ -1,92 +0,0 @@
1
- /**
2
- * TurnstileManager — Cloudflare Turnstile bot protection provider.
3
- *
4
- * Implements BotProtectionTokenProvider via AbstractBotProtectionManager.
5
- * Invisible widget, execution-mode challenge (no user interaction).
6
- */
7
-
8
- /// <reference path="./types/turnstile.d.ts" />
9
- import { AbstractBotProtectionManager } from './abstract-manager';
10
-
11
- export class TurnstileManager extends AbstractBotProtectionManager {
12
- private callbackName = `onloadTurnstileCallback_${Math.random().toString(36).slice(2)}`;
13
-
14
- protected _waitForApi(): Promise<void> {
15
- if (typeof window !== 'undefined' && window.turnstile) {
16
- return Promise.resolve();
17
- }
18
-
19
- return new Promise<void>((resolve) => {
20
- const check = setInterval(() => {
21
- if (typeof window !== 'undefined' && window.turnstile) {
22
- clearInterval(check);
23
- resolve();
24
- }
25
- }, 50);
26
-
27
- // Safety timeout — don't poll forever
28
- setTimeout(() => {
29
- clearInterval(check);
30
- resolve(); // Resolve anyway — execute will fail gracefully
31
- }, 15000);
32
- });
33
- }
34
-
35
- protected _renderWidget(container: HTMLElement, action?: string): string {
36
- if (!window.turnstile) {
37
- throw new Error('Turnstile API not loaded');
38
- }
39
-
40
- return window.turnstile.render(container, {
41
- sitekey: this.siteKey,
42
- size: 'invisible',
43
- execution: 'execute',
44
- action,
45
- });
46
- }
47
-
48
- protected _executeChallenge(widgetId: string, action?: string): Promise<string | null> {
49
- return new Promise<string | null>((resolve) => {
50
- if (!window.turnstile) {
51
- resolve(null);
52
- return;
53
- }
54
-
55
- // Reset to get a fresh token (tokens are single-use)
56
- window.turnstile.reset(widgetId);
57
-
58
- // Remove old widget and re-render with callback
59
- window.turnstile.remove(widgetId);
60
-
61
- if (!this.container) {
62
- resolve(null);
63
- return;
64
- }
65
-
66
- this.widgetId = window.turnstile.render(this.container, {
67
- sitekey: this.siteKey,
68
- size: 'invisible',
69
- execution: 'execute',
70
- action,
71
- callback: (token: string) => {
72
- resolve(token);
73
- },
74
- 'error-callback': () => {
75
- resolve(null);
76
- },
77
- 'expired-callback': () => {
78
- resolve(null);
79
- },
80
- });
81
-
82
- // Trigger execution
83
- window.turnstile.execute(this.widgetId, { action });
84
- });
85
- }
86
-
87
- protected _destroyWidget(widgetId: string): void {
88
- if (window.turnstile) {
89
- window.turnstile.remove(widgetId);
90
- }
91
- }
92
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * EU CAPTCHA (Myra Security) global type declarations.
3
- */
4
-
5
- interface EuCaptchaRenderOptions {
6
- sitekey: string;
7
- callback?: (token: string) => void;
8
- 'error-callback'?: (error: unknown) => void;
9
- 'expired-callback'?: () => void;
10
- size?: 'normal' | 'compact' | 'invisible';
11
- action?: string;
12
- }
13
-
14
- interface EuCaptchaApi {
15
- render(container: string | HTMLElement, options: EuCaptchaRenderOptions): string;
16
- execute(widgetId: string): void;
17
- reset(widgetId: string): void;
18
- getResponse(widgetId: string): string | undefined;
19
- }
20
-
21
- declare global {
22
- interface Window {
23
- eucaptcha?: EuCaptchaApi;
24
- onloadEuCaptchaCallback?: () => void;
25
- }
26
- }
27
-
28
- export {};
@@ -1,33 +0,0 @@
1
- /**
2
- * Cloudflare Turnstile global type declarations.
3
- */
4
-
5
- interface TurnstileRenderOptions {
6
- sitekey: string;
7
- callback?: (token: string) => void;
8
- 'error-callback'?: (error: unknown) => void;
9
- 'expired-callback'?: () => void;
10
- size?: 'normal' | 'compact' | 'invisible';
11
- theme?: 'light' | 'dark' | 'auto';
12
- execution?: 'render' | 'execute';
13
- action?: string;
14
- cData?: string;
15
- }
16
-
17
- interface TurnstileApi {
18
- render(container: string | HTMLElement, options: TurnstileRenderOptions): string;
19
- execute(widgetId: string, options?: { action?: string }): void;
20
- reset(widgetId: string): void;
21
- remove(widgetId: string): void;
22
- getResponse(widgetId: string): string | undefined;
23
- isExpired(widgetId: string): boolean;
24
- }
25
-
26
- declare global {
27
- interface Window {
28
- turnstile?: TurnstileApi;
29
- onloadTurnstileCallback?: () => void;
30
- }
31
- }
32
-
33
- export {};
package/src/core/cache.ts DELETED
@@ -1,102 +0,0 @@
1
- /**
2
- * Cache strategies — functions (not constants) with override support.
3
- *
4
- * Inspired by Shopify Hydrogen's caching strategies.
5
- *
6
- * @example
7
- * ```typescript
8
- * import { cacheLong, cacheShort } from '@doswiftly/storefront-sdk';
9
- *
10
- * // Default
11
- * const data = await client.query(ShopQuery, {}, cacheLong());
12
- *
13
- * // Override with tags (Next.js revalidateTag)
14
- * const data = await client.query(ProductQuery, { slug }, cacheLong({ tags: ['product', slug] }));
15
- * ```
16
- */
17
-
18
- import type { CacheStrategy, CacheOptions } from './client/types';
19
-
20
- export interface CacheOverrides {
21
- /** Override max-age (seconds) */
22
- maxAge?: number;
23
- /** Override stale-while-revalidate (seconds) */
24
- staleWhileRevalidate?: number;
25
- /** Cache tags (for Next.js revalidateTag) */
26
- tags?: string[];
27
- }
28
-
29
- /**
30
- * No caching — every request hits the server.
31
- * Use for: Cart, Customer data, real-time inventory.
32
- */
33
- export function cacheNone(overrides?: CacheOverrides): CacheStrategy {
34
- return {
35
- maxAge: 0,
36
- mode: 'no-store',
37
- tags: overrides?.tags,
38
- };
39
- }
40
-
41
- /**
42
- * Short cache — 1s max-age, 9s stale-while-revalidate (10s total).
43
- * Use for: Product listings, collections, search results.
44
- */
45
- export function cacheShort(overrides?: CacheOverrides): CacheStrategy {
46
- return {
47
- maxAge: overrides?.maxAge ?? 1,
48
- staleWhileRevalidate: overrides?.staleWhileRevalidate ?? 9,
49
- mode: 'public',
50
- tags: overrides?.tags,
51
- };
52
- }
53
-
54
- /**
55
- * Long cache — 1h max-age, 23h stale-while-revalidate (24h total).
56
- * Use for: Static content, shop info, rarely changing data.
57
- */
58
- export function cacheLong(overrides?: CacheOverrides): CacheStrategy {
59
- return {
60
- maxAge: overrides?.maxAge ?? 3600,
61
- staleWhileRevalidate: overrides?.staleWhileRevalidate ?? 82800,
62
- mode: 'public',
63
- tags: overrides?.tags,
64
- };
65
- }
66
-
67
- /**
68
- * Private cache — 1s max-age, no sharing between users.
69
- * Use for: Personalized content, customer-specific data.
70
- */
71
- export function cachePrivate(overrides?: CacheOverrides): CacheStrategy {
72
- return {
73
- maxAge: overrides?.maxAge ?? 1,
74
- staleWhileRevalidate: overrides?.staleWhileRevalidate ?? 9,
75
- mode: 'private',
76
- tags: overrides?.tags,
77
- };
78
- }
79
-
80
- /**
81
- * Custom cache — full control over all options.
82
- */
83
- export function cacheCustom(options: CacheOptions): CacheStrategy {
84
- return { ...options };
85
- }
86
-
87
- /**
88
- * Generate Cache-Control header string from cache strategy.
89
- */
90
- export function generateCacheControlHeader(cache: CacheStrategy): string {
91
- if (cache.mode === 'no-store') {
92
- return 'no-store, no-cache, must-revalidate';
93
- }
94
-
95
- const parts: string[] = [cache.mode, `max-age=${cache.maxAge}`];
96
-
97
- if (cache.staleWhileRevalidate !== undefined) {
98
- parts.push(`stale-while-revalidate=${cache.staleWhileRevalidate}`);
99
- }
100
-
101
- return parts.join(', ');
102
- }
@@ -1,150 +0,0 @@
1
- /**
2
- * CartClient — plain async API for cart operations (no React, no React Query).
3
- *
4
- * Wraps StorefrontClient.mutate/query with typed operations.
5
- * Auto-throws on userErrors via assertNoUserErrors.
6
- *
7
- * @example
8
- * ```typescript
9
- * const cartClient = new CartClient(storefrontClient);
10
- *
11
- * const cart = await cartClient.create();
12
- * const updated = await cartClient.addItem(cart.id, [
13
- * { merchandiseId: 'variant-123', quantity: 1 }
14
- * ]);
15
- * ```
16
- */
17
-
18
- import type { StorefrontClient } from '../client/types';
19
- import type {
20
- Cart,
21
- CartCreateInput,
22
- CartLineInput,
23
- CartLineUpdateInput,
24
- CartBuyerIdentityInput,
25
- } from './types';
26
- import { assertNoUserErrors } from '../helpers/assert-no-user-errors';
27
- import {
28
- CART_QUERY,
29
- CART_CREATE,
30
- CART_LINES_ADD,
31
- CART_LINES_UPDATE,
32
- CART_LINES_REMOVE,
33
- CART_DISCOUNT_CODES_UPDATE,
34
- CART_NOTE_UPDATE,
35
- CART_BUYER_IDENTITY_UPDATE,
36
- } from '../operations/cart';
37
-
38
- // ---------------------------------------------------------------------------
39
- // Response types (internal — match the GraphQL mutation shapes)
40
- // ---------------------------------------------------------------------------
41
-
42
- interface CartMutationResult {
43
- cart: Cart | null;
44
- userErrors: Array<{ message: string; field?: string[]; code?: string }>;
45
- }
46
-
47
- interface CartQueryResult {
48
- cart: Cart | null;
49
- }
50
-
51
- export class CartClient {
52
- constructor(private readonly client: StorefrontClient) {}
53
-
54
- /**
55
- * Fetch existing cart by ID.
56
- * Returns null if cart doesn't exist or has expired.
57
- */
58
- async get(cartId: string): Promise<Cart | null> {
59
- const data = await this.client.query<CartQueryResult>(
60
- CART_QUERY,
61
- { id: cartId },
62
- );
63
- return data.cart;
64
- }
65
-
66
- /**
67
- * Create a new cart, optionally with initial lines.
68
- */
69
- async create(input?: CartCreateInput): Promise<Cart> {
70
- const data = await this.client.mutate<{ cartCreate: CartMutationResult }>(
71
- CART_CREATE,
72
- { input: input ?? {} },
73
- );
74
- assertNoUserErrors(data.cartCreate);
75
- return data.cartCreate.cart!;
76
- }
77
-
78
- /**
79
- * Add line items to an existing cart.
80
- */
81
- async addItems(cartId: string, lines: CartLineInput[]): Promise<Cart> {
82
- const data = await this.client.mutate<{ cartLinesAdd: CartMutationResult }>(
83
- CART_LINES_ADD,
84
- { cartId, lines },
85
- );
86
- assertNoUserErrors(data.cartLinesAdd);
87
- return data.cartLinesAdd.cart!;
88
- }
89
-
90
- /**
91
- * Update line items (quantity, attributes).
92
- */
93
- async updateItems(cartId: string, lines: CartLineUpdateInput[]): Promise<Cart> {
94
- const data = await this.client.mutate<{ cartLinesUpdate: CartMutationResult }>(
95
- CART_LINES_UPDATE,
96
- { cartId, lines },
97
- );
98
- assertNoUserErrors(data.cartLinesUpdate);
99
- return data.cartLinesUpdate.cart!;
100
- }
101
-
102
- /**
103
- * Remove line items by their line IDs.
104
- */
105
- async removeItems(cartId: string, lineIds: string[]): Promise<Cart> {
106
- const data = await this.client.mutate<{ cartLinesRemove: CartMutationResult }>(
107
- CART_LINES_REMOVE,
108
- { cartId, lineIds },
109
- );
110
- assertNoUserErrors(data.cartLinesRemove);
111
- return data.cartLinesRemove.cart!;
112
- }
113
-
114
- /**
115
- * Update discount codes (replaces all existing codes).
116
- * Pass empty array to clear discounts.
117
- */
118
- async updateDiscountCodes(cartId: string, discountCodes: string[]): Promise<Cart> {
119
- const data = await this.client.mutate<{ cartDiscountCodesUpdate: CartMutationResult }>(
120
- CART_DISCOUNT_CODES_UPDATE,
121
- { cartId, discountCodes },
122
- );
123
- assertNoUserErrors(data.cartDiscountCodesUpdate);
124
- return data.cartDiscountCodesUpdate.cart!;
125
- }
126
-
127
- /**
128
- * Update cart note / gift message.
129
- */
130
- async updateNote(cartId: string, note: string): Promise<Cart> {
131
- const data = await this.client.mutate<{ cartNoteUpdate: CartMutationResult }>(
132
- CART_NOTE_UPDATE,
133
- { cartId, note },
134
- );
135
- assertNoUserErrors(data.cartNoteUpdate);
136
- return data.cartNoteUpdate.cart!;
137
- }
138
-
139
- /**
140
- * Update buyer identity (email, phone, country, customer link).
141
- */
142
- async updateBuyerIdentity(cartId: string, buyerIdentity: CartBuyerIdentityInput): Promise<Cart> {
143
- const data = await this.client.mutate<{ cartBuyerIdentityUpdate: CartMutationResult }>(
144
- CART_BUYER_IDENTITY_UPDATE,
145
- { cartId, buyerIdentity },
146
- );
147
- assertNoUserErrors(data.cartBuyerIdentityUpdate);
148
- return data.cartBuyerIdentityUpdate.cart!;
149
- }
150
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * Cart cookie configuration — platform contract.
3
- *
4
- * Used by:
5
- * - SDK cart store (client-side cookie read/write for cartId)
6
- * - Server-side cart prefetching (SSR cart badge, middleware)
7
- * - proxy.ts (edge cart ID detection)
8
- *
9
- * Single cookie for cart ID persistence. Value is a plain cart ID string
10
- * (not JSON) — server can read it directly via cookies().get('cart-id').
11
- */
12
- export const CART_COOKIE_NAME = 'cart-id';
13
- export const CART_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days
@@ -1,104 +0,0 @@
1
- /**
2
- * Cart types — manual (no codegen).
3
- *
4
- * These match the backend storefront-graphql Cart type.
5
- */
6
-
7
- export interface Money {
8
- amount: string;
9
- currencyCode: string;
10
- }
11
-
12
- export interface CartCost {
13
- totalAmount: Money;
14
- subtotalAmount: Money;
15
- totalTaxAmount: Money | null;
16
- totalDutyAmount: Money | null;
17
- }
18
-
19
- export interface CartLineCost {
20
- totalAmount: Money;
21
- amountPerQuantity: Money;
22
- compareAtAmountPerQuantity: Money | null;
23
- }
24
-
25
- export interface CartLineMerchandise {
26
- id: string;
27
- title: string;
28
- sku: string | null;
29
- image: { url: string; altText: string | null } | null;
30
- price: Money;
31
- compareAtPrice: Money | null;
32
- }
33
-
34
- export interface CartLine {
35
- id: string;
36
- quantity: number;
37
- merchandise: CartLineMerchandise;
38
- cost: CartLineCost;
39
- attributes: Array<{ key: string; value: string }>;
40
- productId: string;
41
- productTitle: string;
42
- productHandle: string;
43
- }
44
-
45
- export interface CartDiscountCode {
46
- code: string;
47
- applicable: boolean;
48
- }
49
-
50
- export interface CartDiscountAllocation {
51
- discountedAmount: Money;
52
- }
53
-
54
- export interface CartBuyerIdentity {
55
- email: string | null;
56
- phone: string | null;
57
- countryCode: string | null;
58
- }
59
-
60
- export interface Cart {
61
- id: string;
62
- checkoutUrl: string | null;
63
- totalQuantity: number;
64
- note: string | null;
65
- createdAt: string;
66
- updatedAt: string;
67
- cost: CartCost;
68
- lines: { edges: Array<{ node: CartLine }> };
69
- buyerIdentity: CartBuyerIdentity | null;
70
- discountCodes: CartDiscountCode[];
71
- discountAllocations: CartDiscountAllocation[];
72
- attributes: Array<{ key: string; value: string }>;
73
- }
74
-
75
- // ---------------------------------------------------------------------------
76
- // Input types
77
- // ---------------------------------------------------------------------------
78
-
79
- export interface CartLineInput {
80
- merchandiseId: string;
81
- quantity?: number;
82
- attributes?: Array<{ key: string; value: string }>;
83
- }
84
-
85
- export interface CartLineUpdateInput {
86
- id: string;
87
- quantity: number;
88
- attributes?: Array<{ key: string; value: string }>;
89
- }
90
-
91
- export interface CartCreateInput {
92
- lines?: CartLineInput[];
93
- buyerIdentity?: CartBuyerIdentityInput;
94
- discountCodes?: string[];
95
- note?: string;
96
- attributes?: Array<{ key: string; value: string }>;
97
- }
98
-
99
- export interface CartBuyerIdentityInput {
100
- email?: string;
101
- phone?: string;
102
- countryCode?: string;
103
- customerId?: string;
104
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Compose middleware chain using reduceRight (Hydrogen pattern).
3
- *
4
- * Middleware execute in order: first registered runs first.
5
- * Each can modify request, modify response, retry, or short-circuit.
6
- */
7
-
8
- import type { Middleware, ExecuteFn } from './types';
9
-
10
- export function compose(middlewares: Middleware[], execute: ExecuteFn): ExecuteFn {
11
- return middlewares.reduceRight<ExecuteFn>(
12
- (next, mw) => (req) => mw(req, next),
13
- execute,
14
- );
15
- }
@@ -1,129 +0,0 @@
1
- /**
2
- * createStorefrontClient — transport factory (Hydrogen pattern).
3
- *
4
- * Creates a framework-agnostic GraphQL client with:
5
- * - Middleware pipeline (composable, lazy)
6
- * - Request deduplication (same-tick queries)
7
- * - TypedDocumentString support (Saleor/client-preset)
8
- * - Plain string queries (for custom operations)
9
- * - Debug mode
10
- *
11
- * 0 runtime dependencies.
12
- */
13
-
14
- import type {
15
- StorefrontClientConfig,
16
- StorefrontClient,
17
- Middleware,
18
- CacheStrategy,
19
- TypedDocumentString,
20
- ExecuteFn,
21
- } from './types';
22
- import { createExecute } from './execute';
23
- import { compose } from './compose';
24
- import { dedupe } from './dedupe';
25
- import { hashQuery } from './hash';
26
- import { getOperationName } from './operation-name';
27
-
28
- export function createStorefrontClient(config: StorefrontClientConfig): StorefrontClient {
29
- const {
30
- apiUrl,
31
- shopSlug,
32
- defaultHeaders = {},
33
- middleware: initialMiddleware = [],
34
- fetch: customFetch = globalThis.fetch,
35
- debug = false,
36
- } = config;
37
-
38
- const endpoint = `${apiUrl.replace(/\/$/, '')}/storefront/graphql`;
39
-
40
- // Mutable middleware list — use() adds to it, invalidates compiled pipeline
41
- const middlewares: Middleware[] = [...initialMiddleware];
42
- let compiledPipeline: ExecuteFn | null = null;
43
-
44
- // Create the innermost execute function (native fetch)
45
- const innerExecute = createExecute({ endpoint, fetch: customFetch, debug });
46
-
47
- /**
48
- * Get or build the compiled middleware pipeline (lazy).
49
- * Rebuilt on first call and after use() is called.
50
- */
51
- function getPipeline(): ExecuteFn {
52
- if (!compiledPipeline) {
53
- compiledPipeline = middlewares.length > 0
54
- ? compose(middlewares, innerExecute)
55
- : innerExecute;
56
- }
57
- return compiledPipeline;
58
- }
59
-
60
- /**
61
- * Resolve query string from TypedDocumentString or plain string.
62
- */
63
- function resolveQuery(document: TypedDocumentString | string): string {
64
- return typeof document === 'string' ? document : document.toString();
65
- }
66
-
67
- /**
68
- * Core request execution — shared by query() and mutate().
69
- */
70
- async function request<T>(
71
- document: TypedDocumentString<T, unknown> | string,
72
- variables?: Record<string, unknown>,
73
- isMutation: boolean = false,
74
- cache?: CacheStrategy,
75
- ): Promise<T> {
76
- const query = resolveQuery(document);
77
- const operationName = getOperationName(query);
78
- const pipeline = getPipeline();
79
-
80
- const headers: Record<string, string> = {
81
- 'X-Shop-Slug': shopSlug,
82
- ...defaultHeaders,
83
- };
84
-
85
- if (operationName !== 'anonymous') {
86
- headers['X-Operation-Name'] = operationName;
87
- }
88
-
89
- const graphqlRequest = {
90
- query,
91
- variables,
92
- operationName,
93
- headers,
94
- isMutation,
95
- cache,
96
- };
97
-
98
- // Dedupe queries (not mutations) in the same tick
99
- const executeFn = () => pipeline(graphqlRequest);
100
-
101
- const response = isMutation
102
- ? await executeFn()
103
- : await dedupe(hashQuery(query, variables), executeFn);
104
-
105
- return response.data as T;
106
- }
107
-
108
- return {
109
- query<T, V>(
110
- document: TypedDocumentString<T, V> | string,
111
- variables?: V,
112
- cache?: CacheStrategy,
113
- ): Promise<T> {
114
- return request<T>(document as TypedDocumentString<T, unknown>, variables as Record<string, unknown>, false, cache);
115
- },
116
-
117
- mutate<T, V>(
118
- document: TypedDocumentString<T, V> | string,
119
- variables?: V,
120
- ): Promise<T> {
121
- return request<T>(document as TypedDocumentString<T, unknown>, variables as Record<string, unknown>, true);
122
- },
123
-
124
- use(middleware: Middleware): void {
125
- middlewares.push(middleware);
126
- compiledPipeline = null; // Invalidate — rebuild on next request
127
- },
128
- };
129
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * Request deduplication — identical queries in the same tick share one fetch.
3
- *
4
- * Key = hashQuery(query, variables). Only applies to queries, not mutations.
5
- */
6
-
7
- const pending = new Map<string, Promise<unknown>>();
8
-
9
- export function dedupe<T>(key: string, fn: () => Promise<T>): Promise<T> {
10
- const existing = pending.get(key);
11
- if (existing) return existing as Promise<T>;
12
-
13
- const promise = fn().finally(() => {
14
- pending.delete(key);
15
- });
16
-
17
- pending.set(key, promise);
18
- return promise;
19
- }