@umxr/bagsy 0.1.0 → 0.2.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/dist/hydrogen.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  createBagsyServer,
3
- trackWrites
4
- } from "./chunk-OHSBSGWH.js";
5
- import "./chunk-OM72NJDX.js";
3
+ remember,
4
+ trackWrites,
5
+ warnIfNotPersisted
6
+ } from "./chunk-CJV4SIV7.js";
7
+ import "./chunk-QV3JPND6.js";
6
8
 
7
9
  // ../hydrogen/src/hydrogen.ts
8
10
  import { createShopifyRouteHandler } from "@shopify/hydrogen";
@@ -27,7 +29,11 @@ function bagsyRouteHandlers(server) {
27
29
  async (context) => {
28
30
  const tracked = trackWrites(context.sessionManager);
29
31
  const result = await server.relay(context.request, { ...context, sessionManager: tracked.sessionManager });
30
- const headers = tracked.dirty() ? await context.sessionManager.commit?.() ?? void 0 : void 0;
32
+ let headers;
33
+ if (tracked.dirty()) {
34
+ headers = await context.sessionManager.commit?.() ?? void 0;
35
+ warnIfNotPersisted(new Headers(headers));
36
+ }
31
37
  if ("rejected" in result) {
32
38
  const { status, code, message } = result.rejected;
33
39
  return { type: "error", status, error: { code, message }, ...headers && { headers } };
@@ -39,6 +45,7 @@ function bagsyRouteHandlers(server) {
39
45
  }
40
46
  var CUSTOMER_ID_QUERY = gql(`query BagsyCustomerId { customer { id } }`);
41
47
  var CACHE_KEY = "bagsy:customer";
48
+ var customerIds = /* @__PURE__ */ new Map();
42
49
  function hydrogenCustomerId({
43
50
  customerSession,
44
51
  shopId,
@@ -48,9 +55,13 @@ function hydrogenCustomerId({
48
55
  const token = await fingerprint(accessToken);
49
56
  const cached = await sessionManager.getSessionItem(CACHE_KEY);
50
57
  if (cached?.token === token && cached.id) return { id: cached.id, token, cached: true };
58
+ const known = customerIds.get(`${shopId}|${token}`);
59
+ if (known) return { id: known, token, cached: false };
51
60
  const client = createCustomerAccountClient({ shopId, requestContext, ...fetch && { fetch } });
52
61
  const result = await client.graphql(CUSTOMER_ID_QUERY, { accessToken });
53
- return { id: result.errors ? void 0 : result.data?.customer?.id ?? void 0, token, cached: false };
62
+ const id = result.errors ? void 0 : result.data?.customer?.id ?? void 0;
63
+ if (id) remember(customerIds, `${shopId}|${token}`, id);
64
+ return { id, token, cached: false };
54
65
  }
55
66
  return {
56
67
  async read(context) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../hydrogen/src/hydrogen.ts"],"sourcesContent":["import { createShopifyRouteHandler, type ShopifyRequestContext } from '@shopify/hydrogen';\nimport {\n createCustomerAccountClient,\n gql,\n type CustomerSession,\n type ReadonlyCustomerSessionManager,\n type WritableCustomerSessionManager,\n} from '@shopify/hydrogen/customer-account';\nimport {\n createBagsyServer,\n trackWrites,\n type BagsyServer,\n type CreateBagsyServerOptions,\n type CustomerResolver,\n} from './server.js';\n\n/** For loaders and server components: a read-only session. */\nexport interface HydrogenReadContext {\n sessionManager: ReadonlyCustomerSessionManager;\n requestContext: ShopifyRequestContext;\n}\n\n/** For route handlers and actions: a session the response commits. */\nexport interface HydrogenWriteContext {\n sessionManager: WritableCustomerSessionManager;\n requestContext: ShopifyRequestContext;\n}\n\nexport interface CreateHydrogenBagsyOptions\n extends Omit<CreateBagsyServerOptions<HydrogenReadContext, HydrogenWriteContext>, 'customer'> {\n /**\n * Your `createCustomerSession()` result, so signed-in customers keep one\n * wishlist across devices. Omit for a guest-only storefront.\n */\n customerSession?: Pick<CustomerSession, 'getAccessToken' | 'getOrRefreshAccessToken'>;\n /** Numeric shop id, the same one you pass to `createCustomerSession()`. Required with `customerSession`. */\n shopId?: string;\n /** Fetch for Customer Account API calls (tests, custom runtimes). */\n customerAccountFetch?: typeof globalThis.fetch;\n}\n\n/**\n * Bagsy for Hydrogen 2026-10. Create once at module scope, register\n * `bagsy.handlers` with `handleShopifyRoutes`, and call `bagsy.shopper()` in\n * loaders.\n */\nexport function createHydrogenBagsy(options: CreateHydrogenBagsyOptions) {\n const { customerSession, shopId, customerAccountFetch, ...rest } = options;\n if (customerSession && !shopId) throw new TypeError('createHydrogenBagsy: `shopId` is required with `customerSession`.');\n\n const server = createBagsyServer<HydrogenReadContext, HydrogenWriteContext>({\n ...rest,\n ...(customerSession && shopId && { customer: hydrogenCustomerId({ customerSession, shopId, fetch: customerAccountFetch }) }),\n });\n\n return { ...server, handlers: bagsyRouteHandlers(server) };\n}\n\n/** `handleShopifyRoutes` handlers for the relay endpoint `@umxr/bagsy/react` calls. */\nexport function bagsyRouteHandlers(server: BagsyServer<HydrogenReadContext, HydrogenWriteContext>) {\n return {\n bagsyRelay: createShopifyRouteHandler(\n server.endpoint,\n 'POST',\n async (context: HydrogenWriteContext & { request: Request }) => {\n // Matched Shopify routes skip your app's response boundary, so commit here.\n const tracked = trackWrites(context.sessionManager);\n const result = await server.relay(context.request, { ...context, sessionManager: tracked.sessionManager });\n const headers = tracked.dirty() ? ((await context.sessionManager.commit?.()) ?? undefined) : undefined;\n if ('rejected' in result) {\n const { status, code, message } = result.rejected;\n return { type: 'error' as const, status, error: { code, message }, ...(headers && { headers }) };\n }\n return { type: 'json' as const, data: result.response, ...(headers && { headers }) };\n },\n ),\n };\n}\n\nconst CUSTOMER_ID_QUERY = gql(`query BagsyCustomerId { customer { id } }`);\nconst CACHE_KEY = 'bagsy:customer';\n\n/**\n * Who is signed in, from Hydrogen's customer session. Reads never refresh\n * tokens or write the session; writes may refresh, and cache the customer id\n * against the token so the Customer Account API is asked once per sign-in.\n */\nexport function hydrogenCustomerId({\n customerSession,\n shopId,\n fetch,\n}: {\n customerSession: Pick<CustomerSession, 'getAccessToken' | 'getOrRefreshAccessToken'>;\n shopId: string;\n fetch?: typeof globalThis.fetch | undefined;\n}): CustomerResolver<HydrogenReadContext, HydrogenWriteContext> {\n async function lookup(accessToken: string, { sessionManager, requestContext }: HydrogenReadContext) {\n const token = await fingerprint(accessToken);\n const cached = (await sessionManager.getSessionItem(CACHE_KEY)) as { token?: string; id?: string } | undefined;\n if (cached?.token === token && cached.id) return { id: cached.id, token, cached: true };\n const client = createCustomerAccountClient({ shopId, requestContext, ...(fetch && { fetch }) });\n const result = await client.graphql(CUSTOMER_ID_QUERY, { accessToken });\n return { id: result.errors ? undefined : (result.data?.customer?.id ?? undefined), token, cached: false };\n }\n\n return {\n async read(context) {\n const accessToken = await customerSession.getAccessToken(context.sessionManager, context.requestContext);\n return accessToken ? (await lookup(accessToken, context)).id : undefined;\n },\n async write(context) {\n const accessToken = await customerSession.getOrRefreshAccessToken(context.sessionManager, context.requestContext);\n if (!accessToken) return undefined;\n const { id, token, cached } = await lookup(accessToken, context);\n if (id && !cached) await context.sessionManager.setSessionItem(CACHE_KEY, { token, id });\n return id;\n },\n };\n}\n\n/** A short hash of the token, so the session never holds a second copy of it. */\nasync function fingerprint(token: string) {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(token));\n return [...new Uint8Array(digest).slice(0, 16)].map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../hydrogen/src/hydrogen.ts"],"sourcesContent":["import { createShopifyRouteHandler, type ShopifyRequestContext } from '@shopify/hydrogen';\nimport {\n createCustomerAccountClient,\n gql,\n type CustomerSession,\n type ReadonlyCustomerSessionManager,\n type WritableCustomerSessionManager,\n} from '@shopify/hydrogen/customer-account';\nimport {\n createBagsyServer,\n remember,\n trackWrites,\n warnIfNotPersisted,\n type BagsyServer,\n type CreateBagsyServerOptions,\n type CustomerResolver,\n} from './server.js';\n\n/** For loaders and server components: a read-only session. */\nexport interface HydrogenReadContext {\n sessionManager: ReadonlyCustomerSessionManager;\n requestContext: ShopifyRequestContext;\n}\n\n/** For route handlers and actions: a session the response commits. */\nexport interface HydrogenWriteContext {\n sessionManager: WritableCustomerSessionManager;\n requestContext: ShopifyRequestContext;\n}\n\nexport interface CreateHydrogenBagsyOptions\n extends Omit<CreateBagsyServerOptions<HydrogenReadContext, HydrogenWriteContext>, 'customer'> {\n /**\n * Your `createCustomerSession()` result, so signed-in customers keep one\n * wishlist across devices. Omit for a guest-only storefront.\n */\n customerSession?: Pick<CustomerSession, 'getAccessToken' | 'getOrRefreshAccessToken'>;\n /** Numeric shop id, the same one you pass to `createCustomerSession()`. Required with `customerSession`. */\n shopId?: string;\n /** Fetch for Customer Account API calls (tests, custom runtimes). */\n customerAccountFetch?: typeof globalThis.fetch;\n}\n\n/**\n * Bagsy for Hydrogen 2026-10. Create once at module scope, register\n * `bagsy.handlers` with `handleShopifyRoutes`, and call `bagsy.shopper()` in\n * loaders.\n */\nexport function createHydrogenBagsy(options: CreateHydrogenBagsyOptions) {\n const { customerSession, shopId, customerAccountFetch, ...rest } = options;\n if (customerSession && !shopId) throw new TypeError('createHydrogenBagsy: `shopId` is required with `customerSession`.');\n\n const server = createBagsyServer<HydrogenReadContext, HydrogenWriteContext>({\n ...rest,\n ...(customerSession && shopId && { customer: hydrogenCustomerId({ customerSession, shopId, fetch: customerAccountFetch }) }),\n });\n\n return { ...server, handlers: bagsyRouteHandlers(server) };\n}\n\n/** `handleShopifyRoutes` handlers for the relay endpoint `@umxr/bagsy/react` calls. */\nexport function bagsyRouteHandlers(server: BagsyServer<HydrogenReadContext, HydrogenWriteContext>) {\n return {\n bagsyRelay: createShopifyRouteHandler(\n server.endpoint,\n 'POST',\n async (context: HydrogenWriteContext & { request: Request }) => {\n // Matched Shopify routes skip your app's response boundary, so commit here.\n const tracked = trackWrites(context.sessionManager);\n const result = await server.relay(context.request, { ...context, sessionManager: tracked.sessionManager });\n let headers: HeadersInit | undefined;\n if (tracked.dirty()) {\n headers = (await context.sessionManager.commit?.()) ?? undefined;\n warnIfNotPersisted(new Headers(headers));\n }\n if ('rejected' in result) {\n const { status, code, message } = result.rejected;\n return { type: 'error' as const, status, error: { code, message }, ...(headers && { headers }) };\n }\n return { type: 'json' as const, data: result.response, ...(headers && { headers }) };\n },\n ),\n };\n}\n\nconst CUSTOMER_ID_QUERY = gql(`query BagsyCustomerId { customer { id } }`);\nconst CACHE_KEY = 'bagsy:customer';\n/** Customer ids by token fingerprint, so read-only loaders don't ask the Customer Account API every request. */\nconst customerIds = new Map<string, string>();\n\n/**\n * Who is signed in, from Hydrogen's customer session. Reads never refresh\n * tokens or write the session; writes may refresh, and cache the customer id\n * against the token so the Customer Account API is asked once per sign-in.\n */\nexport function hydrogenCustomerId({\n customerSession,\n shopId,\n fetch,\n}: {\n customerSession: Pick<CustomerSession, 'getAccessToken' | 'getOrRefreshAccessToken'>;\n shopId: string;\n fetch?: typeof globalThis.fetch | undefined;\n}): CustomerResolver<HydrogenReadContext, HydrogenWriteContext> {\n async function lookup(accessToken: string, { sessionManager, requestContext }: HydrogenReadContext) {\n const token = await fingerprint(accessToken);\n const cached = (await sessionManager.getSessionItem(CACHE_KEY)) as { token?: string; id?: string } | undefined;\n if (cached?.token === token && cached.id) return { id: cached.id, token, cached: true };\n const known = customerIds.get(`${shopId}|${token}`);\n if (known) return { id: known, token, cached: false };\n const client = createCustomerAccountClient({ shopId, requestContext, ...(fetch && { fetch }) });\n const result = await client.graphql(CUSTOMER_ID_QUERY, { accessToken });\n const id = result.errors ? undefined : (result.data?.customer?.id ?? undefined);\n if (id) remember(customerIds, `${shopId}|${token}`, id);\n return { id, token, cached: false };\n }\n\n return {\n async read(context) {\n const accessToken = await customerSession.getAccessToken(context.sessionManager, context.requestContext);\n return accessToken ? (await lookup(accessToken, context)).id : undefined;\n },\n async write(context) {\n const accessToken = await customerSession.getOrRefreshAccessToken(context.sessionManager, context.requestContext);\n if (!accessToken) return undefined;\n const { id, token, cached } = await lookup(accessToken, context);\n if (id && !cached) await context.sessionManager.setSessionItem(CACHE_KEY, { token, id });\n return id;\n },\n };\n}\n\n/** A short hash of the token, so the session never holds a second copy of it. */\nasync function fingerprint(token: string) {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(token));\n return [...new Uint8Array(digest).slice(0, 16)].map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,7 @@
1
- export { B as Bagsy, c as createBagsy } from './client-2LxFO18p.js';
2
- import { T as Transport } from './transport-DTApDImd.js';
3
- export { c as createTransport } from './transport-DTApDImd.js';
4
- import { A as AccountOptions, b as ProductRef, I as ItemOptions, e as Item, f as List, L as ListRef, P as PageOptions, g as SharedList, h as ItemPage, S as SavedLookup, M as MergeResult, T as TopProduct, V as VariantRef, c as components } from './types-C1ZgQf5w.js';
5
- export { B as BagsyOptions, C as CustomerRef, O as Owner, i as ProxyOptions, R as RelayOptions, d as SavedEntry, j as ServerOptions, k as customerGid, o as operations, p as paths, l as productGid, v as variantGid } from './types-C1ZgQf5w.js';
6
- export { a as RelayRequest, R as RelayResponse, c as createRelayTransport } from './relay-CU6VnXFE.js';
1
+ export { B as Bagsy, c as createBagsy, a as createRelayClient } from './client-CXCgpFGa.js';
2
+ import { A as AccountOptions, T as Transport, a as ProductRef, I as ItemOptions, d as Item, e as List, L as ListRef, P as PageOptions, f as SharedList, g as ItemPage, S as SavedLookup, M as MergeResult, h as TopProduct, V as VariantRef, c as components } from './transport-Chh09xIP.js';
3
+ export { B as BagsyOptions, C as CustomerRef, O as Owner, i as ProxyOptions, R as RelayOptions, b as SavedEntry, j as ServerOptions, k as createTransport, l as customerGid, o as operations, p as paths, m as productGid, v as variantGid } from './transport-Chh09xIP.js';
4
+ export { a as RelayRequest, R as RelayResponse, c as createRelayTransport } from './relay-0l6VcQp9.js';
7
5
 
8
6
  /**
9
7
  * Transport for customer account UI extensions: every request carries a
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  createAccountTransport,
6
6
  createBagsy,
7
7
  createList,
8
+ createRelayClient,
8
9
  createRelayTransport,
9
10
  createTransport,
10
11
  customerGid,
@@ -28,7 +29,7 @@ import {
28
29
  unshareList,
29
30
  updateItem,
30
31
  variantGid
31
- } from "./chunk-OM72NJDX.js";
32
+ } from "./chunk-QV3JPND6.js";
32
33
  export {
33
34
  BagsyError,
34
35
  addItem,
@@ -36,6 +37,7 @@ export {
36
37
  createAccountTransport,
37
38
  createBagsy,
38
39
  createList,
40
+ createRelayClient,
39
41
  createRelayTransport,
40
42
  createTransport,
41
43
  customerGid,
package/dist/react.d.ts CHANGED
@@ -4,7 +4,10 @@ import { Bagsy } from './index.js';
4
4
  import { SavedEntry, ProductRef, VariantRef, ListRef, Item } from './index.js';
5
5
 
6
6
  interface SavedState {
7
- /** Saved in any list (or, with a variant, that exact variant). False until known. */
7
+ /**
8
+ * Saved in any list. With a variant: that variant, or the product without a
9
+ * variant (which means "any variant"). False until known.
10
+ */
8
11
  saved: boolean;
9
12
  /** True once the server has answered (or the state was server-rendered). */
10
13
  ready: boolean;
@@ -53,13 +56,29 @@ declare class BagsyStore {
53
56
  seed(byProduct: Readonly<Record<string, SavedEntry[]>> | undefined, { notify }?: {
54
57
  notify?: boolean | undefined;
55
58
  }): string[];
56
- seedCount(count: number | undefined): void;
59
+ /**
60
+ * Server-rendered count. Later values (e.g. a root loader revalidating after
61
+ * navigation) replace it, unless a toggle is in flight.
62
+ */
63
+ seedCount(count: number | undefined, { notify }?: {
64
+ notify?: boolean | undefined;
65
+ }): void;
57
66
  /** Current state. Stable object identity until it changes, for useSyncExternalStore. */
58
67
  getState(product: ProductRef, variant?: VariantRef): SavedState;
59
68
  /** Subscribe to one product. Loads its state (batched) if unknown. */
60
69
  subscribe(product: ProductRef, listener: Listener): () => void;
61
- /** Save if unsaved, unsave if saved. The UI updates before the server answers. */
70
+ /**
71
+ * Save if unsaved, unsave if saved. The UI updates before the server answers.
72
+ *
73
+ * - Without a variant: saves the product; unsaving removes every saved
74
+ * variant of it.
75
+ * - With a variant: saves that variant; unsaving removes that variant and
76
+ * any product-level save (which covers all variants).
77
+ *
78
+ * A second tap while the first is in flight is ignored.
79
+ */
62
80
  toggle(product: ProductRef, variant?: VariantRef): Promise<boolean>;
81
+ private unsave;
63
82
  /** Record an item removed elsewhere (e.g. the wishlist page), so hearts update without a request. */
64
83
  forget(product: ProductRef, itemId: string): void;
65
84
  getCount(): number | undefined;
@@ -80,7 +99,7 @@ interface BagsyProviderProps extends StoreEvents {
80
99
  * @default '/api/bagsy'
81
100
  */
82
101
  endpoint?: string;
83
- /** Or bring your own client, e.g. `createBagsy({ mode: 'proxy' })` for React in a Liquid theme. */
102
+ /** Or bring your own client, e.g. `createBagsy({ mode: 'proxy', visitorId: getVisitorId() })` for React in a Liquid theme. */
84
103
  client?: Bagsy;
85
104
  /** Server-rendered saved state: `(await shopper.saved(ids)).byProduct`. */
86
105
  initialSaved?: Readonly<Record<string, SavedEntry[]>>;
@@ -115,7 +134,7 @@ interface UseWishlistOptions {
115
134
  listId?: ListRef;
116
135
  /** Items per page. @default 24 */
117
136
  pageSize?: number;
118
- /** Server-rendered first page: `await shopper.client.items.list('default', { limit })`. */
137
+ /** Server-rendered first page: `await (await bagsy.shopper(context)).items({ limit })`. */
119
138
  initial?: {
120
139
  data: Item[];
121
140
  pageInfo: {
package/dist/react.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
- createBagsy,
2
+ createRelayClient,
3
3
  productGid,
4
4
  variantGid
5
- } from "./chunk-OM72NJDX.js";
5
+ } from "./chunk-QV3JPND6.js";
6
6
 
7
7
  // ../react/src/react.tsx
8
8
  import {
@@ -18,6 +18,9 @@ import {
18
18
 
19
19
  // ../react/src/store.ts
20
20
  var UNKNOWN = Object.freeze({ saved: false, ready: false, pending: false });
21
+ function matching(entries, vid) {
22
+ return vid ? entries.filter((e) => e.variantId === vid || !e.variantId) : entries;
23
+ }
21
24
  var BagsyStore = class {
22
25
  constructor(client, events = {}) {
23
26
  this.client = client;
@@ -53,10 +56,15 @@ var BagsyStore = class {
53
56
  if (notify) this.notify(...filled);
54
57
  return filled;
55
58
  }
56
- seedCount(count) {
57
- if (count === void 0 || this.count !== void 0) return;
59
+ /**
60
+ * Server-rendered count. Later values (e.g. a root loader revalidating after
61
+ * navigation) replace it, unless a toggle is in flight.
62
+ */
63
+ seedCount(count, { notify = true } = {}) {
64
+ if (count === void 0 || count === this.count || this.pendingToggles.size) return;
58
65
  this.count = count;
59
66
  this.countRequest = Promise.resolve(count);
67
+ if (notify) for (const listener of this.countListeners) listener();
60
68
  }
61
69
  /** Current state. Stable object identity until it changes, for useSyncExternalStore. */
62
70
  getState(product, variant) {
@@ -65,7 +73,7 @@ var BagsyStore = class {
65
73
  const key = vid ? `${id}|${vid}` : id;
66
74
  const entries = this.saved.get(id);
67
75
  const next = entries ? {
68
- saved: vid ? entries.some((e) => e.variantId === vid) : entries.length > 0,
76
+ saved: matching(entries, vid).length > 0,
69
77
  ready: true,
70
78
  pending: this.pendingToggles.has(id)
71
79
  } : UNKNOWN;
@@ -87,39 +95,67 @@ var BagsyStore = class {
87
95
  set.delete(listener);
88
96
  };
89
97
  }
90
- /** Save if unsaved, unsave if saved. The UI updates before the server answers. */
98
+ /**
99
+ * Save if unsaved, unsave if saved. The UI updates before the server answers.
100
+ *
101
+ * - Without a variant: saves the product; unsaving removes every saved
102
+ * variant of it.
103
+ * - With a variant: saves that variant; unsaving removes that variant and
104
+ * any product-level save (which covers all variants).
105
+ *
106
+ * A second tap while the first is in flight is ignored.
107
+ */
91
108
  async toggle(product, variant) {
92
109
  const id = productGid(product);
93
110
  const vid = variant === void 0 ? void 0 : variantGid(variant);
111
+ if (this.pendingToggles.has(id)) return this.getState(id, vid).saved;
94
112
  const previous = this.saved.get(id) ?? [];
95
- const wasSaved = this.getState(id, vid).saved;
96
- this.saved.set(
97
- id,
98
- wasSaved ? previous.filter((e) => vid ? e.variantId !== vid : false) : [...previous, { listId: "default", itemId: "pending", ...vid && { variantId: vid } }]
99
- );
100
- this.pendingToggles.set(id, (this.pendingToggles.get(id) ?? 0) + 1);
101
- this.adjustCount(wasSaved ? -1 : 1);
113
+ const matched = matching(previous, vid);
114
+ const wasSaved = matched.length > 0;
115
+ const placeholder = { listId: "default", itemId: "pending", ...vid && { variantId: vid } };
116
+ this.saved.set(id, wasSaved ? previous.filter((e) => !matched.includes(e)) : [...previous, placeholder]);
117
+ this.pendingToggles.set(id, 1);
118
+ this.adjustCount(wasSaved ? -matched.length : 1);
102
119
  this.notify(id);
103
120
  try {
104
- const { saved } = await this.client.items.toggle(id, vid ? { variantId: vid } : {});
105
- if (saved === wasSaved) {
106
- this.queue(id);
107
- this.refreshCount();
121
+ if (wasSaved) {
122
+ await this.unsave(id, vid, matched);
123
+ if (matched.length !== 1) this.refreshCount();
124
+ } else {
125
+ const item = await this.client.items.add(id, vid ? { variantId: vid } : {});
126
+ const current = this.saved.get(id) ?? [];
127
+ this.saved.set(
128
+ id,
129
+ current.map((e) => e === placeholder ? { listId: item.listId, itemId: item.id, ...vid && { variantId: vid } } : e)
130
+ );
108
131
  }
132
+ const saved = !wasSaved;
109
133
  this.events.onChange?.({ productId: id, ...vid && { variantId: vid }, saved });
110
134
  return saved;
111
135
  } catch (error) {
112
136
  this.saved.set(id, previous);
113
- this.adjustCount(wasSaved ? 1 : -1);
137
+ this.adjustCount(wasSaved ? matched.length : -1);
114
138
  this.events.onError?.(error);
139
+ this.queue(id);
115
140
  throw error;
116
141
  } finally {
117
- const left = (this.pendingToggles.get(id) ?? 1) - 1;
118
- if (left) this.pendingToggles.set(id, left);
119
- else this.pendingToggles.delete(id);
142
+ this.pendingToggles.delete(id);
120
143
  this.notify(id);
121
144
  }
122
145
  }
146
+ async unsave(id, vid, matched) {
147
+ const items = this.client.items;
148
+ if (!vid) {
149
+ const lists = [...new Set(matched.map((e) => e.listId))];
150
+ await Promise.all(lists.map((listId) => items.removeProduct(id, { listId })));
151
+ return;
152
+ }
153
+ await Promise.all(
154
+ matched.map(
155
+ (e) => e.variantId ? items.removeProduct(id, { listId: e.listId, variantId: e.variantId }) : items.remove(e.itemId, { listId: e.listId })
156
+ )
157
+ );
158
+ }
123
159
  /** Record an item removed elsewhere (e.g. the wishlist page), so hearts update without a request. */
124
160
  forget(product, itemId) {
125
161
  const id = productGid(product);
@@ -207,11 +243,12 @@ function BagsyProvider({
207
243
  children
208
244
  }) {
209
245
  const [store] = useState(() => {
210
- const created = new BagsyStore(client ?? createBagsy({ mode: "relay", ...endpoint && { endpoint } }));
246
+ const created = new BagsyStore(client ?? createRelayClient(endpoint ? { endpoint } : {}));
211
247
  created.seed(initialSaved);
212
248
  created.seedCount(initialCount);
213
249
  return created;
214
250
  });
251
+ useEffect(() => store.seedCount(initialCount), [store, initialCount]);
215
252
  store.setEvents({ ...onChange && { onChange }, ...onError && { onError } });
216
253
  useSeedInto(store, initialSaved);
217
254
  return /* @__PURE__ */ jsx(BagsyContext.Provider, { value: store, children });
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../react/src/react.tsx","../../react/src/store.ts"],"sourcesContent":["import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ButtonHTMLAttributes,\n type MouseEvent,\n type ReactNode,\n} from 'react';\nimport {\n createBagsy,\n productGid,\n type Bagsy,\n type Item,\n type ListRef,\n type ProductRef,\n type SavedEntry,\n type VariantRef,\n} from '@bagsy/core';\nimport { BagsyStore, type SavedState, type StoreEvents } from './store.js';\n\nconst BagsyContext = createContext<BagsyStore | null>(null);\n\nexport interface BagsyProviderProps extends StoreEvents {\n /**\n * Your server's relay endpoint (see `@umxr/bagsy/hydrogen`).\n * @default '/api/bagsy'\n */\n endpoint?: string;\n /** Or bring your own client, e.g. `createBagsy({ mode: 'proxy' })` for React in a Liquid theme. */\n client?: Bagsy;\n /** Server-rendered saved state: `(await shopper.saved(ids)).byProduct`. */\n initialSaved?: Readonly<Record<string, SavedEntry[]>>;\n /** Server-rendered count: `await shopper.count()`. */\n initialCount?: number;\n children?: ReactNode;\n}\n\n/** One per app. Every Bagsy hook and component below it shares one store. */\nexport function BagsyProvider({\n endpoint,\n client,\n initialSaved,\n initialCount,\n onChange,\n onError,\n children,\n}: BagsyProviderProps) {\n const [store] = useState(() => {\n const created = new BagsyStore(client ?? createBagsy({ mode: 'relay', ...(endpoint && { endpoint }) }));\n created.seed(initialSaved);\n created.seedCount(initialCount);\n return created;\n });\n store.setEvents({ ...(onChange && { onChange }), ...(onError && { onError }) });\n // Later navigations can bring more server-rendered state.\n useSeedInto(store, initialSaved);\n return <BagsyContext.Provider value={store}>{children}</BagsyContext.Provider>;\n}\n\n/** The provider's store and client, for anything the hooks don't cover. */\nexport function useBagsy(): { store: BagsyStore; client: Bagsy } {\n const store = useContext(BagsyContext);\n if (!store) throw new Error('Bagsy hooks need a <BagsyProvider> above them.');\n return { store, client: store.client };\n}\n\n/**\n * Seed saved state from a route's loader, e.g. a collection page that\n * returns `saved: (await shopper.saved(productIds)).byProduct`.\n */\nexport function useSeedSaved(byProduct: Readonly<Record<string, SavedEntry[]>> | undefined) {\n useSeedInto(useBagsy().store, byProduct);\n}\n\n/** Seed while rendering, so children's first render is right; notify existing subscribers after commit. */\nfunction useSeedInto(store: BagsyStore, byProduct: Readonly<Record<string, SavedEntry[]>> | undefined) {\n const seeded = useRef<unknown>(undefined);\n const filled = useRef<string[]>([]);\n if (byProduct && seeded.current !== byProduct) {\n seeded.current = byProduct;\n filled.current = store.seed(byProduct, { notify: false });\n }\n useEffect(() => {\n if (!filled.current.length) return;\n store.notify(...filled.current);\n filled.current = [];\n });\n}\n\nexport interface UseSavedResult extends SavedState {\n /** Save if unsaved, unsave if saved. Resolves to the new state; rejects (after rolling back) on failure. */\n toggle(): Promise<boolean>;\n}\n\n/** Saved state for one product, or one variant of it. */\nexport function useSaved(product: ProductRef, options: { variantId?: VariantRef | undefined } = {}): UseSavedResult {\n const { store } = useBagsy();\n const { variantId } = options;\n const subscribe = useCallback((listener: () => void) => store.subscribe(product, listener), [store, product]);\n const getSnapshot = () => store.getState(product, variantId);\n const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n const toggle = useCallback(() => store.toggle(product, variantId), [store, product, variantId]);\n return useMemo(() => ({ ...state, toggle }), [state, toggle]);\n}\n\n/** Number of items in the shopper's default list, for a header badge. Undefined until known. */\nexport function useWishlistCount(): number | undefined {\n const { store } = useBagsy();\n const subscribe = useCallback((listener: () => void) => store.subscribeCount(listener), [store]);\n const getSnapshot = () => store.getCount();\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\nexport interface UseWishlistOptions {\n /** @default 'default' */\n listId?: ListRef;\n /** Items per page. @default 24 */\n pageSize?: number;\n /** Server-rendered first page: `await shopper.client.items.list('default', { limit })`. */\n initial?: { data: Item[]; pageInfo: { hasNextPage: boolean; endCursor?: string | null } };\n}\n\nexport interface UseWishlistResult {\n items: Item[];\n status: 'loading' | 'ready' | 'error';\n error: unknown;\n hasNextPage: boolean;\n loadingMore: boolean;\n loadMore(): Promise<void>;\n /** Remove an item. Optimistic; restores it on failure. Hearts elsewhere update too. */\n remove(item: Item): Promise<void>;\n refresh(): Promise<void>;\n}\n\n/** The items in a list, newest first, page by page. */\nexport function useWishlist({ listId = 'default', pageSize = 24, initial }: UseWishlistOptions = {}): UseWishlistResult {\n const { store, client } = useBagsy();\n const [items, setItems] = useState<Item[]>(initial?.data ?? []);\n const [cursor, setCursor] = useState<string | null | undefined>(initial?.pageInfo.endCursor);\n const [hasNextPage, setHasNextPage] = useState(initial?.pageInfo.hasNextPage ?? false);\n const [status, setStatus] = useState<UseWishlistResult['status']>(initial ? 'ready' : 'loading');\n const [error, setError] = useState<unknown>(undefined);\n const [loadingMore, setLoadingMore] = useState(false);\n const generation = useRef(0);\n const skipFirstLoad = useRef(Boolean(initial));\n\n const refresh = useCallback(async () => {\n const mine = ++generation.current;\n setStatus((s) => (s === 'ready' ? s : 'loading'));\n try {\n const page = await client.items.list(listId, { limit: pageSize });\n if (mine !== generation.current) return;\n setItems(page.data);\n setCursor(page.pageInfo.endCursor);\n setHasNextPage(page.pageInfo.hasNextPage);\n setStatus('ready');\n setError(undefined);\n } catch (e) {\n if (mine !== generation.current) return;\n setError(e);\n setStatus('error');\n }\n }, [client, listId, pageSize]);\n\n useEffect(() => {\n // `initial` stands in for the first load only; a new list or page size starts over.\n if (skipFirstLoad.current) {\n skipFirstLoad.current = false;\n return;\n }\n void refresh();\n }, [refresh]);\n\n const loadMore = useCallback(async () => {\n if (!hasNextPage || loadingMore || !cursor) return;\n setLoadingMore(true);\n const mine = generation.current;\n try {\n const page = await client.items.list(listId, { limit: pageSize, after: cursor });\n if (mine !== generation.current) return;\n setItems((current) => [...current, ...page.data.filter((i) => !current.some((c) => c.id === i.id))]);\n setCursor(page.pageInfo.endCursor);\n setHasNextPage(page.pageInfo.hasNextPage);\n } catch (e) {\n setError(e);\n } finally {\n setLoadingMore(false);\n }\n }, [client, listId, pageSize, cursor, hasNextPage, loadingMore]);\n\n const remove = useCallback(\n async (item: Item) => {\n setItems((current) => current.filter((i) => i.id !== item.id));\n try {\n await client.items.remove(item.id, { listId });\n store.forget(item.productId, item.id);\n } catch (e) {\n setItems((current) => (current.some((i) => i.id === item.id) ? current : insertByDate(current, item)));\n throw e;\n }\n },\n [client, store, listId],\n );\n\n return { items, status, error, hasNextPage, loadingMore, loadMore, remove, refresh };\n}\n\nfunction insertByDate(items: Item[], item: Item) {\n const index = items.findIndex((i) => i.addedAt < item.addedAt);\n return index === -1 ? [...items, item] : [...items.slice(0, index), item, ...items.slice(index)];\n}\n\nexport interface WishlistButtonProps\n extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'onToggle' | 'onError'> {\n productId: ProductRef;\n variantId?: VariantRef | undefined;\n /** Content, or a function of the saved state. Defaults to \"Save\" / \"Saved\". */\n children?: ReactNode | ((state: SavedState) => ReactNode);\n onToggle?: (saved: boolean) => void;\n onError?: (error: unknown) => void;\n}\n\n/**\n * An accessible save toggle. Unstyled: style it with `[data-saved]`,\n * `[data-pending]` and `[aria-pressed]`. Safe inside product-card links.\n */\nexport function WishlistButton({\n productId,\n variantId,\n children,\n onToggle,\n onError,\n onClick,\n ...rest\n}: WishlistButtonProps) {\n const state = useSaved(productId, { variantId });\n const handleClick = (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n if (event.defaultPrevented) return;\n // Inside a product-card <a>, a heart must not navigate.\n event.preventDefault();\n event.stopPropagation();\n state.toggle().then(onToggle, (error: unknown) => onError?.(error));\n };\n return (\n <button\n type=\"button\"\n aria-pressed={state.saved}\n data-saved={state.saved ? '' : undefined}\n data-pending={state.pending ? '' : undefined}\n data-ready={state.ready ? '' : undefined}\n data-product-id={productGid(productId)}\n onClick={handleClick}\n {...rest}\n >\n {typeof children === 'function' ? children(state) : (children ?? (state.saved ? 'Saved' : 'Save'))}\n </button>\n );\n}\n","import { productGid, variantGid, type Bagsy, type ProductRef, type SavedEntry, type VariantRef } from '@bagsy/core';\n\nexport interface SavedState {\n /** Saved in any list (or, with a variant, that exact variant). False until known. */\n saved: boolean;\n /** True once the server has answered (or the state was server-rendered). */\n ready: boolean;\n /** A toggle is in flight. */\n pending: boolean;\n}\n\nexport interface StoreEvents {\n /** A toggle was confirmed by the server. */\n onChange?: (change: { productId: string; variantId?: string; saved: boolean }) => void;\n /** Any failed request. Toggles have already been rolled back. */\n onError?: (error: unknown) => void;\n}\n\ntype Listener = () => void;\n\nconst UNKNOWN: SavedState = Object.freeze({ saved: false, ready: false, pending: false });\n\n/**\n * Saved state for one shopper, shared by every component under a provider.\n * Plain TypeScript, so it can be used without React (and tested in Node).\n *\n * - Lookups for products not seen before are batched: every component that\n * mounts in the same tick shares one `/saved` request.\n * - Toggles are optimistic and roll back on failure.\n * - Server-rendered state can be seeded, so the first render is already right.\n */\nexport class BagsyStore {\n private saved = new Map<string, SavedEntry[]>();\n private pendingToggles = new Map<string, number>();\n private queued = new Set<string>();\n private flushing?: Promise<void>;\n private productListeners = new Map<string, Set<Listener>>();\n private countListeners = new Set<Listener>();\n private snapshots = new Map<string, SavedState>();\n private count?: number;\n private countRequest?: Promise<number>;\n\n constructor(\n readonly client: Bagsy,\n private events: StoreEvents = {},\n ) {}\n\n setEvents(events: StoreEvents) {\n this.events = events;\n }\n\n /**\n * Server-rendered saved state, e.g. `shopper.saved(ids).byProduct`. Never\n * overwrites state the client already has. Returns the products it filled\n * in; pass `notify: false` while rendering and call `notify()` after.\n */\n seed(byProduct: Readonly<Record<string, SavedEntry[]>> | undefined, { notify = true } = {}): string[] {\n if (!byProduct) return [];\n const filled: string[] = [];\n for (const [id, entries] of Object.entries(byProduct)) {\n if (this.saved.has(id) || this.pendingToggles.has(id)) continue;\n this.saved.set(id, entries);\n filled.push(id);\n }\n if (notify) this.notify(...filled);\n return filled;\n }\n\n seedCount(count: number | undefined) {\n if (count === undefined || this.count !== undefined) return;\n this.count = count;\n this.countRequest = Promise.resolve(count);\n }\n\n /** Current state. Stable object identity until it changes, for useSyncExternalStore. */\n getState(product: ProductRef, variant?: VariantRef): SavedState {\n const id = productGid(product);\n const vid = variant === undefined ? undefined : variantGid(variant);\n const key = vid ? `${id}|${vid}` : id;\n const entries = this.saved.get(id);\n const next: SavedState = entries\n ? {\n saved: vid ? entries.some((e) => e.variantId === vid) : entries.length > 0,\n ready: true,\n pending: this.pendingToggles.has(id),\n }\n : UNKNOWN;\n const previous = this.snapshots.get(key);\n if (previous && previous.saved === next.saved && previous.ready === next.ready && previous.pending === next.pending) {\n return previous;\n }\n this.snapshots.set(key, next);\n return next;\n }\n\n /** Subscribe to one product. Loads its state (batched) if unknown. */\n subscribe(product: ProductRef, listener: Listener): () => void {\n const id = productGid(product);\n let set = this.productListeners.get(id);\n if (!set) this.productListeners.set(id, (set = new Set()));\n set.add(listener);\n if (!this.saved.has(id)) this.queue(id);\n return () => {\n set.delete(listener);\n };\n }\n\n /** Save if unsaved, unsave if saved. The UI updates before the server answers. */\n async toggle(product: ProductRef, variant?: VariantRef): Promise<boolean> {\n const id = productGid(product);\n const vid = variant === undefined ? undefined : variantGid(variant);\n const previous = this.saved.get(id) ?? [];\n const wasSaved = this.getState(id, vid).saved;\n\n this.saved.set(\n id,\n wasSaved\n ? previous.filter((e) => (vid ? e.variantId !== vid : false))\n : [...previous, { listId: 'default', itemId: 'pending', ...(vid && { variantId: vid }) }],\n );\n this.pendingToggles.set(id, (this.pendingToggles.get(id) ?? 0) + 1);\n this.adjustCount(wasSaved ? -1 : 1);\n this.notify(id);\n\n try {\n const { saved } = await this.client.items.toggle(id, vid ? { variantId: vid } : {});\n if (saved === wasSaved) {\n // Changed elsewhere (another tab, another list): take the server's word.\n this.queue(id);\n this.refreshCount();\n }\n this.events.onChange?.({ productId: id, ...(vid && { variantId: vid }), saved });\n return saved;\n } catch (error) {\n this.saved.set(id, previous);\n this.adjustCount(wasSaved ? 1 : -1);\n this.events.onError?.(error);\n throw error;\n } finally {\n const left = (this.pendingToggles.get(id) ?? 1) - 1;\n if (left) this.pendingToggles.set(id, left);\n else this.pendingToggles.delete(id);\n this.notify(id);\n }\n }\n\n /** Record an item removed elsewhere (e.g. the wishlist page), so hearts update without a request. */\n forget(product: ProductRef, itemId: string) {\n const id = productGid(product);\n const entries = this.saved.get(id);\n if (entries) {\n const left = entries.filter((e) => e.itemId !== itemId);\n if (left.length !== entries.length) {\n this.saved.set(id, left);\n this.notify(id);\n }\n }\n this.adjustCount(-1);\n }\n\n getCount(): number | undefined {\n return this.count;\n }\n\n subscribeCount(listener: Listener): () => void {\n this.countListeners.add(listener);\n void this.ensureCount().catch(() => {});\n return () => {\n this.countListeners.delete(listener);\n };\n }\n\n /** Loads the default list's item count once; later calls share it. */\n ensureCount(): Promise<number> {\n return (this.countRequest ??= this.loadCount());\n }\n\n private refreshCount() {\n this.countRequest = this.loadCount();\n void this.countRequest.catch(() => {});\n }\n\n private async loadCount(): Promise<number> {\n try {\n const lists = await this.client.lists.list();\n this.count = lists.find((list) => list.isDefault)?.itemCount ?? 0;\n for (const listener of this.countListeners) listener();\n return this.count;\n } catch (error) {\n this.countRequest = undefined;\n this.events.onError?.(error);\n throw error;\n }\n }\n\n private adjustCount(delta: number) {\n if (this.count === undefined) return;\n this.count = Math.max(0, this.count + delta);\n for (const listener of this.countListeners) listener();\n }\n\n private queue(id: string) {\n this.queued.add(id);\n this.flushing ??= Promise.resolve().then(async () => {\n const ids = [...this.queued];\n this.queued.clear();\n this.flushing = undefined;\n try {\n const lookup = await this.client.saved(ids);\n for (const pid of ids) {\n // A toggle that started meanwhile wins; its own result settles the state.\n if (this.pendingToggles.has(pid)) continue;\n this.saved.set(pid, lookup.get(pid));\n this.notify(pid);\n }\n } catch (error) {\n this.events.onError?.(error);\n }\n });\n }\n\n /** Tell subscribers of these products their state may have changed. */\n notify(...ids: string[]) {\n for (const id of ids) for (const listener of this.productListeners.get(id) ?? []) listener();\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../react/src/react.tsx","../../react/src/store.ts"],"sourcesContent":["import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ButtonHTMLAttributes,\n type MouseEvent,\n type ReactNode,\n} from 'react';\nimport {\n createRelayClient,\n productGid,\n type Bagsy,\n type Item,\n type ListRef,\n type ProductRef,\n type SavedEntry,\n type VariantRef,\n} from '@bagsy/core';\nimport { BagsyStore, type SavedState, type StoreEvents } from './store.js';\n\nconst BagsyContext = createContext<BagsyStore | null>(null);\n\nexport interface BagsyProviderProps extends StoreEvents {\n /**\n * Your server's relay endpoint (see `@umxr/bagsy/hydrogen`).\n * @default '/api/bagsy'\n */\n endpoint?: string;\n /** Or bring your own client, e.g. `createBagsy({ mode: 'proxy', visitorId: getVisitorId() })` for React in a Liquid theme. */\n client?: Bagsy;\n /** Server-rendered saved state: `(await shopper.saved(ids)).byProduct`. */\n initialSaved?: Readonly<Record<string, SavedEntry[]>>;\n /** Server-rendered count: `await shopper.count()`. */\n initialCount?: number;\n children?: ReactNode;\n}\n\n/** One per app. Every Bagsy hook and component below it shares one store. */\nexport function BagsyProvider({\n endpoint,\n client,\n initialSaved,\n initialCount,\n onChange,\n onError,\n children,\n}: BagsyProviderProps) {\n const [store] = useState(() => {\n const created = new BagsyStore(client ?? createRelayClient(endpoint ? { endpoint } : {}));\n created.seed(initialSaved);\n created.seedCount(initialCount);\n return created;\n });\n // A revalidated root loader brings a fresh count.\n useEffect(() => store.seedCount(initialCount), [store, initialCount]);\n store.setEvents({ ...(onChange && { onChange }), ...(onError && { onError }) });\n // Later navigations can bring more server-rendered state.\n useSeedInto(store, initialSaved);\n return <BagsyContext.Provider value={store}>{children}</BagsyContext.Provider>;\n}\n\n/** The provider's store and client, for anything the hooks don't cover. */\nexport function useBagsy(): { store: BagsyStore; client: Bagsy } {\n const store = useContext(BagsyContext);\n if (!store) throw new Error('Bagsy hooks need a <BagsyProvider> above them.');\n return { store, client: store.client };\n}\n\n/**\n * Seed saved state from a route's loader, e.g. a collection page that\n * returns `saved: (await shopper.saved(productIds)).byProduct`.\n */\nexport function useSeedSaved(byProduct: Readonly<Record<string, SavedEntry[]>> | undefined) {\n useSeedInto(useBagsy().store, byProduct);\n}\n\n/** Seed while rendering, so children's first render is right; notify existing subscribers after commit. */\nfunction useSeedInto(store: BagsyStore, byProduct: Readonly<Record<string, SavedEntry[]>> | undefined) {\n const seeded = useRef<unknown>(undefined);\n const filled = useRef<string[]>([]);\n if (byProduct && seeded.current !== byProduct) {\n seeded.current = byProduct;\n filled.current = store.seed(byProduct, { notify: false });\n }\n useEffect(() => {\n if (!filled.current.length) return;\n store.notify(...filled.current);\n filled.current = [];\n });\n}\n\nexport interface UseSavedResult extends SavedState {\n /** Save if unsaved, unsave if saved. Resolves to the new state; rejects (after rolling back) on failure. */\n toggle(): Promise<boolean>;\n}\n\n/** Saved state for one product, or one variant of it. */\nexport function useSaved(product: ProductRef, options: { variantId?: VariantRef | undefined } = {}): UseSavedResult {\n const { store } = useBagsy();\n const { variantId } = options;\n const subscribe = useCallback((listener: () => void) => store.subscribe(product, listener), [store, product]);\n const getSnapshot = () => store.getState(product, variantId);\n const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n const toggle = useCallback(() => store.toggle(product, variantId), [store, product, variantId]);\n return useMemo(() => ({ ...state, toggle }), [state, toggle]);\n}\n\n/** Number of items in the shopper's default list, for a header badge. Undefined until known. */\nexport function useWishlistCount(): number | undefined {\n const { store } = useBagsy();\n const subscribe = useCallback((listener: () => void) => store.subscribeCount(listener), [store]);\n const getSnapshot = () => store.getCount();\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\nexport interface UseWishlistOptions {\n /** @default 'default' */\n listId?: ListRef;\n /** Items per page. @default 24 */\n pageSize?: number;\n /** Server-rendered first page: `await (await bagsy.shopper(context)).items({ limit })`. */\n initial?: { data: Item[]; pageInfo: { hasNextPage: boolean; endCursor?: string | null } };\n}\n\nexport interface UseWishlistResult {\n items: Item[];\n status: 'loading' | 'ready' | 'error';\n error: unknown;\n hasNextPage: boolean;\n loadingMore: boolean;\n loadMore(): Promise<void>;\n /** Remove an item. Optimistic; restores it on failure. Hearts elsewhere update too. */\n remove(item: Item): Promise<void>;\n refresh(): Promise<void>;\n}\n\n/** The items in a list, newest first, page by page. */\nexport function useWishlist({ listId = 'default', pageSize = 24, initial }: UseWishlistOptions = {}): UseWishlistResult {\n const { store, client } = useBagsy();\n const [items, setItems] = useState<Item[]>(initial?.data ?? []);\n const [cursor, setCursor] = useState<string | null | undefined>(initial?.pageInfo.endCursor);\n const [hasNextPage, setHasNextPage] = useState(initial?.pageInfo.hasNextPage ?? false);\n const [status, setStatus] = useState<UseWishlistResult['status']>(initial ? 'ready' : 'loading');\n const [error, setError] = useState<unknown>(undefined);\n const [loadingMore, setLoadingMore] = useState(false);\n const generation = useRef(0);\n const skipFirstLoad = useRef(Boolean(initial));\n\n const refresh = useCallback(async () => {\n const mine = ++generation.current;\n setStatus((s) => (s === 'ready' ? s : 'loading'));\n try {\n const page = await client.items.list(listId, { limit: pageSize });\n if (mine !== generation.current) return;\n setItems(page.data);\n setCursor(page.pageInfo.endCursor);\n setHasNextPage(page.pageInfo.hasNextPage);\n setStatus('ready');\n setError(undefined);\n } catch (e) {\n if (mine !== generation.current) return;\n setError(e);\n setStatus('error');\n }\n }, [client, listId, pageSize]);\n\n useEffect(() => {\n // `initial` stands in for the first load only; a new list or page size starts over.\n if (skipFirstLoad.current) {\n skipFirstLoad.current = false;\n return;\n }\n void refresh();\n }, [refresh]);\n\n const loadMore = useCallback(async () => {\n if (!hasNextPage || loadingMore || !cursor) return;\n setLoadingMore(true);\n const mine = generation.current;\n try {\n const page = await client.items.list(listId, { limit: pageSize, after: cursor });\n if (mine !== generation.current) return;\n setItems((current) => [...current, ...page.data.filter((i) => !current.some((c) => c.id === i.id))]);\n setCursor(page.pageInfo.endCursor);\n setHasNextPage(page.pageInfo.hasNextPage);\n } catch (e) {\n setError(e);\n } finally {\n setLoadingMore(false);\n }\n }, [client, listId, pageSize, cursor, hasNextPage, loadingMore]);\n\n const remove = useCallback(\n async (item: Item) => {\n setItems((current) => current.filter((i) => i.id !== item.id));\n try {\n await client.items.remove(item.id, { listId });\n store.forget(item.productId, item.id);\n } catch (e) {\n setItems((current) => (current.some((i) => i.id === item.id) ? current : insertByDate(current, item)));\n throw e;\n }\n },\n [client, store, listId],\n );\n\n return { items, status, error, hasNextPage, loadingMore, loadMore, remove, refresh };\n}\n\nfunction insertByDate(items: Item[], item: Item) {\n const index = items.findIndex((i) => i.addedAt < item.addedAt);\n return index === -1 ? [...items, item] : [...items.slice(0, index), item, ...items.slice(index)];\n}\n\nexport interface WishlistButtonProps\n extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'onToggle' | 'onError'> {\n productId: ProductRef;\n variantId?: VariantRef | undefined;\n /** Content, or a function of the saved state. Defaults to \"Save\" / \"Saved\". */\n children?: ReactNode | ((state: SavedState) => ReactNode);\n onToggle?: (saved: boolean) => void;\n onError?: (error: unknown) => void;\n}\n\n/**\n * An accessible save toggle. Unstyled: style it with `[data-saved]`,\n * `[data-pending]` and `[aria-pressed]`. Safe inside product-card links.\n */\nexport function WishlistButton({\n productId,\n variantId,\n children,\n onToggle,\n onError,\n onClick,\n ...rest\n}: WishlistButtonProps) {\n const state = useSaved(productId, { variantId });\n const handleClick = (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n if (event.defaultPrevented) return;\n // Inside a product-card <a>, a heart must not navigate.\n event.preventDefault();\n event.stopPropagation();\n state.toggle().then(onToggle, (error: unknown) => onError?.(error));\n };\n return (\n <button\n type=\"button\"\n aria-pressed={state.saved}\n data-saved={state.saved ? '' : undefined}\n data-pending={state.pending ? '' : undefined}\n data-ready={state.ready ? '' : undefined}\n data-product-id={productGid(productId)}\n onClick={handleClick}\n {...rest}\n >\n {typeof children === 'function' ? children(state) : (children ?? (state.saved ? 'Saved' : 'Save'))}\n </button>\n );\n}\n","import { productGid, variantGid, type Bagsy, type ProductRef, type SavedEntry, type VariantRef } from '@bagsy/core';\n\nexport interface SavedState {\n /**\n * Saved in any list. With a variant: that variant, or the product without a\n * variant (which means \"any variant\"). False until known.\n */\n saved: boolean;\n /** True once the server has answered (or the state was server-rendered). */\n ready: boolean;\n /** A toggle is in flight. */\n pending: boolean;\n}\n\nexport interface StoreEvents {\n /** A toggle was confirmed by the server. */\n onChange?: (change: { productId: string; variantId?: string; saved: boolean }) => void;\n /** Any failed request. Toggles have already been rolled back. */\n onError?: (error: unknown) => void;\n}\n\ntype Listener = () => void;\n\nconst UNKNOWN: SavedState = Object.freeze({ saved: false, ready: false, pending: false });\n\n/** Entries that make a product (or one variant of it) count as saved. No variant on an entry means any variant. */\nfunction matching(entries: SavedEntry[], vid: string | undefined) {\n return vid ? entries.filter((e) => e.variantId === vid || !e.variantId) : entries;\n}\n\n/**\n * Saved state for one shopper, shared by every component under a provider.\n * Plain TypeScript, so it can be used without React (and tested in Node).\n *\n * - Lookups for products not seen before are batched: every component that\n * mounts in the same tick shares one `/saved` request.\n * - Toggles are optimistic and roll back on failure.\n * - Server-rendered state can be seeded, so the first render is already right.\n */\nexport class BagsyStore {\n private saved = new Map<string, SavedEntry[]>();\n private pendingToggles = new Map<string, number>();\n private queued = new Set<string>();\n private flushing?: Promise<void>;\n private productListeners = new Map<string, Set<Listener>>();\n private countListeners = new Set<Listener>();\n private snapshots = new Map<string, SavedState>();\n private count?: number;\n private countRequest?: Promise<number>;\n\n constructor(\n readonly client: Bagsy,\n private events: StoreEvents = {},\n ) {}\n\n setEvents(events: StoreEvents) {\n this.events = events;\n }\n\n /**\n * Server-rendered saved state, e.g. `shopper.saved(ids).byProduct`. Never\n * overwrites state the client already has. Returns the products it filled\n * in; pass `notify: false` while rendering and call `notify()` after.\n */\n seed(byProduct: Readonly<Record<string, SavedEntry[]>> | undefined, { notify = true } = {}): string[] {\n if (!byProduct) return [];\n const filled: string[] = [];\n for (const [id, entries] of Object.entries(byProduct)) {\n if (this.saved.has(id) || this.pendingToggles.has(id)) continue;\n this.saved.set(id, entries);\n filled.push(id);\n }\n if (notify) this.notify(...filled);\n return filled;\n }\n\n /**\n * Server-rendered count. Later values (e.g. a root loader revalidating after\n * navigation) replace it, unless a toggle is in flight.\n */\n seedCount(count: number | undefined, { notify = true } = {}) {\n if (count === undefined || count === this.count || this.pendingToggles.size) return;\n this.count = count;\n this.countRequest = Promise.resolve(count);\n if (notify) for (const listener of this.countListeners) listener();\n }\n\n /** Current state. Stable object identity until it changes, for useSyncExternalStore. */\n getState(product: ProductRef, variant?: VariantRef): SavedState {\n const id = productGid(product);\n const vid = variant === undefined ? undefined : variantGid(variant);\n const key = vid ? `${id}|${vid}` : id;\n const entries = this.saved.get(id);\n const next: SavedState = entries\n ? {\n saved: matching(entries, vid).length > 0,\n ready: true,\n pending: this.pendingToggles.has(id),\n }\n : UNKNOWN;\n const previous = this.snapshots.get(key);\n if (previous && previous.saved === next.saved && previous.ready === next.ready && previous.pending === next.pending) {\n return previous;\n }\n this.snapshots.set(key, next);\n return next;\n }\n\n /** Subscribe to one product. Loads its state (batched) if unknown. */\n subscribe(product: ProductRef, listener: Listener): () => void {\n const id = productGid(product);\n let set = this.productListeners.get(id);\n if (!set) this.productListeners.set(id, (set = new Set()));\n set.add(listener);\n if (!this.saved.has(id)) this.queue(id);\n return () => {\n set.delete(listener);\n };\n }\n\n /**\n * Save if unsaved, unsave if saved. The UI updates before the server answers.\n *\n * - Without a variant: saves the product; unsaving removes every saved\n * variant of it.\n * - With a variant: saves that variant; unsaving removes that variant and\n * any product-level save (which covers all variants).\n *\n * A second tap while the first is in flight is ignored.\n */\n async toggle(product: ProductRef, variant?: VariantRef): Promise<boolean> {\n const id = productGid(product);\n const vid = variant === undefined ? undefined : variantGid(variant);\n if (this.pendingToggles.has(id)) return this.getState(id, vid).saved;\n\n const previous = this.saved.get(id) ?? [];\n const matched = matching(previous, vid);\n const wasSaved = matched.length > 0;\n const placeholder: SavedEntry = { listId: 'default', itemId: 'pending', ...(vid && { variantId: vid }) };\n\n this.saved.set(id, wasSaved ? previous.filter((e) => !matched.includes(e)) : [...previous, placeholder]);\n this.pendingToggles.set(id, 1);\n this.adjustCount(wasSaved ? -matched.length : 1);\n this.notify(id);\n\n try {\n if (wasSaved) {\n await this.unsave(id, vid, matched);\n // Entries may span lists; only the server knows the default list's count now.\n if (matched.length !== 1) this.refreshCount();\n } else {\n const item = await this.client.items.add(id, vid ? { variantId: vid } : {});\n const current = this.saved.get(id) ?? [];\n this.saved.set(\n id,\n current.map((e) => (e === placeholder ? { listId: item.listId, itemId: item.id, ...(vid && { variantId: vid }) } : e)),\n );\n }\n const saved = !wasSaved;\n this.events.onChange?.({ productId: id, ...(vid && { variantId: vid }), saved });\n return saved;\n } catch (error) {\n this.saved.set(id, previous);\n this.adjustCount(wasSaved ? matched.length : -1);\n this.events.onError?.(error);\n this.queue(id); // a partial unsave may have happened; re-sync\n throw error;\n } finally {\n this.pendingToggles.delete(id);\n this.notify(id);\n }\n }\n\n private async unsave(id: string, vid: string | undefined, matched: SavedEntry[]) {\n const items = this.client.items;\n if (!vid) {\n // Whole product: one request per list it's saved in.\n const lists = [...new Set(matched.map((e) => e.listId))];\n await Promise.all(lists.map((listId) => items.removeProduct(id, { listId })));\n return;\n }\n await Promise.all(\n matched.map((e) =>\n e.variantId\n ? items.removeProduct(id, { listId: e.listId, variantId: e.variantId })\n : items.remove(e.itemId, { listId: e.listId }),\n ),\n );\n }\n\n /** Record an item removed elsewhere (e.g. the wishlist page), so hearts update without a request. */\n forget(product: ProductRef, itemId: string) {\n const id = productGid(product);\n const entries = this.saved.get(id);\n if (entries) {\n const left = entries.filter((e) => e.itemId !== itemId);\n if (left.length !== entries.length) {\n this.saved.set(id, left);\n this.notify(id);\n }\n }\n this.adjustCount(-1);\n }\n\n getCount(): number | undefined {\n return this.count;\n }\n\n subscribeCount(listener: Listener): () => void {\n this.countListeners.add(listener);\n void this.ensureCount().catch(() => {});\n return () => {\n this.countListeners.delete(listener);\n };\n }\n\n /** Loads the default list's item count once; later calls share it. */\n ensureCount(): Promise<number> {\n return (this.countRequest ??= this.loadCount());\n }\n\n private refreshCount() {\n this.countRequest = this.loadCount();\n void this.countRequest.catch(() => {});\n }\n\n private async loadCount(): Promise<number> {\n try {\n const lists = await this.client.lists.list();\n this.count = lists.find((list) => list.isDefault)?.itemCount ?? 0;\n for (const listener of this.countListeners) listener();\n return this.count;\n } catch (error) {\n this.countRequest = undefined;\n this.events.onError?.(error);\n throw error;\n }\n }\n\n private adjustCount(delta: number) {\n if (this.count === undefined) return;\n this.count = Math.max(0, this.count + delta);\n for (const listener of this.countListeners) listener();\n }\n\n private queue(id: string) {\n this.queued.add(id);\n this.flushing ??= Promise.resolve().then(async () => {\n const ids = [...this.queued];\n this.queued.clear();\n this.flushing = undefined;\n try {\n const lookup = await this.client.saved(ids);\n for (const pid of ids) {\n // A toggle that started meanwhile wins; its own result settles the state.\n if (this.pendingToggles.has(pid)) continue;\n this.saved.set(pid, lookup.get(pid));\n this.notify(pid);\n }\n } catch (error) {\n this.events.onError?.(error);\n }\n });\n }\n\n /** Tell subscribers of these products their state may have changed. */\n notify(...ids: string[]) {\n for (const id of ids) for (const listener of this.productListeners.get(id) ?? []) listener();\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,4 @@
1
- import { T as Transport } from './transport-DTApDImd.js';
2
- import { R as RelayOptions } from './types-C1ZgQf5w.js';
1
+ import { R as RelayOptions, T as Transport } from './transport-Chh09xIP.js';
3
2
 
4
3
  /** One Bagsy API call, as the browser sends it to your server's relay endpoint. */
5
4
  interface RelayRequest {
package/dist/server.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Bagsy } from './index.js';
2
2
  import { ProductRef, SavedLookup, PageOptions, ItemPage } from './index.js';
3
3
  import { RelayResponse } from './index.js';
4
- import './transport-DTApDImd.js';
5
4
 
6
5
  type Awaitable<T> = T | Promise<T>;
7
6
  /** A session you can only read: what loaders and server components get. */
@@ -67,9 +66,9 @@ interface WritableShopper {
67
66
  readonly client: Bagsy;
68
67
  }
69
68
  /**
70
- * Bagsy for Hydrogen and other server-rendered storefronts. Create one at
71
- * module scope; call `shopper()` in loaders and `relay`/`handle` for the
72
- * browser endpoint.
69
+ * Bagsy for Hydrogen and other server-rendered storefronts. Cheap to create:
70
+ * make one per request from your env, or one at module scope. Call
71
+ * `shopper()` in loaders and `relay`/`handle` for the browser endpoint.
73
72
  */
74
73
  declare function createBagsyServer<R extends ReadContext = ReadContext, W extends WriteContext = WriteContext>(options: CreateBagsyServerOptions<R, W>): {
75
74
  endpoint: string;
package/dist/server.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createBagsyServer,
3
3
  trackWrites
4
- } from "./chunk-OHSBSGWH.js";
5
- import "./chunk-OM72NJDX.js";
4
+ } from "./chunk-CJV4SIV7.js";
5
+ import "./chunk-QV3JPND6.js";
6
6
  export {
7
7
  createBagsyServer,
8
8
  trackWrites
@@ -1006,5 +1006,17 @@ interface ItemOptions {
1006
1006
  variantId?: VariantRef;
1007
1007
  }
1008
1008
 
1009
- export { customerGid as k, productGid as l, variantGid as v };
1010
- export type { AccountOptions as A, BagsyOptions as B, CustomerRef as C, ItemOptions as I, ListRef as L, MergeResult as M, Owner as O, PageOptions as P, RelayOptions as R, SavedLookup as S, TopProduct as T, VariantRef as V, RequestOptions as a, ProductRef as b, components as c, SavedEntry as d, Item as e, List as f, SharedList as g, ItemPage as h, ProxyOptions as i, ServerOptions as j, operations as o, paths as p };
1009
+ /** How operations reach the API: base URL, identity headers and fetch. */
1010
+ interface Transport {
1011
+ readonly options: BagsyOptions;
1012
+ send<T>(method: string, path: string, init?: RequestOptions): Promise<{
1013
+ status: number;
1014
+ data: T;
1015
+ }>;
1016
+ request<T>(method: string, path: string, init?: RequestOptions): Promise<T>;
1017
+ }
1018
+ /** Proxy, server and account modes. Relay mode has its own: `createRelayTransport()`. */
1019
+ declare function createTransport(options: Exclude<BagsyOptions, RelayOptions>): Transport;
1020
+
1021
+ export { createTransport as k, customerGid as l, productGid as m, variantGid as v };
1022
+ export type { AccountOptions as A, BagsyOptions as B, CustomerRef as C, ItemOptions as I, ListRef as L, MergeResult as M, Owner as O, PageOptions as P, RelayOptions as R, SavedLookup as S, Transport as T, VariantRef as V, ProductRef as a, SavedEntry as b, components as c, Item as d, List as e, SharedList as f, ItemPage as g, TopProduct as h, ProxyOptions as i, ServerOptions as j, operations as o, paths as p };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umxr/bagsy",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Wishlists for Shopify, built for developers. Typed client, React hooks, Hydrogen handlers and web components.",
5
5
  "keywords": [
6
6
  "shopify",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../hydrogen/src/server.ts"],"sourcesContent":["import {\n BagsyError,\n createBagsy,\n createTransport,\n productGid,\n type Bagsy,\n type ItemPage,\n type Owner,\n type PageOptions,\n type ProductRef,\n type RelayRequest,\n type RelayResponse,\n type SavedEntry,\n type SavedLookup,\n type ServerOptions,\n} from '@bagsy/core';\n\ntype Awaitable<T> = T | Promise<T>;\n\n/** A session you can only read: what loaders and server components get. */\nexport interface ReadonlySessionManager {\n getSessionItem(key: string): Awaitable<unknown>;\n}\n\n/** A session you can write and commit. Same shape as Hydrogen's `ShopifyRouteSessionManager`. */\nexport interface BagsySessionManager extends ReadonlySessionManager {\n setSessionItem(key: string, value: unknown): Awaitable<void>;\n removeSessionItem(key: string): Awaitable<void>;\n /** Saves changes and returns headers (a cookie) for the response. */\n commit?(): Awaitable<HeadersInit | void>;\n}\n\n/** The parts of Hydrogen's `ShopifyRequestContext` Bagsy uses. */\nexport interface BagsyRequestContext {\n markResponseAsPersonalized?(reason: string): void;\n}\n\n/** For reads (loaders): in Hydrogen, your request's session manager and `requestContext`. */\nexport interface ReadContext {\n sessionManager: ReadonlySessionManager;\n requestContext: BagsyRequestContext;\n}\n\n/** For writes: only where the response can commit the session (route handlers, actions). */\nexport interface WriteContext extends ReadContext {\n sessionManager: BagsySessionManager;\n}\n\n/** How Bagsy learns who is signed in. `hydrogenCustomerId()` implements it for Hydrogen. */\nexport interface CustomerResolver<R extends ReadContext = ReadContext, W extends WriteContext = WriteContext> {\n /** Without refreshing tokens or writing the session. */\n read(context: R): Promise<string | undefined>;\n /** May refresh tokens and cache in the session; the caller commits. */\n write(context: W): Promise<string | undefined>;\n}\n\nexport interface CreateBagsyServerOptions<R extends ReadContext = ReadContext, W extends WriteContext = WriteContext> {\n /** A server key from the Bagsy admin (Apps → Bagsy → Developers). Keep it server-side. */\n apiKey: string;\n /** Bagsy API base URL, e.g. `https://bagsy-api.example.workers.dev/v1`. */\n baseUrl?: string;\n /** Who is signed in. Omit for a guest-only storefront. */\n customer?: CustomerResolver<R, W>;\n /** Path the browser calls; `@umxr/bagsy/react` uses the same default. @default '/api/bagsy' */\n endpoint?: string;\n fetch?: typeof globalThis.fetch;\n}\n\n/** Read-only view of the shopper, for loaders and server rendering. Never writes the session. */\nexport interface Shopper {\n /** Signed-in customer GID, if any. */\n readonly customerId: string | undefined;\n /** Saved state for products. Guests who have never saved cost no API call. Pass `.byProduct` to `@umxr/bagsy/react`. */\n saved(products: Iterable<ProductRef>): Promise<SavedLookup>;\n /** Items in the default list, for a header badge. */\n count(): Promise<number>;\n /** A page of the default list, newest first. Pass to `useWishlist({ initial })`. */\n items(page?: PageOptions): Promise<ItemPage>;\n}\n\n/** The shopper with write access, for actions and route handlers that commit the session. */\nexport interface WritableShopper {\n readonly customerId: string | undefined;\n /**\n * A Bagsy client acting for this shopper. A guest without a wishlist gets\n * an id on first use, stored in the session: commit it with the response.\n */\n readonly client: Bagsy;\n}\n\n/** What Bagsy keeps in your storefront session. */\ninterface BagsySession {\n /** Guest wishlist owner, until they sign in and it's merged. */\n visitorId?: string;\n}\n\nconst SESSION_KEY = 'bagsy';\nconst EMPTY_PAGE: ItemPage = { data: [], pageInfo: { hasNextPage: false, endCursor: null } } as ItemPage;\n\n/**\n * Bagsy for Hydrogen and other server-rendered storefronts. Create one at\n * module scope; call `shopper()` in loaders and `relay`/`handle` for the\n * browser endpoint.\n */\nexport function createBagsyServer<R extends ReadContext = ReadContext, W extends WriteContext = WriteContext>(\n options: CreateBagsyServerOptions<R, W>,\n) {\n const endpoint = options.endpoint ?? '/api/bagsy';\n const serverOptions: ServerOptions = {\n mode: 'server',\n apiKey: options.apiKey,\n ...(options.baseUrl && { baseUrl: options.baseUrl }),\n ...(options.fetch && { fetch: options.fetch }),\n };\n const base = createBagsy(serverOptions);\n const transportFor = (owner: Owner) => createTransport({ ...serverOptions, ...owner });\n\n /** The shopper for this request, read-only. Safe in loaders and server components. */\n async function shopper(context: R): Promise<Shopper> {\n context.requestContext.markResponseAsPersonalized?.('Bagsy wishlist');\n const session = await readSession(context.sessionManager);\n const customerId = await options.customer?.read(context);\n // Signed in, but the guest list isn't merged yet (that happens on the next\n // write path): show both, so nothing seems to vanish for a page.\n const owners: Bagsy[] = [];\n if (customerId) owners.push(base.as({ customerId }));\n if (session.visitorId) owners.push(base.as({ visitorId: session.visitorId }));\n\n return {\n customerId,\n async saved(products) {\n const list = [...products];\n const lookups = await Promise.all(owners.map((o) => o.saved(list)));\n return unionLookup(list, lookups);\n },\n async count() {\n const counts = await Promise.all(\n owners.map(async (o) => (await o.lists.list()).find((l) => l.isDefault)?.itemCount ?? 0),\n );\n return counts.reduce((a, b) => a + b, 0);\n },\n async items(page = {}) {\n if (!owners[0]) return EMPTY_PAGE;\n return owners[0].items.list('default', page);\n },\n };\n }\n\n /**\n * The shopper with write access: merges a guest list into the customer's\n * after sign-in, and creates a guest id on first write. Use where the\n * response commits the session, e.g. a React Router action.\n */\n async function writableShopper(context: W): Promise<WritableShopper & InternalWritable> {\n context.requestContext.markResponseAsPersonalized?.('Bagsy wishlist');\n const session = await readSession(context.sessionManager);\n const customerId = await options.customer?.write(context);\n let write: Promise<void> = Promise.resolve();\n\n if (customerId && session.visitorId) {\n try {\n await base.as({ customerId }).identity.merge(session.visitorId);\n delete session.visitorId;\n await writeSession(context.sessionManager, session);\n } catch (error) {\n // Keep the guest id; the next write retries. Merging is idempotent.\n if (!(error instanceof BagsyError)) throw error;\n }\n }\n\n const identityKnown = Boolean(customerId || session.visitorId);\n const owner = (): Owner => {\n if (customerId) return { customerId };\n if (!session.visitorId) {\n session.visitorId = crypto.randomUUID();\n write = writeSession(context.sessionManager, session);\n }\n return { visitorId: session.visitorId };\n };\n let client: Bagsy | undefined;\n\n return {\n customerId,\n get client() {\n return (client ??= base.as(owner()));\n },\n identityKnown,\n owner,\n settled: () => write,\n };\n }\n\n /**\n * Run one call from the browser. Returns the API's status and body, or why\n * the call was rejected. Commit the session after (`handle` does).\n */\n async function relay(\n request: Request,\n context: W,\n ): Promise<{ response: RelayResponse } | { rejected: { status: number; code: string; message: string } }> {\n if (request.method !== 'POST') return { rejected: { status: 405, code: 'method_not_allowed', message: 'Use POST.' } };\n // Only same-origin scripts can send JSON: forms can't, and other origins\n // would need CORS, which this endpoint never grants.\n if (!request.headers.get('Content-Type')?.toLowerCase().startsWith('application/json')) {\n return { rejected: { status: 415, code: 'unsupported_media_type', message: 'Send application/json.' } };\n }\n const origin = request.headers.get('Origin');\n if (origin && origin !== new URL(request.url).origin) {\n return { rejected: { status: 403, code: 'forbidden_origin', message: 'Cross-origin requests are not allowed.' } };\n }\n\n let call: RelayRequest;\n try {\n call = parseCall(await request.json());\n } catch (error) {\n return { rejected: { status: 400, code: 'invalid_call', message: error instanceof Error ? error.message : 'Invalid call.' } };\n }\n\n const who = await writableShopper(context);\n // A guest who has never saved reads with a throwaway id, so browsing\n // doesn't create a session.\n const owner = who.identityKnown || call.method !== 'GET' ? who.owner() : { visitorId: crypto.randomUUID() };\n try {\n const { status, data } = await transportFor(owner).send(call.method, call.path, {\n ...(call.query && { query: call.query }),\n ...(call.body !== undefined && { body: call.body }),\n });\n return { response: { status, data } };\n } catch (error) {\n if (!(error instanceof BagsyError)) throw error;\n return { response: { status: error.status, data: error.problem ?? null } };\n } finally {\n await who.settled();\n }\n }\n\n /**\n * The relay endpoint as a plain `Request → Response` handler, for servers\n * without Hydrogen's route handlers. Returns null for other paths, and\n * commits the session if Bagsy changed it.\n */\n async function handle(request: Request, context: W): Promise<Response | null> {\n if (new URL(request.url).pathname !== endpoint) return null;\n const tracked = trackWrites(context.sessionManager);\n const result = await relay(request, { ...context, sessionManager: tracked.sessionManager });\n const headers = new Headers({ 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });\n if (tracked.dirty()) {\n for (const [name, value] of new Headers((await context.sessionManager.commit?.()) ?? undefined)) {\n headers.append(name, value);\n }\n }\n return 'rejected' in result\n ? new Response(JSON.stringify({ error: { code: result.rejected.code, message: result.rejected.message } }), {\n status: result.rejected.status,\n headers,\n })\n : new Response(JSON.stringify(result.response), { status: 200, headers });\n }\n\n const writable: (context: W) => Promise<WritableShopper> = writableShopper;\n return { endpoint, shopper, writableShopper: writable, relay, handle };\n}\n\nexport type BagsyServer<R extends ReadContext = ReadContext, W extends WriteContext = WriteContext> = ReturnType<\n typeof createBagsyServer<R, W>\n>;\n\ninterface InternalWritable {\n identityKnown: boolean;\n owner(): Owner;\n /** Resolves once any session write this shopper started has finished. */\n settled(): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n\n/** Calls browsers may make, by path and method. Insights and identity stay server-side. */\nconst SEGMENT = '[A-Za-z0-9_-]{1,128}';\nconst ALLOWED: [RegExp, string[]][] = [\n [/^\\/lists$/, ['GET', 'POST']],\n [new RegExp(`^/lists/${SEGMENT}$`), ['GET', 'PATCH', 'DELETE']],\n [new RegExp(`^/lists/${SEGMENT}/items$`), ['GET', 'POST', 'DELETE']],\n [new RegExp(`^/lists/${SEGMENT}/items/${SEGMENT}$`), ['PATCH', 'DELETE']],\n [new RegExp(`^/lists/${SEGMENT}/share$`), ['POST', 'DELETE']],\n [/^\\/saved$/, ['GET']],\n [new RegExp(`^/shared/${SEGMENT}$`), ['GET']],\n [/^\\/health$/, ['GET']],\n];\n\nfunction parseCall(value: unknown): RelayRequest {\n if (typeof value !== 'object' || value === null) throw new TypeError('Expected a JSON object.');\n const { method, path, query, body } = value as Record<string, unknown>;\n if (typeof method !== 'string' || typeof path !== 'string') throw new TypeError('`method` and `path` are required.');\n const rule = ALLOWED.find(([pattern]) => pattern.test(path));\n if (!rule || !rule[1].includes(method)) throw new TypeError(`${method} ${path} is not available to browsers.`);\n const parsed: RelayRequest = { method, path };\n if (query !== undefined) {\n if (typeof query !== 'object' || query === null || Object.values(query).some((v) => typeof v !== 'string')) {\n throw new TypeError('`query` must map names to strings.');\n }\n // Identity comes from the session, never the browser.\n const { visitor_id: _ignored, ...rest } = query as Record<string, string>;\n parsed.query = rest;\n }\n if (body !== undefined) parsed.body = body;\n return parsed;\n}\n\n/** Wraps a session manager to report whether anything was written. */\nexport function trackWrites<SM extends BagsySessionManager>(sessionManager: SM) {\n let dirty = false;\n const tracked: SM = Object.create(sessionManager);\n tracked.getSessionItem = (key) => sessionManager.getSessionItem(key);\n tracked.setSessionItem = (key, value) => ((dirty = true), sessionManager.setSessionItem(key, value));\n tracked.removeSessionItem = (key) => ((dirty = true), sessionManager.removeSessionItem(key));\n return { sessionManager: tracked, dirty: () => dirty };\n}\n\nasync function readSession(sessionManager: ReadonlySessionManager): Promise<BagsySession> {\n const value = await sessionManager.getSessionItem(SESSION_KEY);\n return value && typeof value === 'object' ? { ...(value as BagsySession) } : {};\n}\n\nasync function writeSession(sessionManager: BagsySessionManager, value: BagsySession) {\n if (value.visitorId) await sessionManager.setSessionItem(SESSION_KEY, { visitorId: value.visitorId });\n else await sessionManager.removeSessionItem(SESSION_KEY);\n}\n\nfunction unionLookup(products: ProductRef[], lookups: SavedLookup[]): SavedLookup {\n if (lookups.length === 1) return lookups[0]!;\n const byProduct: Record<string, SavedEntry[]> = {};\n for (const product of products) {\n const id = productGid(product);\n byProduct[id] = lookups.flatMap((lookup) => lookup.get(id));\n }\n return {\n byProduct,\n get: (product) => byProduct[productGid(product)] ?? [],\n has: (product, variant) => lookups.some((lookup) => lookup.has(product, variant)),\n };\n}\n"],"mappings":"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","names":[]}