@commerce-blocks/sdk 1.2.1 → 2.0.0-alpha.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 (4) hide show
  1. package/README.md +226 -93
  2. package/dist/index.d.ts +231 -119
  3. package/dist/index.js +1224 -1038
  4. package/package.json +7 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @commerce-blocks/sdk
2
2
 
3
- ES module SDK for Shopify storefronts with Layers API integration.
3
+ ES module SDK powered by Layers API with optional Shopify Storefront enrichment.
4
4
 
5
5
  ## Installation
6
6
 
@@ -22,7 +22,7 @@ const result = createSdk({
22
22
  ],
23
23
  facets: ['options.color', 'options.size', 'vendor'],
24
24
 
25
- // Opt in to Shopify Storefront API for richer product data
25
+ // Opt in to Storefront API for additional product data
26
26
  // enableStorefront: true,
27
27
  // shop: 'your-store.myshopify.com',
28
28
  // storefrontPublicToken: 'your-storefront-token',
@@ -38,9 +38,20 @@ if (result.error) {
38
38
 
39
39
  ## Configuration
40
40
 
41
- ### Shopify (optional)
41
+ ### Layers
42
42
 
43
- Storefront API is opt-in. Enable it for richer product data (metafields, full variant info, collection/page metadata).
43
+ | Option | Type | Required | Description |
44
+ | ------------------- | ------------- | -------- | ---------------------------------------------------- |
45
+ | `layersPublicToken` | `string` | Yes | Layers API public token |
46
+ | `sorts` | `Sort[]` | Yes | Sort options (`{ label, code }`) |
47
+ | `facets` | `string[]` | Yes | Facet fields for filtering |
48
+ | `attributes` | `string[]` | No | Product attributes to fetch |
49
+ | `layersBaseUrl` | `string` | No | Custom API URL |
50
+ | `fetch` | `CustomFetch` | No | Custom fetch implementation (SSR, testing, proxying) |
51
+
52
+ ### Storefront (optional)
53
+
54
+ Storefront API is opt-in. Enable it for collection/page metadata and Shopify-specific fields (metafields, variant detail).
44
55
 
45
56
  | Option | Type | Required | Description |
46
57
  | ----------------------- | ------------- | ----------------------------- | ---------------------------------------------------- |
@@ -50,37 +61,40 @@ Storefront API is opt-in. Enable it for richer product data (metafields, full va
50
61
  | `storefrontApiVersion` | `string` | No | API version (default: `2025-01`) |
51
62
  | `fetch` | `CustomFetch` | No | Custom fetch implementation (SSR, testing, proxying) |
52
63
 
53
- ### Layers
64
+ Layers API supports identity tracking for personalization. Pass identity fields via request context:
54
65
 
55
- | Option | Type | Required | Description |
56
- | ------------------- | ------------- | -------- | ---------------------------------------------------- |
57
- | `layersPublicToken` | `string` | Yes | Layers API public token |
58
- | `sorts` | `Sort[]` | Yes | Sort options (`{ label, code }`) |
59
- | `facets` | `string[]` | Yes | Facet fields for filtering |
60
- | `attributes` | `string[]` | No | Product attributes to fetch |
61
- | `layersBaseUrl` | `string` | No | Custom API URL |
62
- | `fetch` | `CustomFetch` | No | Custom fetch implementation (SSR, testing, proxying) |
66
+ ```typescript
67
+ // Identity fields available in browse/search contexts
68
+ interface LayersIdentity {
69
+ deviceId?: string // Device identifier
70
+ sessionId?: string // Session identifier
71
+ customerId?: string // Customer identifier
72
+ }
73
+ ```
63
74
 
64
75
  ### Product
65
76
 
66
- | Option | Type | Description |
67
- | ------------------- | ------------------------------ | ----------------------------- |
68
- | `currencyCode` | `string` | Currency for price formatting |
69
- | `formatPrice` | `(amount, currency) => string` | Custom price formatter |
70
- | `swatches` | `Swatch[]` | Color swatch definitions |
71
- | `options` | `string[]` | Product options to expose |
72
- | `productMetafields` | `{ namespace, key }[]` | Product metafields to fetch |
73
- | `variantMetafields` | `{ namespace, key }[]` | Variant metafields to fetch |
77
+ | Option | Type | Description |
78
+ | ---------------------- | ------------------------------ | ------------------------------ |
79
+ | `currencyCode` | `string` | Currency for price formatting |
80
+ | `formatPrice` | `(amount, currency) => string` | Custom price formatter |
81
+ | `swatches` | `Swatch[]` | Color swatch definitions |
82
+ | `options` | `string[]` | Product options to expose |
83
+ | `productMetafields` | `{ namespace, key }[]` | Product metafields to fetch |
84
+ | `variantMetafields` | `{ namespace, key }[]` | Variant metafields to fetch |
85
+ | `collectionMetafields` | `{ namespace, key }[]` | Collection metafields to fetch |
86
+ | `pageMetafields` | `{ namespace, key }[]` | Page metafields to fetch |
74
87
 
75
88
  ### Extensibility
76
89
 
77
- | Option | Type | Description |
78
- | ------------------ | ------------------------------- | ---------------------------------- |
79
- | `extendProduct` | `({ base, raw, shopify }) => P` | Transform products after hydration |
80
- | `extendCollection` | `(result, raw) => result` | Transform collection results |
81
- | `extendSearch` | `(result, raw) => result` | Transform search results |
82
- | `transformFilters` | `(filters) => FilterGroup` | Custom filter transformation |
83
- | `filterMap` | `FilterMap` | URL-friendly filter key mapping |
90
+ | Option | Type | Description |
91
+ | ------------------ | ---------------------------------- | ---------------------------------- |
92
+ | `extendProduct` | `({ base, raw, storefront }) => P` | Transform products after hydration |
93
+ | `extendCollection` | `(result, raw) => result` | Transform collection results |
94
+ | `extendSearch` | `(result, raw) => result` | Transform search results |
95
+ | `extendBlock` | `(result, raw) => result` | Transform blocks results |
96
+ | `transformFilters` | `(filters) => FilterGroup` | Custom filter transformation |
97
+ | `filterMap` | `FilterMap` | URL-friendly filter key mapping |
84
98
 
85
99
  Once configured, extenders and transformers are applied automatically. You pass simple inputs and receive transformed outputs—no manual transformation needed on each call.
86
100
 
@@ -88,7 +102,7 @@ Once configured, extenders and transformers are applied automatically. You pass
88
102
  // Configure once at initialization
89
103
  const sdk = createSdk({
90
104
  // ... base config
91
- extendProduct: ({ base, raw, shopify }) => ({
105
+ extendProduct: ({ base, raw, storefront }) => ({
92
106
  ...base,
93
107
  isNew: raw.tags?.includes('new') ?? false,
94
108
  rating: raw.calculated?.average_rating,
@@ -113,6 +127,13 @@ await collection.execute({ filters: { color: 'Red' } }) // filterMap applied
113
127
  | `cacheMaxEntries` | `number` | Max query entries in cache |
114
128
  | `cacheTtl` | `number` | TTL in milliseconds |
115
129
 
130
+ ### Data Hydration
131
+
132
+ | Option | Type | Description |
133
+ | -------------------- | --------------------------------------- | ----------------------------------------- |
134
+ | `initialData` | `{ products?, queries?, collections? }` | Pre-populate cache at init |
135
+ | `restoreFromStorage` | `boolean` | Auto-restore from storage (default: true) |
136
+
116
137
  ## SDK Methods
117
138
 
118
139
  ### `sdk.collection()` - Browse Collections
@@ -162,13 +183,18 @@ collection.dispose()
162
183
 
163
184
  **Execute parameters:**
164
185
 
165
- | Parameter | Type | Description |
166
- | --------------- | ------------- | ------------------------------- |
167
- | `page` | `number` | Page number (default: 1) |
168
- | `limit` | `number` | Products per page (default: 24) |
169
- | `sortOrderCode` | `string` | Sort option code |
170
- | `filters` | `unknown` | Filter criteria |
171
- | `signal` | `AbortSignal` | External abort signal |
186
+ | Parameter | Type | Description |
187
+ | ---------------- | ------------------------- | ------------------------------------- |
188
+ | `page` | `number` | Page number (default: 1) |
189
+ | `limit` | `number` | Products per page (default: 24) |
190
+ | `sortOrderCode` | `string` | Sort option code |
191
+ | `filters` | `unknown` | Filter criteria |
192
+ | `signal` | `AbortSignal` | External abort signal |
193
+ | `includeMeta` | `boolean` | Fetch collection metadata |
194
+ | `includeFilters` | `boolean` | Include filter counts in response |
195
+ | `dynamicLinking` | `Record<string, unknown>` | Custom dynamic linking parameters |
196
+ | `params` | `Record<string, unknown>` | Additional request parameters |
197
+ | `transformBody` | `(body) => body` | Custom request body mutation function |
172
198
 
173
199
  ### `sdk.blocks()` - Product Recommendations
174
200
 
@@ -220,14 +246,17 @@ blocks.dispose()
220
246
 
221
247
  **Execute parameters:**
222
248
 
223
- | Parameter | Type | Description |
224
- | ---------------------- | ----------------------- | ------------------------------- |
225
- | `page` | `number` | Page number (default: 1) |
226
- | `limit` | `number` | Products per page (default: 24) |
227
- | `filters` | `unknown` | Filter criteria |
228
- | `signal` | `AbortSignal` | External abort signal |
229
- | `discountEntitlements` | `DiscountEntitlement[]` | Discount entitlements to apply |
230
- | `context` | `BlocksContext` | Cart, geo, and custom context |
249
+ | Parameter | Type | Description |
250
+ | ---------------------- | ------------------------- | ------------------------------------- |
251
+ | `page` | `number` | Page number (default: 1) |
252
+ | `limit` | `number` | Products per page (default: 24) |
253
+ | `filters` | `unknown` | Filter criteria |
254
+ | `signal` | `AbortSignal` | External abort signal |
255
+ | `discountEntitlements` | `DiscountEntitlement[]` | Discount entitlements to apply |
256
+ | `context` | `BlocksContext` | Cart, geo, and custom context |
257
+ | `dynamicLinking` | `Record<string, unknown>` | Custom dynamic linking parameters |
258
+ | `params` | `Record<string, unknown>` | Additional request parameters |
259
+ | `transformBody` | `(body) => body` | Custom request body mutation function |
231
260
 
232
261
  **`BlocksContext`:**
233
262
 
@@ -237,6 +266,23 @@ blocks.dispose()
237
266
  | `geo` | `{ country?, province?, city? }` | Geographic context |
238
267
  | `custom` | `Record<string, unknown>` | Custom context data |
239
268
 
269
+ **`DiscountEntitlement`:**
270
+
271
+ ```typescript
272
+ interface DiscountEntitlement {
273
+ entitled: {
274
+ all?: boolean // Apply to all products
275
+ products?: string[] // Product IDs
276
+ variants?: (string | number)[] // Variant IDs
277
+ collections?: string[] // Collection handles
278
+ }
279
+ discount: {
280
+ type: 'PERCENTAGE' | 'FIXED_AMOUNT'
281
+ value: number
282
+ }
283
+ }
284
+ ```
285
+
240
286
  ### `sdk.autocomplete()` - Predictive Search
241
287
 
242
288
  Creates a standalone autocomplete controller with debounced search and local caching. Only full words (completed by a trailing space) are cached — partial input filters cached results client-side.
@@ -316,14 +362,26 @@ search.dispose()
316
362
 
317
363
  **Search parameters:**
318
364
 
319
- | Parameter | Type | Description |
320
- | --------- | -------------- | ------------------------------- |
321
- | `query` | `string` | Search query |
322
- | `page` | `number` | Page number (default: 1) |
323
- | `limit` | `number` | Products per page (default: 24) |
324
- | `filters` | `unknown` | Filter criteria |
325
- | `tuning` | `LayersTuning` | Search tuning parameters |
326
- | `signal` | `AbortSignal` | External abort signal |
365
+ | Parameter | Type | Description |
366
+ | ---------------- | ------------------------- | ------------------------------------- |
367
+ | `query` | `string` | Search query |
368
+ | `page` | `number` | Page number (default: 1) |
369
+ | `limit` | `number` | Products per page (default: 24) |
370
+ | `filters` | `unknown` | Filter criteria |
371
+ | `tuning` | `LayersTuning` | Search tuning parameters |
372
+ | `signal` | `AbortSignal` | External abort signal |
373
+ | `dynamicLinking` | `Record<string, unknown>` | Custom dynamic linking parameters |
374
+ | `params` | `Record<string, unknown>` | Additional request parameters |
375
+ | `transformBody` | `(body) => body` | Custom request body mutation function |
376
+
377
+ **`LayersTuning`:**
378
+
379
+ | Property | Type | Description |
380
+ | ---------------- | -------- | ------------------------------------------- |
381
+ | `textualWeight` | `number` | Weight for text-based matching (0-1) |
382
+ | `visualWeight` | `number` | Weight for visual similarity matching (0-1) |
383
+ | `multipleFactor` | `number` | Factor for multiple keyword matching |
384
+ | `minimumMatch` | `number` | Minimum match threshold |
327
385
 
328
386
  ### `sdk.uploadImage()` - Upload Image for Search
329
387
 
@@ -399,7 +457,7 @@ effect(() => {
399
457
 
400
458
  | Parameter | Type | Required | Description |
401
459
  | --------------------- | ------------- | -------- | ---------------------------------------- |
402
- | `ids` | `string[]` | Yes | Shopify Product GIDs |
460
+ | `ids` | `string[]` | Yes | Product GIDs |
403
461
  | `meta.collection` | `string` | No | Collection handle to fetch metadata |
404
462
  | `meta.page` | `string` | No | Page handle to fetch metadata |
405
463
  | `meta.includeFilters` | `boolean` | No | Include available filters for collection |
@@ -441,8 +499,11 @@ interface CollectionResult {
441
499
  totalResults: number
442
500
  totalPages: number
443
501
  page: number
502
+ resultsPerPage?: number
444
503
  facets: Record<string, Record<string, number>>
445
- collection?: ShopifyCollection
504
+ facetRanges?: Record<string, { min: number; max: number }>
505
+ attributionToken: string
506
+ collection?: StorefrontCollection
446
507
  }
447
508
 
448
509
  interface SearchResult {
@@ -450,7 +511,10 @@ interface SearchResult {
450
511
  totalResults: number
451
512
  totalPages: number
452
513
  page: number
514
+ resultsPerPage?: number
453
515
  facets: Record<string, Record<string, number>>
516
+ facetRanges?: Record<string, { min: number; max: number }>
517
+ attributionToken: string
454
518
  }
455
519
 
456
520
  interface BlocksResult {
@@ -458,14 +522,17 @@ interface BlocksResult {
458
522
  totalResults: number
459
523
  totalPages: number
460
524
  page: number
525
+ resultsPerPage?: number
461
526
  facets: Record<string, Record<string, number>>
527
+ facetRanges?: Record<string, { min: number; max: number }>
528
+ attributionToken: string
462
529
  block?: BlocksInfo // { id, title, anchor_type, strategy_type, strategy_key }
463
530
  }
464
531
 
465
532
  interface StorefrontResult {
466
533
  products: Product[]
467
- collection?: ShopifyCollection
468
- page?: ShopifyPage
534
+ collection?: StorefrontCollection
535
+ page?: StorefrontPage
469
536
  }
470
537
  ```
471
538
 
@@ -484,7 +551,7 @@ if (result.error) {
484
551
  break
485
552
  case 'ApiError':
486
553
  // Server errors, rate limits
487
- console.log(result.error.source) // 'layers' | 'shopify'
554
+ console.log(result.error.source) // 'layers' | 'storefront'
488
555
  console.log(result.error.status) // HTTP status code
489
556
  break
490
557
  case 'ValidationError':
@@ -518,7 +585,7 @@ if (result.error) {
518
585
  | Field | Type | Description |
519
586
  | -------------- | -------------- | -------------------------------------------------- |
520
587
  | `code` | `ApiErrorCode` | `NOT_FOUND`, `RATE_LIMITED`, `GRAPHQL_ERROR`, etc. |
521
- | `source` | `ApiSource` | `'layers'` or `'shopify'` |
588
+ | `source` | `ApiSource` | `'layers'` or `'storefront'` |
522
589
  | `status` | `number?` | HTTP status code |
523
590
  | `retryable` | `boolean` | Whether the request can be retried |
524
591
  | `retryAfterMs` | `number?` | Suggested retry delay |
@@ -539,6 +606,8 @@ if (result.error) {
539
606
 
540
607
  ### Error Helpers
541
608
 
609
+ Always check `isRetryable()` before calling `getRetryDelay()`. `getRetryDelay()` returns `undefined` for non-retryable errors — it's not meant to be used standalone.
610
+
542
611
  ```typescript
543
612
  import { isRetryable, getRetryDelay } from '@commerce-blocks/sdk'
544
613
 
@@ -569,7 +638,7 @@ const priceFilter = filter(and(gte('price', 50), lte('price', 200)))
569
638
 
570
639
  // Use in query
571
640
  await collection.execute({
572
- filters: multiFilter.filter_group,
641
+ filters: multiFilter,
573
642
  })
574
643
  ```
575
644
 
@@ -608,57 +677,80 @@ await collection.execute({
608
677
  })
609
678
  ```
610
679
 
611
- ## Hooks
680
+ ## Product Card
612
681
 
613
- ### `useProductCard()` - Product Card Helper
682
+ ### `createProductCard()` - Reactive Product Card Controller
614
683
 
615
- Utility for building product cards with variant selection and availability logic.
684
+ Creates a reactive controller for product cards with variant selection and availability logic. All derived values are computed signals that auto-update when inputs change.
616
685
 
617
686
  ```typescript
618
- import { useProductCard } from '@commerce-blocks/sdk'
687
+ import { createProductCard, effect } from '@commerce-blocks/sdk'
619
688
 
620
- const card = useProductCard({
689
+ const card = createProductCard({
621
690
  product,
622
- selectedOptions: [{ name: 'Size', value: 'M' }],
623
- breakAwayOptions: [{ name: 'Color', value: 'Red' }],
691
+ selectedOptions: [{ name: 'Size', value: '7' }],
692
+ breakoutOptions: [{ name: 'Stone', value: 'Ruby' }],
624
693
  })
625
694
 
626
- // Access computed data
627
- card.variants // Variants filtered by breakAwayOptions
628
- card.selectedVariant // Currently selected variant
629
- card.options // Available options (excludes breakaway option names)
630
- card.images // Variant image or product images
631
- card.carouselIndex // Image index for carousel
695
+ // Subscribe to reactive state
696
+ effect(() => {
697
+ console.log('Selected variant:', card.selectedVariant.value)
698
+ console.log('Available options:', card.options.value)
699
+ })
700
+
701
+ // Access reactive signals
702
+ card.variants.value // Variants filtered by breakoutOptions
703
+ card.selectedVariant.value // Currently selected variant
704
+ card.options.value // Available options (excludes breakout option names)
705
+ card.images.value // Variant image or product images
706
+ card.carouselIndex.value // Image index for carousel
707
+
708
+ // Mutate state via actions
709
+ card.selectOption('Size', 'L') // Select a single option
710
+ card.setSelectedOptions([{ name: 'Size', value: 'L' }]) // Merge options by name
711
+ card.setBreakoutOptions([{ name: 'Stone', value: 'Emerald' }]) // Change breakout filter
632
712
 
633
- // Helper methods
713
+ // Query methods (pure)
634
714
  card.getOptionValues('Size') // ['S', 'M', 'L']
635
715
  card.getSwatches('Color') // Swatch definitions
636
716
  card.isOptionAvailable('Size', 'L') // Check if selecting 'L' results in available variant
637
717
  card.getVariantByOptions([{ name: 'Size', value: 'L' }])
718
+
719
+ // Cleanup
720
+ card.dispose()
638
721
  ```
639
722
 
640
723
  **Parameters:**
641
724
 
642
- | Parameter | Type | Required | Description |
643
- | ------------------ | ----------------- | -------- | ------------------------------------------------------------- |
644
- | `product` | `Product` | Yes | Product to display |
645
- | `selectedOptions` | `ProductOption[]` | No | Currently selected options |
646
- | `breakAwayOptions` | `ProductOption[]` | No | Options to filter visible variants (e.g., pre-selected color) |
725
+ | Parameter | Type | Required | Description |
726
+ | ----------------- | ----------------- | -------- | -------------------------------------------------------- |
727
+ | `product` | `Product` | Yes | Product to display |
728
+ | `selectedOptions` | `ProductOption[]` | No | Initially selected options |
729
+ | `breakoutOptions` | `ProductOption[]` | No | Options to filter variants (auto-set from variant tiles) |
730
+
731
+ **Reactive State (ReadonlySignal):**
732
+
733
+ | Property | Type | Description |
734
+ | ----------------- | ---------------------------------------- | --------------------------------------- |
735
+ | `product` | `Product` | Original product (static) |
736
+ | `variants` | `ReadonlySignal<ProductVariant[]>` | Variants matching breakoutOptions |
737
+ | `selectedVariant` | `ReadonlySignal<ProductVariant \| null>` | Variant matching all selected options |
738
+ | `selectedOptions` | `ReadonlySignal<ProductOption[]>` | Combined breakout + selected options |
739
+ | `options` | `ReadonlySignal<RichProductOption[]>` | Available options from visible variants |
740
+ | `optionNames` | `ReadonlySignal<string[]>` | Option names (excludes breakout) |
741
+ | `images` | `ReadonlySignal<Image[]>` | Variant image or product images |
742
+ | `carouselIndex` | `ReadonlySignal<number>` | Index of selected variant's image |
647
743
 
648
- **Returns:**
744
+ **Actions:**
649
745
 
650
- | Property | Type | Description |
651
- | ----------------- | ------------------------ | --------------------------------------- |
652
- | `product` | `Product` | Original product |
653
- | `variants` | `ProductVariant[]` | Variants matching breakAwayOptions |
654
- | `selectedVariant` | `ProductVariant \| null` | Variant matching all selected options |
655
- | `selectedOptions` | `ProductOption[]` | Combined breakaway + selected options |
656
- | `options` | `RichProductOption[]` | Available options from visible variants |
657
- | `optionNames` | `string[]` | Option names (excludes breakaway) |
658
- | `images` | `Image[]` | Variant image or product images |
659
- | `carouselIndex` | `number` | Index of selected variant's image |
746
+ | Method | Description |
747
+ | ----------------------------- | ------------------------------- |
748
+ | `selectOption(name, value)` | Select a single option by name |
749
+ | `setSelectedOptions(options)` | Merge options by name |
750
+ | `setBreakoutOptions(options)` | Replace breakout filter options |
751
+ | `dispose()` | Cleanup controller |
660
752
 
661
- **Helper Methods:**
753
+ **Query Methods:**
662
754
 
663
755
  | Method | Returns | Description |
664
756
  | -------------------------------- | ------------------------ | ------------------------------------------------------ |
@@ -703,13 +795,54 @@ store.queries.invalidate('browse:*') // invalidate by pattern
703
795
  // Collection metadata
704
796
  const meta = store.collections.get('shirts')
705
797
 
798
+ // Page metadata
799
+ const page = store.pages.get('about')
800
+ store.pages.set('about', { title: 'About Us', body: '...' })
801
+ store.pages.delete('about')
802
+
706
803
  // Persistence
707
- store.persist() // save to localStorage
708
- store.restore() // restore from localStorage
804
+ store.persist() // save to storage
805
+ store.restore() // restore from storage
709
806
  store.clear() // clear all caches
710
807
 
711
808
  // Stats
712
- console.log(store.stats) // { products, queries, collections }
809
+ console.log(store.stats) // { products, queries, collections, pages }
810
+ ```
811
+
812
+ ## Storage Adapters
813
+
814
+ Configure how the SDK persists cache data. By default, the SDK uses `localStorage` in browsers.
815
+
816
+ ### Built-in Adapters
817
+
818
+ ```typescript
819
+ import { localStorageAdapter, jsonFileAdapter } from '@commerce-blocks/sdk'
820
+
821
+ // Browser: localStorage (returns null if unavailable)
822
+ const browserAdapter = localStorageAdapter('my-cache-key')
823
+
824
+ // Node.js: JSON file
825
+ import fs from 'fs'
826
+ const nodeAdapter = jsonFileAdapter('./cache.json', fs)
827
+ ```
828
+
829
+ ### Custom Adapter
830
+
831
+ Implement the `StorageAdapter` interface:
832
+
833
+ ```typescript
834
+ interface StorageAdapter {
835
+ read(): string | null
836
+ write(data: string): void
837
+ remove(): void
838
+ }
839
+
840
+ // Example: sessionStorage adapter
841
+ const sessionAdapter: StorageAdapter = {
842
+ read: () => sessionStorage.getItem('my-key'),
843
+ write: (data) => sessionStorage.setItem('my-key', data),
844
+ remove: () => sessionStorage.removeItem('my-key'),
845
+ }
713
846
  ```
714
847
 
715
848
  ## Standalone Utilities