@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/README.md +131 -28
- package/dist/{chunk-OHSBSGWH.js → chunk-CJV4SIV7.js} +72 -19
- package/dist/chunk-CJV4SIV7.js.map +1 -0
- package/dist/{chunk-OM72NJDX.js → chunk-QV3JPND6.js} +24 -17
- package/dist/{chunk-OM72NJDX.js.map → chunk-QV3JPND6.js.map} +1 -1
- package/dist/client-CXCgpFGa.d.ts +169 -0
- package/dist/element.d.ts +1 -2
- package/dist/element.js +6 -4
- package/dist/element.js.map +1 -1
- package/dist/hydrogen.d.ts +2 -3
- package/dist/hydrogen.js +16 -5
- package/dist/hydrogen.js.map +1 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.js +3 -1
- package/dist/react.d.ts +24 -5
- package/dist/react.js +59 -22
- package/dist/react.js.map +1 -1
- package/dist/{relay-CU6VnXFE.d.ts → relay-0l6VcQp9.d.ts} +1 -2
- package/dist/server.d.ts +3 -4
- package/dist/server.js +2 -2
- package/dist/{types-C1ZgQf5w.d.ts → transport-Chh09xIP.d.ts} +14 -2
- package/package.json +1 -1
- package/dist/chunk-OHSBSGWH.js.map +0 -1
- package/dist/client-2LxFO18p.d.ts +0 -309
- package/dist/transport-DTApDImd.d.ts +0 -16
package/README.md
CHANGED
|
@@ -21,65 +21,109 @@ Everything is ESM, typed and tree-shakeable.
|
|
|
21
21
|
|
|
22
22
|
## Hydrogen
|
|
23
23
|
|
|
24
|
-
Your server relays the browser's calls to Bagsy, adding your key and the shopper's identity from the session.
|
|
24
|
+
Your storefront's server relays the browser's calls to Bagsy, adding your key and the shopper's identity from the session. Browser code never sees your key or a customer id. A guest's id lives in your session cookie, which is HttpOnly, so scripts can't read it. Use an encrypted session if you don't want it readable at all.
|
|
25
|
+
|
|
26
|
+
### 1. Keys
|
|
27
|
+
|
|
28
|
+
Create a server key in **Apps → Bagsy → Developers**, then add it to your storefront's environment:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
BAGSY_API_KEY=bsk_live_…
|
|
32
|
+
BAGSY_API_URL=https://bagsy-api.<your-subdomain>.workers.dev/v1
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. A persistent session
|
|
36
|
+
|
|
37
|
+
Bagsy keeps a guest's wishlist id in your storefront session. Hydrogen's templates fall back to an **in-memory** session when customer accounts aren't configured, so a guest's saves would disappear on the next request. Use a cookie session with `SESSION_SECRET`, even for a guest-only store. Bagsy logs a warning if it writes the session and committing it sets no cookie.
|
|
38
|
+
|
|
39
|
+
### 3. Server
|
|
40
|
+
|
|
41
|
+
Hydrogen gives you `env` per request (Oxygen and MiniOxygen), so create Bagsy in your request handler. It's cheap: caches live at module scope, not on the instance. Register its handlers with `handleShopifyRoutes`, and put it in a router context for your loaders:
|
|
25
42
|
|
|
26
43
|
```ts
|
|
27
44
|
// app/lib/bagsy.server.ts
|
|
28
|
-
import {
|
|
45
|
+
import { createContext } from 'react-router';
|
|
46
|
+
import { createHydrogenBagsy, type HydrogenWriteContext } from '@umxr/bagsy/hydrogen';
|
|
47
|
+
import type { CustomerSession } from '@shopify/hydrogen/customer-account';
|
|
48
|
+
|
|
49
|
+
export function getBagsy(env: Env, customerSession?: CustomerSession) {
|
|
50
|
+
return createHydrogenBagsy({
|
|
51
|
+
apiKey: env.BAGSY_API_KEY,
|
|
52
|
+
baseUrl: env.BAGSY_API_URL,
|
|
53
|
+
// Leave both out for a guest-only storefront.
|
|
54
|
+
...(customerSession && { customerSession, shopId: env.SHOP_ID }),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
29
57
|
|
|
30
|
-
export const
|
|
31
|
-
apiKey: process.env.BAGSY_API_KEY!,
|
|
32
|
-
baseUrl: process.env.BAGSY_API_URL, // https://bagsy-api.<your-subdomain>.workers.dev/v1
|
|
33
|
-
customerSession, // from createCustomerSession(); omit both for guest-only
|
|
34
|
-
shopId: process.env.SHOP_ID!,
|
|
35
|
-
});
|
|
58
|
+
export const bagsyContext = createContext<HydrogenWriteContext & { bagsy: ReturnType<typeof getBagsy> }>();
|
|
36
59
|
```
|
|
37
60
|
|
|
38
61
|
```ts
|
|
39
|
-
//
|
|
40
|
-
|
|
62
|
+
// app/root.tsx: in your existing Hydrogen middleware
|
|
63
|
+
const bagsy = getBagsy(env, customerSession);
|
|
64
|
+
|
|
65
|
+
const shopifyRoute = handleShopifyRoutes({
|
|
41
66
|
request,
|
|
42
67
|
requestContext,
|
|
43
68
|
storefrontClient,
|
|
44
69
|
sessionManager,
|
|
70
|
+
routeTemplates,
|
|
45
71
|
handlers: [cartHandlers, customerAccountHandlers, bagsy.handlers],
|
|
46
72
|
});
|
|
73
|
+
if (shopifyRoute) return shopifyRoute;
|
|
74
|
+
|
|
75
|
+
context.set(bagsyContext, { bagsy, sessionManager, requestContext });
|
|
47
76
|
```
|
|
48
77
|
|
|
78
|
+
The relay commits the session itself when it writes one, like Hydrogen's other handlers.
|
|
79
|
+
|
|
80
|
+
### 4. Provider and header count
|
|
81
|
+
|
|
49
82
|
```tsx
|
|
50
|
-
// root.tsx
|
|
51
|
-
import { BagsyProvider } from '@umxr/bagsy/react';
|
|
83
|
+
// app/root.tsx
|
|
84
|
+
import { BagsyProvider, useWishlistCount } from '@umxr/bagsy/react';
|
|
52
85
|
|
|
53
|
-
export async function loader({ context }) {
|
|
54
|
-
const
|
|
86
|
+
export async function loader({ context }: Route.LoaderArgs) {
|
|
87
|
+
const { bagsy, ...ctx } = context.get(bagsyContext);
|
|
88
|
+
const shopper = await bagsy.shopper(ctx); // read-only: never refreshes tokens or writes the session
|
|
55
89
|
return { wishlistCount: await shopper.count() };
|
|
56
90
|
}
|
|
57
91
|
|
|
58
|
-
export default function App({ loaderData }) {
|
|
92
|
+
export default function App({ loaderData }: Route.ComponentProps) {
|
|
59
93
|
return (
|
|
60
94
|
<BagsyProvider initialCount={loaderData.wishlistCount}>
|
|
61
95
|
<Outlet />
|
|
62
96
|
</BagsyProvider>
|
|
63
97
|
);
|
|
64
98
|
}
|
|
99
|
+
|
|
100
|
+
function HeaderWishlistLink() {
|
|
101
|
+
const count = useWishlistCount();
|
|
102
|
+
return <a href="/wishlist">Wishlist {count ? `(${count})` : null}</a>;
|
|
103
|
+
}
|
|
65
104
|
```
|
|
66
105
|
|
|
106
|
+
`initialCount` follows the root loader as it revalidates. Calling `shopper()` marks the response as personalized, so Hydrogen won't cache it publicly. To keep pages cacheable, leave out `initialCount` and the count loads in the browser.
|
|
107
|
+
|
|
108
|
+
### 5. Hearts
|
|
109
|
+
|
|
67
110
|
```tsx
|
|
68
|
-
// a collection page: one Bagsy call for the whole grid, rendered on the server
|
|
69
111
|
import { WishlistButton, useSeedSaved } from '@umxr/bagsy/react';
|
|
70
112
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
113
|
+
// A collection: one Bagsy call for the whole grid, rendered on the server.
|
|
114
|
+
export async function loader({ context }: Route.LoaderArgs) {
|
|
115
|
+
const products = await getCollectionProducts(context);
|
|
116
|
+
const { bagsy, ...ctx } = context.get(bagsyContext);
|
|
117
|
+
const shopper = await bagsy.shopper(ctx);
|
|
74
118
|
return { products, saved: (await shopper.saved(products.map((p) => p.id))).byProduct };
|
|
75
119
|
}
|
|
76
120
|
|
|
77
|
-
export default function Collection({ loaderData }) {
|
|
121
|
+
export default function Collection({ loaderData }: Route.ComponentProps) {
|
|
78
122
|
useSeedSaved(loaderData.saved);
|
|
79
123
|
return loaderData.products.map((product) => (
|
|
80
124
|
<a key={product.id} href={`/products/${product.handle}`}>
|
|
81
125
|
{product.title}
|
|
82
|
-
<WishlistButton productId={product.id} aria-label=
|
|
126
|
+
<WishlistButton productId={product.id} aria-label="Save to wishlist">
|
|
83
127
|
{({ saved }) => (saved ? '♥' : '♡')}
|
|
84
128
|
</WishlistButton>
|
|
85
129
|
</a>
|
|
@@ -87,16 +131,74 @@ export default function Collection({ loaderData }) {
|
|
|
87
131
|
}
|
|
88
132
|
```
|
|
89
133
|
|
|
90
|
-
|
|
134
|
+
On a product page, pass the selected variant: `<WishlistButton productId={product.id} variantId={selectedVariant.id}>`.
|
|
135
|
+
|
|
136
|
+
Without seeding, hearts still work. Every heart that mounts in the same tick shares one lookup, and a guest who hasn't saved anything costs no API call.
|
|
137
|
+
|
|
138
|
+
**How variants work:**
|
|
139
|
+
- A product saved without a variant (from a card) counts as saved for every variant.
|
|
140
|
+
- A variant heart is filled for that variant, or for a product-level save.
|
|
141
|
+
- Unsaving from a variant heart removes that variant and any product-level save.
|
|
142
|
+
- Unsaving from a product heart removes every variant.
|
|
143
|
+
|
|
144
|
+
### 6. Wishlist page
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { useWishlist } from '@umxr/bagsy/react';
|
|
148
|
+
|
|
149
|
+
export async function loader({ context }: Route.LoaderArgs) {
|
|
150
|
+
const { bagsy, ...ctx } = context.get(bagsyContext);
|
|
151
|
+
const shopper = await bagsy.shopper(ctx);
|
|
152
|
+
const page = await shopper.items({ limit: 24 });
|
|
153
|
+
// Bagsy stores ids; product data stays live from the Storefront API.
|
|
154
|
+
const products = await getProductsByIds(context, page.data.map((item) => item.productId));
|
|
155
|
+
return { page, products };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export default function Wishlist({ loaderData }: Route.ComponentProps) {
|
|
159
|
+
const { items, remove, hasNextPage, loadMore } = useWishlist({ pageSize: 24, initial: loaderData.page });
|
|
160
|
+
// `remove(item)` is optimistic, and hearts elsewhere update too. Items added by
|
|
161
|
+
// loadMore() need their product data fetched, e.g. from a resource route.
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Without JavaScript
|
|
166
|
+
|
|
167
|
+
In an action, `bagsy.writableShopper(ctx)` gives you a client that acts for the shopper. Commit the session with the response:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
export async function action({ request, context }: Route.ActionArgs) {
|
|
171
|
+
const { bagsy, ...ctx } = context.get(bagsyContext);
|
|
172
|
+
const shopper = await bagsy.writableShopper(ctx);
|
|
173
|
+
await shopper.client.items.toggle(String((await request.formData()).get('productId')));
|
|
174
|
+
return redirect(request.headers.get('Referer') ?? '/', { headers: await commitSession() });
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### How identity works
|
|
179
|
+
|
|
180
|
+
| Shopper | Owner sent to Bagsy | Stored |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| Guest, before saving | nothing: reads are answered with no API call | nothing |
|
|
183
|
+
| Guest, after a successful save | a random visitor id | `bagsy` in your session |
|
|
184
|
+
| Signed in | customer GID from the Customer Account API | `bagsy:customer` in the session (the id and a hash of the access token), plus an in-memory cache |
|
|
185
|
+
|
|
186
|
+
Right after sign-in, the first loader merges the guest's wishlist into the customer's on the API. Every read on that page agrees, and the merge runs once. The next relay call or action clears the guest id from the session. If the merge fails, loaders show both lists until it succeeds.
|
|
187
|
+
|
|
188
|
+
### What browsers can call
|
|
189
|
+
|
|
190
|
+
The relay accepts same-origin `POST` requests with `Content-Type: application/json`. Hydrogen itself rejects cross-origin `POST`s before the relay runs. It forwards only these calls:
|
|
191
|
+
|
|
192
|
+
- **Lists:** create, read, rename and delete.
|
|
193
|
+
- **Items:** add, update and remove.
|
|
194
|
+
- **Sharing:** share and unshare a list.
|
|
195
|
+
- **Reads:** saved lookups, shared lists and health.
|
|
91
196
|
|
|
92
|
-
|
|
93
|
-
- **Signed-in customers** are identified through the Customer Account API, once per sign-in.
|
|
94
|
-
- **Signing in** merges the guest's wishlist into the customer's.
|
|
197
|
+
Insights and identity merging are never forwarded, and any `visitor_id` the browser sends is ignored. API errors pass through with their status, so `BagsyError.code` works in the browser.
|
|
95
198
|
|
|
96
|
-
|
|
199
|
+
### Other frameworks
|
|
97
200
|
|
|
98
|
-
|
|
99
|
-
- **Calls:** only the ones a browser should make. Insights and identity merging are never forwarded.
|
|
201
|
+
`@umxr/bagsy/server` does the same without importing Hydrogen. `createBagsyServer({ apiKey, baseUrl, customer })` returns `shopper()`, `writableShopper()`, and `handle(request, ctx)`, a plain `Request → Response | null` handler for `/api/bagsy`. `customer` is `{ read, write }`, two functions that return the signed-in customer's GID.
|
|
100
202
|
|
|
101
203
|
## React
|
|
102
204
|
|
|
@@ -107,6 +209,7 @@ const { items, status, hasNextPage, loadMore, remove } = useWishlist({ pageSize:
|
|
|
107
209
|
```
|
|
108
210
|
|
|
109
211
|
- **Batched lookups:** every heart that mounts in the same tick shares one lookup.
|
|
212
|
+
- **Small:** `@umxr/bagsy/react` only bundles the relay transport, not the server, proxy or account code.
|
|
110
213
|
- **Optimistic toggles:** the UI updates at once and rolls back if the request fails.
|
|
111
214
|
- **Removing:** `remove(item)` on the wishlist page updates hearts and the count elsewhere too.
|
|
112
215
|
- **`<WishlistButton>` is unstyled:** style it with `[aria-pressed]`, `[data-saved]` and `[data-pending]`. It's safe inside product-card links.
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
createBagsy,
|
|
4
4
|
createTransport,
|
|
5
5
|
productGid
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QV3JPND6.js";
|
|
7
7
|
|
|
8
8
|
// ../hydrogen/src/server.ts
|
|
9
9
|
var SESSION_KEY = "bagsy";
|
|
@@ -18,13 +18,27 @@ function createBagsyServer(options) {
|
|
|
18
18
|
};
|
|
19
19
|
const base = createBagsy(serverOptions);
|
|
20
20
|
const transportFor = (owner) => createTransport({ ...serverOptions, ...owner });
|
|
21
|
+
async function mergeGuest(customerId, visitorId) {
|
|
22
|
+
const key = `${customerId}|${visitorId}`;
|
|
23
|
+
if (mergedGuests.has(key)) return true;
|
|
24
|
+
try {
|
|
25
|
+
await base.as({ customerId }).identity.merge(visitorId);
|
|
26
|
+
remember(mergedGuests, key);
|
|
27
|
+
return true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (!(error instanceof BagsyError)) throw error;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
21
33
|
async function shopper(context) {
|
|
22
34
|
context.requestContext.markResponseAsPersonalized?.("Bagsy wishlist");
|
|
23
35
|
const session = await readSession(context.sessionManager);
|
|
24
36
|
const customerId = await options.customer?.read(context);
|
|
25
37
|
const owners = [];
|
|
26
38
|
if (customerId) owners.push(base.as({ customerId }));
|
|
27
|
-
if (session.visitorId
|
|
39
|
+
if (session.visitorId && !(customerId && await mergeGuest(customerId, session.visitorId))) {
|
|
40
|
+
owners.push(base.as({ visitorId: session.visitorId }));
|
|
41
|
+
}
|
|
28
42
|
return {
|
|
29
43
|
customerId,
|
|
30
44
|
async saved(products) {
|
|
@@ -49,32 +63,35 @@ function createBagsyServer(options) {
|
|
|
49
63
|
const session = await readSession(context.sessionManager);
|
|
50
64
|
const customerId = await options.customer?.write(context);
|
|
51
65
|
let write = Promise.resolve();
|
|
52
|
-
if (customerId && session.visitorId) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
delete session.visitorId;
|
|
56
|
-
await writeSession(context.sessionManager, session);
|
|
57
|
-
} catch (error) {
|
|
58
|
-
if (!(error instanceof BagsyError)) throw error;
|
|
59
|
-
}
|
|
66
|
+
if (customerId && session.visitorId && await mergeGuest(customerId, session.visitorId)) {
|
|
67
|
+
delete session.visitorId;
|
|
68
|
+
await writeSession(context.sessionManager, session);
|
|
60
69
|
}
|
|
61
70
|
const identityKnown = Boolean(customerId || session.visitorId);
|
|
71
|
+
let fresh;
|
|
62
72
|
const owner = () => {
|
|
63
73
|
if (customerId) return { customerId };
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
session.visitorId ??= fresh = crypto.randomUUID();
|
|
75
|
+
return { visitorId: session.visitorId };
|
|
76
|
+
};
|
|
77
|
+
const persist = () => {
|
|
78
|
+
if (fresh) {
|
|
66
79
|
write = writeSession(context.sessionManager, session);
|
|
80
|
+
fresh = void 0;
|
|
67
81
|
}
|
|
68
|
-
return
|
|
82
|
+
return write;
|
|
69
83
|
};
|
|
70
84
|
let client;
|
|
71
85
|
return {
|
|
72
86
|
customerId,
|
|
73
87
|
get client() {
|
|
74
|
-
|
|
88
|
+
client ??= base.as(owner());
|
|
89
|
+
void persist();
|
|
90
|
+
return client;
|
|
75
91
|
},
|
|
76
92
|
identityKnown,
|
|
77
93
|
owner,
|
|
94
|
+
persist,
|
|
78
95
|
settled: () => write
|
|
79
96
|
};
|
|
80
97
|
}
|
|
@@ -94,12 +111,19 @@ function createBagsyServer(options) {
|
|
|
94
111
|
return { rejected: { status: 400, code: "invalid_call", message: error instanceof Error ? error.message : "Invalid call." } };
|
|
95
112
|
}
|
|
96
113
|
const who = await writableShopper(context);
|
|
97
|
-
|
|
114
|
+
let owner = {};
|
|
115
|
+
if (who.identityKnown || call.method !== "GET") owner = who.owner();
|
|
116
|
+
else {
|
|
117
|
+
const empty = emptyAnswer(call);
|
|
118
|
+
if (empty) return { response: empty };
|
|
119
|
+
if (!PUBLIC_READ.test(call.path)) owner = { visitorId: crypto.randomUUID() };
|
|
120
|
+
}
|
|
98
121
|
try {
|
|
99
122
|
const { status, data } = await transportFor(owner).send(call.method, call.path, {
|
|
100
123
|
...call.query && { query: call.query },
|
|
101
124
|
...call.body !== void 0 && { body: call.body }
|
|
102
125
|
});
|
|
126
|
+
await who.persist();
|
|
103
127
|
return { response: { status, data } };
|
|
104
128
|
} catch (error) {
|
|
105
129
|
if (!(error instanceof BagsyError)) throw error;
|
|
@@ -114,9 +138,9 @@ function createBagsyServer(options) {
|
|
|
114
138
|
const result = await relay(request, { ...context, sessionManager: tracked.sessionManager });
|
|
115
139
|
const headers = new Headers({ "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
116
140
|
if (tracked.dirty()) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
141
|
+
const committed = new Headers(await context.sessionManager.commit?.() ?? void 0);
|
|
142
|
+
warnIfNotPersisted(committed);
|
|
143
|
+
for (const [name, value] of committed) headers.append(name, value);
|
|
120
144
|
}
|
|
121
145
|
return "rejected" in result ? new Response(JSON.stringify({ error: { code: result.rejected.code, message: result.rejected.message } }), {
|
|
122
146
|
status: result.rejected.status,
|
|
@@ -126,6 +150,33 @@ function createBagsyServer(options) {
|
|
|
126
150
|
const writable = writableShopper;
|
|
127
151
|
return { endpoint, shopper, writableShopper: writable, relay, handle };
|
|
128
152
|
}
|
|
153
|
+
var mergedGuests = /* @__PURE__ */ new Set();
|
|
154
|
+
var CACHE_LIMIT = 1e3;
|
|
155
|
+
function remember(cache, key, value) {
|
|
156
|
+
if (cache.size >= CACHE_LIMIT) cache.delete(cache.keys().next().value);
|
|
157
|
+
if (cache instanceof Map) cache.set(key, value);
|
|
158
|
+
else cache.add(key);
|
|
159
|
+
}
|
|
160
|
+
var warned = false;
|
|
161
|
+
function warnIfNotPersisted(headers) {
|
|
162
|
+
if (warned || headers.has("Set-Cookie")) return;
|
|
163
|
+
warned = true;
|
|
164
|
+
console.warn(
|
|
165
|
+
"Bagsy: the session was changed but committing it set no cookie, so guest wishlists will not persist. Use a persistent session (e.g. a signed cookie session with SESSION_SECRET), even without customer accounts."
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
var PUBLIC_READ = /^\/(?:shared\/|health$)/;
|
|
169
|
+
function emptyAnswer(call) {
|
|
170
|
+
if (call.path === "/saved") {
|
|
171
|
+
const ids = (call.query?.productIds ?? "").split(",").filter(Boolean);
|
|
172
|
+
return { status: 200, data: { data: Object.fromEntries(ids.map((id) => [id, []])) } };
|
|
173
|
+
}
|
|
174
|
+
if (call.path === "/lists") return { status: 200, data: { data: [] } };
|
|
175
|
+
if (call.path === "/lists/default/items") {
|
|
176
|
+
return { status: 200, data: { data: [], pageInfo: { hasNextPage: false, endCursor: null } } };
|
|
177
|
+
}
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
129
180
|
var SEGMENT = "[A-Za-z0-9_-]{1,128}";
|
|
130
181
|
var ALLOWED = [
|
|
131
182
|
[/^\/lists$/, ["GET", "POST"]],
|
|
@@ -186,6 +237,8 @@ function unionLookup(products, lookups) {
|
|
|
186
237
|
|
|
187
238
|
export {
|
|
188
239
|
createBagsyServer,
|
|
240
|
+
remember,
|
|
241
|
+
warnIfNotPersisted,
|
|
189
242
|
trackWrites
|
|
190
243
|
};
|
|
191
|
-
//# sourceMappingURL=chunk-
|
|
244
|
+
//# sourceMappingURL=chunk-CJV4SIV7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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. Cheap to create:\n * make one per request from your env, or one at module scope. Call\n * `shopper()` in loaders and `relay`/`handle` for the 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 /** Fold a guest list into the customer's. Idempotent; remembered per isolate so it runs once. */\n async function mergeGuest(customerId: string, visitorId: string): Promise<boolean> {\n const key = `${customerId}|${visitorId}`;\n if (mergedGuests.has(key)) return true;\n try {\n await base.as({ customerId }).identity.merge(visitorId);\n remember(mergedGuests, key);\n return true;\n } catch (error) {\n if (!(error instanceof BagsyError)) throw error;\n return false;\n }\n }\n\n /**\n * The shopper for this request. Never refreshes tokens or writes the\n * session, so it's safe in loaders and server components. Right after\n * sign-in it merges the guest list on the API, so every read agrees.\n */\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 const owners: Bagsy[] = [];\n if (customerId) owners.push(base.as({ customerId }));\n // The session still holds the guest id until a write path clears it.\n // If the merge failed, show both so nothing seems to vanish.\n if (session.visitorId && !(customerId && (await mergeGuest(customerId, session.visitorId)))) {\n owners.push(base.as({ visitorId: session.visitorId }));\n }\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 // With an unmerged guest list, the customer's comes first; the guest's\n // items appear once the merge succeeds.\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 // Keep the guest id if the merge fails; the next write retries.\n if (customerId && session.visitorId && (await mergeGuest(customerId, session.visitorId))) {\n delete session.visitorId;\n await writeSession(context.sessionManager, session);\n }\n\n const identityKnown = Boolean(customerId || session.visitorId);\n let fresh: string | undefined;\n /** Who to act as. A guest without a wishlist gets a new id, saved by `persist()`. */\n const owner = (): Owner => {\n if (customerId) return { customerId };\n session.visitorId ??= fresh = crypto.randomUUID();\n return { visitorId: session.visitorId };\n };\n const persist = () => {\n if (fresh) {\n write = writeSession(context.sessionManager, session);\n fresh = undefined;\n }\n return write;\n };\n let client: Bagsy | undefined;\n\n return {\n customerId,\n get client() {\n // Actions can't tell us whether their call succeeded, so save the id now.\n client ??= base.as(owner());\n void persist();\n return client;\n },\n identityKnown,\n owner,\n persist,\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 let owner: Owner = {};\n if (who.identityKnown || call.method !== 'GET') owner = who.owner();\n else {\n // A guest who has never saved: answer reads here, with no API call and\n // no session. Public reads need no owner.\n const empty = emptyAnswer(call);\n if (empty) return { response: empty };\n if (!PUBLIC_READ.test(call.path)) owner = { visitorId: crypto.randomUUID() };\n }\n\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 // Only now is a new guest id worth keeping.\n await who.persist();\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 const committed = new Headers((await context.sessionManager.commit?.()) ?? undefined);\n warnIfNotPersisted(committed);\n for (const [name, value] of committed) headers.append(name, value);\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 /** Save a newly created guest id to the session. */\n persist(): Promise<void>;\n /** Resolves once any session write this shopper started has finished. */\n settled(): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n\n/** Guests merged this isolate, and customer ids by token: bounded caches that survive across requests. */\nconst mergedGuests = new Set<string>();\nconst CACHE_LIMIT = 1000;\nexport function remember<K>(cache: Set<K> | Map<K, unknown>, key: K, value?: unknown) {\n if (cache.size >= CACHE_LIMIT) cache.delete(cache.keys().next().value as K);\n if (cache instanceof Map) cache.set(key, value);\n else cache.add(key);\n}\n\nlet warned = false;\n/**\n * Warn once if Bagsy wrote the session but committing it produced no cookie.\n * Hydrogen's in-memory session (the default without customer accounts) does\n * this, and guest wishlists then vanish on the next request.\n */\nexport function warnIfNotPersisted(headers: Headers) {\n if (warned || headers.has('Set-Cookie')) return;\n warned = true;\n console.warn(\n 'Bagsy: the session was changed but committing it set no cookie, so guest wishlists will not persist. ' +\n 'Use a persistent session (e.g. a signed cookie session with SESSION_SECRET), even without customer accounts.',\n );\n}\n\nconst PUBLIC_READ = /^\\/(?:shared\\/|health$)/;\n\n/** What the API returns for an owner with nothing saved, for calls guests make on every page. */\nfunction emptyAnswer(call: RelayRequest): RelayResponse | undefined {\n if (call.path === '/saved') {\n const ids = (call.query?.productIds ?? '').split(',').filter(Boolean);\n return { status: 200, data: { data: Object.fromEntries(ids.map((id) => [id, []])) } };\n }\n if (call.path === '/lists') return { status: 200, data: { data: [] } };\n if (call.path === '/lists/default/items') {\n return { status: 200, data: { data: [], pageInfo: { hasNextPage: false, endCursor: null } } };\n }\n return undefined;\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":[]}
|
|
@@ -229,7 +229,27 @@ function isRelayResponse(value) {
|
|
|
229
229
|
|
|
230
230
|
// ../core/src/client.ts
|
|
231
231
|
function createBagsy(options) {
|
|
232
|
-
|
|
232
|
+
if (options.mode === "relay") return createRelayClient(options);
|
|
233
|
+
const t = options.mode === "account" ? createAccountTransport(options) : createTransport(options);
|
|
234
|
+
return clientFor(t, (owner) => {
|
|
235
|
+
if (options.mode === "account") {
|
|
236
|
+
throw new TypeError("bagsy.as: in customer accounts the session token identifies the customer.");
|
|
237
|
+
}
|
|
238
|
+
if (options.mode === "proxy") {
|
|
239
|
+
if (owner.customerId !== void 0) {
|
|
240
|
+
throw new TypeError("bagsy.as: on the app proxy the customer comes from Shopify; pass only `visitorId`.");
|
|
241
|
+
}
|
|
242
|
+
return createBagsy({ ...options, visitorId: owner.visitorId ?? options.visitorId });
|
|
243
|
+
}
|
|
244
|
+
return createBagsy({ ...options, ...owner });
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function createRelayClient(options = {}) {
|
|
248
|
+
return clientFor(createRelayTransport({ ...options, mode: "relay" }), () => {
|
|
249
|
+
throw new TypeError("bagsy.as: through a relay, your server's session identifies the shopper.");
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function clientFor(t, as) {
|
|
233
253
|
const items = {
|
|
234
254
|
list: (listId, page) => listItems(t, listId, page),
|
|
235
255
|
/** Every item in a list, fetched page by page. */
|
|
@@ -277,21 +297,7 @@ function createBagsy(options) {
|
|
|
277
297
|
* A copy of this client acting for a specific shopper. In Hydrogen, call
|
|
278
298
|
* once per request with the customer from your Customer Account session.
|
|
279
299
|
*/
|
|
280
|
-
as
|
|
281
|
-
if (options.mode === "account") {
|
|
282
|
-
throw new TypeError("bagsy.as: in customer accounts the session token identifies the customer.");
|
|
283
|
-
}
|
|
284
|
-
if (options.mode === "relay") {
|
|
285
|
-
throw new TypeError("bagsy.as: through a relay, your server's session identifies the shopper.");
|
|
286
|
-
}
|
|
287
|
-
if (options.mode === "proxy") {
|
|
288
|
-
if (owner.customerId !== void 0) {
|
|
289
|
-
throw new TypeError("bagsy.as: on the app proxy the customer comes from Shopify; pass only `visitorId`.");
|
|
290
|
-
}
|
|
291
|
-
return createBagsy({ ...options, visitorId: owner.visitorId ?? options.visitorId });
|
|
292
|
-
}
|
|
293
|
-
return createBagsy({ ...options, ...owner });
|
|
294
|
-
}
|
|
300
|
+
as
|
|
295
301
|
};
|
|
296
302
|
}
|
|
297
303
|
|
|
@@ -361,8 +367,9 @@ export {
|
|
|
361
367
|
createAccountTransport,
|
|
362
368
|
createRelayTransport,
|
|
363
369
|
createBagsy,
|
|
370
|
+
createRelayClient,
|
|
364
371
|
getVisitorId,
|
|
365
372
|
peekVisitorId,
|
|
366
373
|
clearVisitorId
|
|
367
374
|
};
|
|
368
|
-
//# sourceMappingURL=chunk-
|
|
375
|
+
//# sourceMappingURL=chunk-QV3JPND6.js.map
|