@umxr/bagsy 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +257 -2
- package/dist/chunk-CJV4SIV7.js +244 -0
- package/dist/chunk-CJV4SIV7.js.map +1 -0
- package/dist/chunk-QV3JPND6.js +375 -0
- package/dist/chunk-QV3JPND6.js.map +1 -0
- package/dist/client-CXCgpFGa.d.ts +169 -0
- package/dist/element.d.ts +169 -0
- package/dist/element.js +566 -0
- package/dist/element.js.map +1 -0
- package/dist/hydrogen.d.ts +73 -0
- package/dist/hydrogen.js +89 -0
- package/dist/hydrogen.js.map +1 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +174 -0
- package/dist/react.js +403 -0
- package/dist/react.js.map +1 -0
- package/dist/relay-0l6VcQp9.d.ts +25 -0
- package/dist/server.d.ts +96 -0
- package/dist/server.js +10 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +157 -0
- package/dist/transport-Chh09xIP.d.ts +1022 -0
- package/package.json +89 -4
package/dist/react.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRelayClient,
|
|
3
|
+
productGid,
|
|
4
|
+
variantGid
|
|
5
|
+
} from "./chunk-QV3JPND6.js";
|
|
6
|
+
|
|
7
|
+
// ../react/src/react.tsx
|
|
8
|
+
import {
|
|
9
|
+
createContext,
|
|
10
|
+
useCallback,
|
|
11
|
+
useContext,
|
|
12
|
+
useEffect,
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
useSyncExternalStore
|
|
17
|
+
} from "react";
|
|
18
|
+
|
|
19
|
+
// ../react/src/store.ts
|
|
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
|
+
}
|
|
24
|
+
var BagsyStore = class {
|
|
25
|
+
constructor(client, events = {}) {
|
|
26
|
+
this.client = client;
|
|
27
|
+
this.events = events;
|
|
28
|
+
}
|
|
29
|
+
client;
|
|
30
|
+
events;
|
|
31
|
+
saved = /* @__PURE__ */ new Map();
|
|
32
|
+
pendingToggles = /* @__PURE__ */ new Map();
|
|
33
|
+
queued = /* @__PURE__ */ new Set();
|
|
34
|
+
flushing;
|
|
35
|
+
productListeners = /* @__PURE__ */ new Map();
|
|
36
|
+
countListeners = /* @__PURE__ */ new Set();
|
|
37
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
38
|
+
count;
|
|
39
|
+
countRequest;
|
|
40
|
+
setEvents(events) {
|
|
41
|
+
this.events = events;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Server-rendered saved state, e.g. `shopper.saved(ids).byProduct`. Never
|
|
45
|
+
* overwrites state the client already has. Returns the products it filled
|
|
46
|
+
* in; pass `notify: false` while rendering and call `notify()` after.
|
|
47
|
+
*/
|
|
48
|
+
seed(byProduct, { notify = true } = {}) {
|
|
49
|
+
if (!byProduct) return [];
|
|
50
|
+
const filled = [];
|
|
51
|
+
for (const [id, entries] of Object.entries(byProduct)) {
|
|
52
|
+
if (this.saved.has(id) || this.pendingToggles.has(id)) continue;
|
|
53
|
+
this.saved.set(id, entries);
|
|
54
|
+
filled.push(id);
|
|
55
|
+
}
|
|
56
|
+
if (notify) this.notify(...filled);
|
|
57
|
+
return filled;
|
|
58
|
+
}
|
|
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;
|
|
65
|
+
this.count = count;
|
|
66
|
+
this.countRequest = Promise.resolve(count);
|
|
67
|
+
if (notify) for (const listener of this.countListeners) listener();
|
|
68
|
+
}
|
|
69
|
+
/** Current state. Stable object identity until it changes, for useSyncExternalStore. */
|
|
70
|
+
getState(product, variant) {
|
|
71
|
+
const id = productGid(product);
|
|
72
|
+
const vid = variant === void 0 ? void 0 : variantGid(variant);
|
|
73
|
+
const key = vid ? `${id}|${vid}` : id;
|
|
74
|
+
const entries = this.saved.get(id);
|
|
75
|
+
const next = entries ? {
|
|
76
|
+
saved: matching(entries, vid).length > 0,
|
|
77
|
+
ready: true,
|
|
78
|
+
pending: this.pendingToggles.has(id)
|
|
79
|
+
} : UNKNOWN;
|
|
80
|
+
const previous = this.snapshots.get(key);
|
|
81
|
+
if (previous && previous.saved === next.saved && previous.ready === next.ready && previous.pending === next.pending) {
|
|
82
|
+
return previous;
|
|
83
|
+
}
|
|
84
|
+
this.snapshots.set(key, next);
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
/** Subscribe to one product. Loads its state (batched) if unknown. */
|
|
88
|
+
subscribe(product, listener) {
|
|
89
|
+
const id = productGid(product);
|
|
90
|
+
let set = this.productListeners.get(id);
|
|
91
|
+
if (!set) this.productListeners.set(id, set = /* @__PURE__ */ new Set());
|
|
92
|
+
set.add(listener);
|
|
93
|
+
if (!this.saved.has(id)) this.queue(id);
|
|
94
|
+
return () => {
|
|
95
|
+
set.delete(listener);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
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
|
+
*/
|
|
108
|
+
async toggle(product, variant) {
|
|
109
|
+
const id = productGid(product);
|
|
110
|
+
const vid = variant === void 0 ? void 0 : variantGid(variant);
|
|
111
|
+
if (this.pendingToggles.has(id)) return this.getState(id, vid).saved;
|
|
112
|
+
const previous = this.saved.get(id) ?? [];
|
|
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);
|
|
119
|
+
this.notify(id);
|
|
120
|
+
try {
|
|
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
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const saved = !wasSaved;
|
|
133
|
+
this.events.onChange?.({ productId: id, ...vid && { variantId: vid }, saved });
|
|
134
|
+
return saved;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
this.saved.set(id, previous);
|
|
137
|
+
this.adjustCount(wasSaved ? matched.length : -1);
|
|
138
|
+
this.events.onError?.(error);
|
|
139
|
+
this.queue(id);
|
|
140
|
+
throw error;
|
|
141
|
+
} finally {
|
|
142
|
+
this.pendingToggles.delete(id);
|
|
143
|
+
this.notify(id);
|
|
144
|
+
}
|
|
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
|
+
}
|
|
159
|
+
/** Record an item removed elsewhere (e.g. the wishlist page), so hearts update without a request. */
|
|
160
|
+
forget(product, itemId) {
|
|
161
|
+
const id = productGid(product);
|
|
162
|
+
const entries = this.saved.get(id);
|
|
163
|
+
if (entries) {
|
|
164
|
+
const left = entries.filter((e) => e.itemId !== itemId);
|
|
165
|
+
if (left.length !== entries.length) {
|
|
166
|
+
this.saved.set(id, left);
|
|
167
|
+
this.notify(id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
this.adjustCount(-1);
|
|
171
|
+
}
|
|
172
|
+
getCount() {
|
|
173
|
+
return this.count;
|
|
174
|
+
}
|
|
175
|
+
subscribeCount(listener) {
|
|
176
|
+
this.countListeners.add(listener);
|
|
177
|
+
void this.ensureCount().catch(() => {
|
|
178
|
+
});
|
|
179
|
+
return () => {
|
|
180
|
+
this.countListeners.delete(listener);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** Loads the default list's item count once; later calls share it. */
|
|
184
|
+
ensureCount() {
|
|
185
|
+
return this.countRequest ??= this.loadCount();
|
|
186
|
+
}
|
|
187
|
+
refreshCount() {
|
|
188
|
+
this.countRequest = this.loadCount();
|
|
189
|
+
void this.countRequest.catch(() => {
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async loadCount() {
|
|
193
|
+
try {
|
|
194
|
+
const lists = await this.client.lists.list();
|
|
195
|
+
this.count = lists.find((list) => list.isDefault)?.itemCount ?? 0;
|
|
196
|
+
for (const listener of this.countListeners) listener();
|
|
197
|
+
return this.count;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
this.countRequest = void 0;
|
|
200
|
+
this.events.onError?.(error);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
adjustCount(delta) {
|
|
205
|
+
if (this.count === void 0) return;
|
|
206
|
+
this.count = Math.max(0, this.count + delta);
|
|
207
|
+
for (const listener of this.countListeners) listener();
|
|
208
|
+
}
|
|
209
|
+
queue(id) {
|
|
210
|
+
this.queued.add(id);
|
|
211
|
+
this.flushing ??= Promise.resolve().then(async () => {
|
|
212
|
+
const ids = [...this.queued];
|
|
213
|
+
this.queued.clear();
|
|
214
|
+
this.flushing = void 0;
|
|
215
|
+
try {
|
|
216
|
+
const lookup = await this.client.saved(ids);
|
|
217
|
+
for (const pid of ids) {
|
|
218
|
+
if (this.pendingToggles.has(pid)) continue;
|
|
219
|
+
this.saved.set(pid, lookup.get(pid));
|
|
220
|
+
this.notify(pid);
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
this.events.onError?.(error);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/** Tell subscribers of these products their state may have changed. */
|
|
228
|
+
notify(...ids) {
|
|
229
|
+
for (const id of ids) for (const listener of this.productListeners.get(id) ?? []) listener();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// ../react/src/react.tsx
|
|
234
|
+
import { jsx } from "react/jsx-runtime";
|
|
235
|
+
var BagsyContext = createContext(null);
|
|
236
|
+
function BagsyProvider({
|
|
237
|
+
endpoint,
|
|
238
|
+
client,
|
|
239
|
+
initialSaved,
|
|
240
|
+
initialCount,
|
|
241
|
+
onChange,
|
|
242
|
+
onError,
|
|
243
|
+
children
|
|
244
|
+
}) {
|
|
245
|
+
const [store] = useState(() => {
|
|
246
|
+
const created = new BagsyStore(client ?? createRelayClient(endpoint ? { endpoint } : {}));
|
|
247
|
+
created.seed(initialSaved);
|
|
248
|
+
created.seedCount(initialCount);
|
|
249
|
+
return created;
|
|
250
|
+
});
|
|
251
|
+
useEffect(() => store.seedCount(initialCount), [store, initialCount]);
|
|
252
|
+
store.setEvents({ ...onChange && { onChange }, ...onError && { onError } });
|
|
253
|
+
useSeedInto(store, initialSaved);
|
|
254
|
+
return /* @__PURE__ */ jsx(BagsyContext.Provider, { value: store, children });
|
|
255
|
+
}
|
|
256
|
+
function useBagsy() {
|
|
257
|
+
const store = useContext(BagsyContext);
|
|
258
|
+
if (!store) throw new Error("Bagsy hooks need a <BagsyProvider> above them.");
|
|
259
|
+
return { store, client: store.client };
|
|
260
|
+
}
|
|
261
|
+
function useSeedSaved(byProduct) {
|
|
262
|
+
useSeedInto(useBagsy().store, byProduct);
|
|
263
|
+
}
|
|
264
|
+
function useSeedInto(store, byProduct) {
|
|
265
|
+
const seeded = useRef(void 0);
|
|
266
|
+
const filled = useRef([]);
|
|
267
|
+
if (byProduct && seeded.current !== byProduct) {
|
|
268
|
+
seeded.current = byProduct;
|
|
269
|
+
filled.current = store.seed(byProduct, { notify: false });
|
|
270
|
+
}
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (!filled.current.length) return;
|
|
273
|
+
store.notify(...filled.current);
|
|
274
|
+
filled.current = [];
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function useSaved(product, options = {}) {
|
|
278
|
+
const { store } = useBagsy();
|
|
279
|
+
const { variantId } = options;
|
|
280
|
+
const subscribe = useCallback((listener) => store.subscribe(product, listener), [store, product]);
|
|
281
|
+
const getSnapshot = () => store.getState(product, variantId);
|
|
282
|
+
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
283
|
+
const toggle = useCallback(() => store.toggle(product, variantId), [store, product, variantId]);
|
|
284
|
+
return useMemo(() => ({ ...state, toggle }), [state, toggle]);
|
|
285
|
+
}
|
|
286
|
+
function useWishlistCount() {
|
|
287
|
+
const { store } = useBagsy();
|
|
288
|
+
const subscribe = useCallback((listener) => store.subscribeCount(listener), [store]);
|
|
289
|
+
const getSnapshot = () => store.getCount();
|
|
290
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
291
|
+
}
|
|
292
|
+
function useWishlist({ listId = "default", pageSize = 24, initial } = {}) {
|
|
293
|
+
const { store, client } = useBagsy();
|
|
294
|
+
const [items, setItems] = useState(initial?.data ?? []);
|
|
295
|
+
const [cursor, setCursor] = useState(initial?.pageInfo.endCursor);
|
|
296
|
+
const [hasNextPage, setHasNextPage] = useState(initial?.pageInfo.hasNextPage ?? false);
|
|
297
|
+
const [status, setStatus] = useState(initial ? "ready" : "loading");
|
|
298
|
+
const [error, setError] = useState(void 0);
|
|
299
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
300
|
+
const generation = useRef(0);
|
|
301
|
+
const skipFirstLoad = useRef(Boolean(initial));
|
|
302
|
+
const refresh = useCallback(async () => {
|
|
303
|
+
const mine = ++generation.current;
|
|
304
|
+
setStatus((s) => s === "ready" ? s : "loading");
|
|
305
|
+
try {
|
|
306
|
+
const page = await client.items.list(listId, { limit: pageSize });
|
|
307
|
+
if (mine !== generation.current) return;
|
|
308
|
+
setItems(page.data);
|
|
309
|
+
setCursor(page.pageInfo.endCursor);
|
|
310
|
+
setHasNextPage(page.pageInfo.hasNextPage);
|
|
311
|
+
setStatus("ready");
|
|
312
|
+
setError(void 0);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
if (mine !== generation.current) return;
|
|
315
|
+
setError(e);
|
|
316
|
+
setStatus("error");
|
|
317
|
+
}
|
|
318
|
+
}, [client, listId, pageSize]);
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (skipFirstLoad.current) {
|
|
321
|
+
skipFirstLoad.current = false;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
void refresh();
|
|
325
|
+
}, [refresh]);
|
|
326
|
+
const loadMore = useCallback(async () => {
|
|
327
|
+
if (!hasNextPage || loadingMore || !cursor) return;
|
|
328
|
+
setLoadingMore(true);
|
|
329
|
+
const mine = generation.current;
|
|
330
|
+
try {
|
|
331
|
+
const page = await client.items.list(listId, { limit: pageSize, after: cursor });
|
|
332
|
+
if (mine !== generation.current) return;
|
|
333
|
+
setItems((current) => [...current, ...page.data.filter((i) => !current.some((c) => c.id === i.id))]);
|
|
334
|
+
setCursor(page.pageInfo.endCursor);
|
|
335
|
+
setHasNextPage(page.pageInfo.hasNextPage);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
setError(e);
|
|
338
|
+
} finally {
|
|
339
|
+
setLoadingMore(false);
|
|
340
|
+
}
|
|
341
|
+
}, [client, listId, pageSize, cursor, hasNextPage, loadingMore]);
|
|
342
|
+
const remove = useCallback(
|
|
343
|
+
async (item) => {
|
|
344
|
+
setItems((current) => current.filter((i) => i.id !== item.id));
|
|
345
|
+
try {
|
|
346
|
+
await client.items.remove(item.id, { listId });
|
|
347
|
+
store.forget(item.productId, item.id);
|
|
348
|
+
} catch (e) {
|
|
349
|
+
setItems((current) => current.some((i) => i.id === item.id) ? current : insertByDate(current, item));
|
|
350
|
+
throw e;
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
[client, store, listId]
|
|
354
|
+
);
|
|
355
|
+
return { items, status, error, hasNextPage, loadingMore, loadMore, remove, refresh };
|
|
356
|
+
}
|
|
357
|
+
function insertByDate(items, item) {
|
|
358
|
+
const index = items.findIndex((i) => i.addedAt < item.addedAt);
|
|
359
|
+
return index === -1 ? [...items, item] : [...items.slice(0, index), item, ...items.slice(index)];
|
|
360
|
+
}
|
|
361
|
+
function WishlistButton({
|
|
362
|
+
productId,
|
|
363
|
+
variantId,
|
|
364
|
+
children,
|
|
365
|
+
onToggle,
|
|
366
|
+
onError,
|
|
367
|
+
onClick,
|
|
368
|
+
...rest
|
|
369
|
+
}) {
|
|
370
|
+
const state = useSaved(productId, { variantId });
|
|
371
|
+
const handleClick = (event) => {
|
|
372
|
+
onClick?.(event);
|
|
373
|
+
if (event.defaultPrevented) return;
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
event.stopPropagation();
|
|
376
|
+
state.toggle().then(onToggle, (error) => onError?.(error));
|
|
377
|
+
};
|
|
378
|
+
return /* @__PURE__ */ jsx(
|
|
379
|
+
"button",
|
|
380
|
+
{
|
|
381
|
+
type: "button",
|
|
382
|
+
"aria-pressed": state.saved,
|
|
383
|
+
"data-saved": state.saved ? "" : void 0,
|
|
384
|
+
"data-pending": state.pending ? "" : void 0,
|
|
385
|
+
"data-ready": state.ready ? "" : void 0,
|
|
386
|
+
"data-product-id": productGid(productId),
|
|
387
|
+
onClick: handleClick,
|
|
388
|
+
...rest,
|
|
389
|
+
children: typeof children === "function" ? children(state) : children ?? (state.saved ? "Saved" : "Save")
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
export {
|
|
394
|
+
BagsyProvider,
|
|
395
|
+
BagsyStore,
|
|
396
|
+
WishlistButton,
|
|
397
|
+
useBagsy,
|
|
398
|
+
useSaved,
|
|
399
|
+
useSeedSaved,
|
|
400
|
+
useWishlist,
|
|
401
|
+
useWishlistCount
|
|
402
|
+
};
|
|
403
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +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 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":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { R as RelayOptions, T as Transport } from './transport-Chh09xIP.js';
|
|
2
|
+
|
|
3
|
+
/** One Bagsy API call, as the browser sends it to your server's relay endpoint. */
|
|
4
|
+
interface RelayRequest {
|
|
5
|
+
method: string;
|
|
6
|
+
/** API path below `/v1`, e.g. `/lists/default/items`. */
|
|
7
|
+
path: string;
|
|
8
|
+
query?: Record<string, string>;
|
|
9
|
+
body?: unknown;
|
|
10
|
+
}
|
|
11
|
+
/** The API's answer, passed back unchanged so errors keep their status and problem. */
|
|
12
|
+
interface RelayResponse<T = unknown> {
|
|
13
|
+
status: number;
|
|
14
|
+
data: T;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Transport for browsers on headless storefronts (Hydrogen and other React
|
|
18
|
+
* apps). Each call goes to one endpoint on your own server, which adds the
|
|
19
|
+
* secret key and the shopper's identity from its session, then calls Bagsy.
|
|
20
|
+
* The browser never holds a key, a customer id or a guest id.
|
|
21
|
+
*/
|
|
22
|
+
declare function createRelayTransport(options: RelayOptions): Transport;
|
|
23
|
+
|
|
24
|
+
export { createRelayTransport as c };
|
|
25
|
+
export type { RelayResponse as R, RelayRequest as a };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Bagsy } from './index.js';
|
|
2
|
+
import { ProductRef, SavedLookup, PageOptions, ItemPage } from './index.js';
|
|
3
|
+
import { RelayResponse } from './index.js';
|
|
4
|
+
|
|
5
|
+
type Awaitable<T> = T | Promise<T>;
|
|
6
|
+
/** A session you can only read: what loaders and server components get. */
|
|
7
|
+
interface ReadonlySessionManager {
|
|
8
|
+
getSessionItem(key: string): Awaitable<unknown>;
|
|
9
|
+
}
|
|
10
|
+
/** A session you can write and commit. Same shape as Hydrogen's `ShopifyRouteSessionManager`. */
|
|
11
|
+
interface BagsySessionManager extends ReadonlySessionManager {
|
|
12
|
+
setSessionItem(key: string, value: unknown): Awaitable<void>;
|
|
13
|
+
removeSessionItem(key: string): Awaitable<void>;
|
|
14
|
+
/** Saves changes and returns headers (a cookie) for the response. */
|
|
15
|
+
commit?(): Awaitable<HeadersInit | void>;
|
|
16
|
+
}
|
|
17
|
+
/** The parts of Hydrogen's `ShopifyRequestContext` Bagsy uses. */
|
|
18
|
+
interface BagsyRequestContext {
|
|
19
|
+
markResponseAsPersonalized?(reason: string): void;
|
|
20
|
+
}
|
|
21
|
+
/** For reads (loaders): in Hydrogen, your request's session manager and `requestContext`. */
|
|
22
|
+
interface ReadContext {
|
|
23
|
+
sessionManager: ReadonlySessionManager;
|
|
24
|
+
requestContext: BagsyRequestContext;
|
|
25
|
+
}
|
|
26
|
+
/** For writes: only where the response can commit the session (route handlers, actions). */
|
|
27
|
+
interface WriteContext extends ReadContext {
|
|
28
|
+
sessionManager: BagsySessionManager;
|
|
29
|
+
}
|
|
30
|
+
/** How Bagsy learns who is signed in. `hydrogenCustomerId()` implements it for Hydrogen. */
|
|
31
|
+
interface CustomerResolver<R extends ReadContext = ReadContext, W extends WriteContext = WriteContext> {
|
|
32
|
+
/** Without refreshing tokens or writing the session. */
|
|
33
|
+
read(context: R): Promise<string | undefined>;
|
|
34
|
+
/** May refresh tokens and cache in the session; the caller commits. */
|
|
35
|
+
write(context: W): Promise<string | undefined>;
|
|
36
|
+
}
|
|
37
|
+
interface CreateBagsyServerOptions<R extends ReadContext = ReadContext, W extends WriteContext = WriteContext> {
|
|
38
|
+
/** A server key from the Bagsy admin (Apps → Bagsy → Developers). Keep it server-side. */
|
|
39
|
+
apiKey: string;
|
|
40
|
+
/** Bagsy API base URL, e.g. `https://bagsy-api.example.workers.dev/v1`. */
|
|
41
|
+
baseUrl?: string;
|
|
42
|
+
/** Who is signed in. Omit for a guest-only storefront. */
|
|
43
|
+
customer?: CustomerResolver<R, W>;
|
|
44
|
+
/** Path the browser calls; `@umxr/bagsy/react` uses the same default. @default '/api/bagsy' */
|
|
45
|
+
endpoint?: string;
|
|
46
|
+
fetch?: typeof globalThis.fetch;
|
|
47
|
+
}
|
|
48
|
+
/** Read-only view of the shopper, for loaders and server rendering. Never writes the session. */
|
|
49
|
+
interface Shopper {
|
|
50
|
+
/** Signed-in customer GID, if any. */
|
|
51
|
+
readonly customerId: string | undefined;
|
|
52
|
+
/** Saved state for products. Guests who have never saved cost no API call. Pass `.byProduct` to `@umxr/bagsy/react`. */
|
|
53
|
+
saved(products: Iterable<ProductRef>): Promise<SavedLookup>;
|
|
54
|
+
/** Items in the default list, for a header badge. */
|
|
55
|
+
count(): Promise<number>;
|
|
56
|
+
/** A page of the default list, newest first. Pass to `useWishlist({ initial })`. */
|
|
57
|
+
items(page?: PageOptions): Promise<ItemPage>;
|
|
58
|
+
}
|
|
59
|
+
/** The shopper with write access, for actions and route handlers that commit the session. */
|
|
60
|
+
interface WritableShopper {
|
|
61
|
+
readonly customerId: string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* A Bagsy client acting for this shopper. A guest without a wishlist gets
|
|
64
|
+
* an id on first use, stored in the session: commit it with the response.
|
|
65
|
+
*/
|
|
66
|
+
readonly client: Bagsy;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
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.
|
|
72
|
+
*/
|
|
73
|
+
declare function createBagsyServer<R extends ReadContext = ReadContext, W extends WriteContext = WriteContext>(options: CreateBagsyServerOptions<R, W>): {
|
|
74
|
+
endpoint: string;
|
|
75
|
+
shopper: (context: R) => Promise<Shopper>;
|
|
76
|
+
writableShopper: (context: W) => Promise<WritableShopper>;
|
|
77
|
+
relay: (request: Request, context: W) => Promise<{
|
|
78
|
+
response: RelayResponse;
|
|
79
|
+
} | {
|
|
80
|
+
rejected: {
|
|
81
|
+
status: number;
|
|
82
|
+
code: string;
|
|
83
|
+
message: string;
|
|
84
|
+
};
|
|
85
|
+
}>;
|
|
86
|
+
handle: (request: Request, context: W) => Promise<Response | null>;
|
|
87
|
+
};
|
|
88
|
+
type BagsyServer<R extends ReadContext = ReadContext, W extends WriteContext = WriteContext> = ReturnType<typeof createBagsyServer<R, W>>;
|
|
89
|
+
/** Wraps a session manager to report whether anything was written. */
|
|
90
|
+
declare function trackWrites<SM extends BagsySessionManager>(sessionManager: SM): {
|
|
91
|
+
sessionManager: SM;
|
|
92
|
+
dirty: () => boolean;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export { createBagsyServer, trackWrites };
|
|
96
|
+
export type { BagsyRequestContext, BagsyServer, BagsySessionManager, CreateBagsyServerOptions, CustomerResolver, ReadContext, ReadonlySessionManager, Shopper, WritableShopper, WriteContext };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|