@shoppexio/storefront 1.0.85 → 1.0.88
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/CHANGELOG.md +12 -0
- package/README.md +2 -0
- package/dist/index.cjs +77 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -8
- package/dist/index.d.ts +15 -8
- package/dist/index.js +77 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.88
|
|
4
|
+
|
|
5
|
+
- Cart pings carry `cart_seq` and `cart_seq_scope`, a sequence number taken when the cart state is reported and shared by all tabs of the shop (the current time in ms, or one above the highest sequence any tab handed out). The counter record holds the scope (created on first use) and is updated under a Web Lock (`navigator.locks`), so two tabs never hand out the same sequence or create two scopes; where Web Locks are unavailable it is updated without the lock. The server keeps the cart with the highest sequence of the scope and ignores an older one that arrives later, so a slow ping from one tab can no longer overwrite a newer cart another tab already reported.
|
|
6
|
+
- The confirmed cart state records its sequence and is only replaced by a delivered state with a higher one, the same rule the server applies. A cart state counts as already reported when it is the state the server will hold: a newer state another tab is sending, else the confirmed state. So reverting to the confirmed cart while another tab sends a newer one still goes out.
|
|
7
|
+
- Each tab still sends its cart states one at a time and drops a waiting state once a newer one is asked for; this now only saves pings, the order comes from the sequence.
|
|
8
|
+
- Page views and heartbeats without a cart are unchanged and carry no sequence.
|
|
9
|
+
|
|
10
|
+
## 1.0.87
|
|
11
|
+
|
|
12
|
+
- `getPage()`, `getPages()` and `getShopReviewsPage()` keep the transport `status` (and `code`) on a failure, like the other modules, so a storefront can tell a page that does not exist (404) from an API it could not reach.
|
|
13
|
+
- `getShopReviewsPage(cursor, { product })` narrows a page to one product by its public id (the catalog `id`). The page's `summary` then counts that product's reviews only, so a product's review list can page on demand with a correct breakdown.
|
|
14
|
+
|
|
3
15
|
## 1.0.85
|
|
4
16
|
|
|
5
17
|
- Cart reporting keeps two shared values: the cart state the server confirmed (written only after a 2xx response) and a short-lived in-flight claim that lets another tab skip the `storage` echo while a send runs. Only the newest claim settles: a send whose claim was replaced confirms and clears nothing, and a failed send (network error, non-OK response, or no answer within 10 seconds) writes nothing, so an unsent cart can never look reported. A tab sends its cart states one at a time and in order, dropping a waiting state once a newer one is asked for, so an older cart cannot reach the server after a newer one. Whether a change is already reported is decided when its turn comes, so reverting to the confirmed cart while another state is being sent still goes out.
|
package/README.md
CHANGED
|
@@ -151,6 +151,8 @@ await refreshLiveUsers();
|
|
|
151
151
|
setInterval(refreshLiveUsers, 30_000);
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
+
`getShopReviewsPage(cursor, { product })` pages the shop's reviews on demand; pass a product's `id` to page only its reviews, with a `summary` for that product.
|
|
155
|
+
|
|
154
156
|
`touchStorefrontPresence()` keeps the visitor online but never counts as a page view, so it is safe on a timer. Report page views with `trackPageView()` (once per page load or navigation) and later cart changes with `trackCartActivity(cartValue, itemCount)`, which updates the live visitor map without counting a view.
|
|
155
157
|
|
|
156
158
|
## Not this package
|
package/dist/index.cjs
CHANGED
|
@@ -25924,13 +25924,17 @@ function normalizeFeedback(raw, index) {
|
|
|
25924
25924
|
created_at
|
|
25925
25925
|
};
|
|
25926
25926
|
}
|
|
25927
|
-
async function getShopReviewsPage(cursor) {
|
|
25927
|
+
async function getShopReviewsPage(cursor, options = {}) {
|
|
25928
25928
|
const config2 = getConfig();
|
|
25929
25929
|
const query = new URLSearchParams();
|
|
25930
25930
|
query.set("limit", String(REVIEWS_PAGE_LIMIT));
|
|
25931
25931
|
if (typeof cursor === "string" && cursor.trim().length > 0) {
|
|
25932
25932
|
query.set("cursor", cursor);
|
|
25933
25933
|
}
|
|
25934
|
+
const product = typeof options.product === "string" ? options.product.trim() : "";
|
|
25935
|
+
if (product) {
|
|
25936
|
+
query.set("product", product);
|
|
25937
|
+
}
|
|
25934
25938
|
const querySuffix = `?${query.toString()}`;
|
|
25935
25939
|
const response = await get(
|
|
25936
25940
|
`${buildEndpoint("/v1/storefront/feedback/shop/:storeSlug", {
|
|
@@ -25938,7 +25942,7 @@ async function getShopReviewsPage(cursor) {
|
|
|
25938
25942
|
})}${querySuffix}`,
|
|
25939
25943
|
{
|
|
25940
25944
|
cache: {
|
|
25941
|
-
key: `reviews:${config2.storeSlug}:${cursor ?? "start"}:${REVIEWS_PAGE_LIMIT}`,
|
|
25945
|
+
key: `reviews:${config2.storeSlug}:${product || "all"}:${cursor ?? "start"}:${REVIEWS_PAGE_LIMIT}`,
|
|
25942
25946
|
ttl: REVIEWS_CACHE_TTL,
|
|
25943
25947
|
staleWhileRevalidate: true
|
|
25944
25948
|
}
|
|
@@ -25953,10 +25957,7 @@ async function getShopReviewsPage(cursor) {
|
|
|
25953
25957
|
}
|
|
25954
25958
|
};
|
|
25955
25959
|
}
|
|
25956
|
-
return
|
|
25957
|
-
success: false,
|
|
25958
|
-
message: response.message
|
|
25959
|
-
};
|
|
25960
|
+
return forwardFailure(response);
|
|
25960
25961
|
}
|
|
25961
25962
|
async function getShopReviews() {
|
|
25962
25963
|
const allFeedback = [];
|
|
@@ -26426,10 +26427,7 @@ async function getPages() {
|
|
|
26426
26427
|
data: response.data.pages
|
|
26427
26428
|
};
|
|
26428
26429
|
}
|
|
26429
|
-
return
|
|
26430
|
-
success: false,
|
|
26431
|
-
message: response.message
|
|
26432
|
-
};
|
|
26430
|
+
return forwardFailure(response);
|
|
26433
26431
|
}
|
|
26434
26432
|
async function getPage(slug) {
|
|
26435
26433
|
const config2 = getConfig();
|
|
@@ -26452,10 +26450,7 @@ async function getPage(slug) {
|
|
|
26452
26450
|
data: response.data.page
|
|
26453
26451
|
};
|
|
26454
26452
|
}
|
|
26455
|
-
return
|
|
26456
|
-
success: false,
|
|
26457
|
-
message: response.message
|
|
26458
|
-
};
|
|
26453
|
+
return forwardFailure(response);
|
|
26459
26454
|
}
|
|
26460
26455
|
|
|
26461
26456
|
// ../sdk/src/modules/navigation.ts
|
|
@@ -26639,6 +26634,7 @@ function isPlatformPageTrackerActive() {
|
|
|
26639
26634
|
// ../sdk/src/modules/analytics.ts
|
|
26640
26635
|
var CONFIRMED_CART_STORAGE_PREFIX = "reported_cart_";
|
|
26641
26636
|
var IN_FLIGHT_CART_STORAGE_PREFIX = "reporting_cart_";
|
|
26637
|
+
var CART_SEQUENCE_STORAGE_PREFIX = "presence_cart_seq_";
|
|
26642
26638
|
var CART_CLAIM_TTL_MS = 1e4;
|
|
26643
26639
|
function confirmedCartKey(storeSlug) {
|
|
26644
26640
|
return `${CONFIRMED_CART_STORAGE_PREFIX}${storeSlug}`;
|
|
@@ -26646,33 +26642,77 @@ function confirmedCartKey(storeSlug) {
|
|
|
26646
26642
|
function inFlightCartKey(storeSlug) {
|
|
26647
26643
|
return `${IN_FLIGHT_CART_STORAGE_PREFIX}${storeSlug}`;
|
|
26648
26644
|
}
|
|
26645
|
+
function cartSequenceKey(storeSlug) {
|
|
26646
|
+
return `${CART_SEQUENCE_STORAGE_PREFIX}${storeSlug}`;
|
|
26647
|
+
}
|
|
26649
26648
|
function cartSnapshot(cartValue, itemCount) {
|
|
26650
26649
|
return `${cartValue ?? ""}|${itemCount ?? ""}`;
|
|
26651
26650
|
}
|
|
26651
|
+
function isCartSequence(value) {
|
|
26652
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
26653
|
+
}
|
|
26654
|
+
var CART_SEQUENCE_SCOPE_MAX_LENGTH = 64;
|
|
26655
|
+
function readCartCounter(storeSlug) {
|
|
26656
|
+
const counter = getItem(cartSequenceKey(storeSlug));
|
|
26657
|
+
if (!counter || typeof counter.scope !== "string" || counter.scope.length === 0 || counter.scope.length > CART_SEQUENCE_SCOPE_MAX_LENGTH || !isCartSequence(counter.seq)) {
|
|
26658
|
+
return null;
|
|
26659
|
+
}
|
|
26660
|
+
return counter;
|
|
26661
|
+
}
|
|
26662
|
+
var lastCartOrder = null;
|
|
26663
|
+
function nextCartOrder(storeSlug) {
|
|
26664
|
+
const stored = readCartCounter(storeSlug);
|
|
26665
|
+
const scope = stored?.scope ?? lastCartOrder?.scope ?? createClaimToken();
|
|
26666
|
+
const floor = Math.max(stored?.seq ?? 0, lastCartOrder?.scope === scope ? lastCartOrder.seq : 0);
|
|
26667
|
+
const order2 = { scope, seq: Math.max(Date.now(), floor + 1) };
|
|
26668
|
+
lastCartOrder = order2;
|
|
26669
|
+
setItem(cartSequenceKey(storeSlug), order2);
|
|
26670
|
+
return order2;
|
|
26671
|
+
}
|
|
26672
|
+
function cartSequenceLocks() {
|
|
26673
|
+
if (typeof navigator === "undefined") return null;
|
|
26674
|
+
const locks = navigator.locks;
|
|
26675
|
+
return locks && typeof locks.request === "function" ? locks : null;
|
|
26676
|
+
}
|
|
26677
|
+
function reserveCartOrder(storeSlug) {
|
|
26678
|
+
const take = () => nextCartOrder(storeSlug);
|
|
26679
|
+
const locks = cartSequenceLocks();
|
|
26680
|
+
if (!locks) return Promise.resolve(take());
|
|
26681
|
+
return locks.request(cartSequenceKey(storeSlug), take).catch(take);
|
|
26682
|
+
}
|
|
26683
|
+
function readConfirmedCart(storeSlug) {
|
|
26684
|
+
const confirmed = getItem(confirmedCartKey(storeSlug));
|
|
26685
|
+
if (!confirmed || typeof confirmed.snapshot !== "string" || !isCartSequence(confirmed.seq)) return null;
|
|
26686
|
+
return confirmed;
|
|
26687
|
+
}
|
|
26652
26688
|
function readInFlightCart(storeSlug) {
|
|
26653
26689
|
const claim2 = getItem(inFlightCartKey(storeSlug));
|
|
26654
|
-
if (!claim2 || typeof claim2.snapshot !== "string" || typeof claim2.token !== "string" || typeof claim2.at !== "number") {
|
|
26690
|
+
if (!claim2 || typeof claim2.snapshot !== "string" || !isCartSequence(claim2.seq) || typeof claim2.token !== "string" || typeof claim2.at !== "number") {
|
|
26655
26691
|
return null;
|
|
26656
26692
|
}
|
|
26657
26693
|
return claim2;
|
|
26658
26694
|
}
|
|
26659
26695
|
function isCartReportedOrInFlight(storeSlug, snapshot) {
|
|
26660
|
-
|
|
26696
|
+
const confirmed = readConfirmedCart(storeSlug);
|
|
26661
26697
|
const claim2 = readInFlightCart(storeSlug);
|
|
26662
|
-
|
|
26698
|
+
const liveClaim = claim2 && Date.now() - claim2.at < CART_CLAIM_TTL_MS ? claim2 : null;
|
|
26699
|
+
const expected = liveClaim && (!confirmed || liveClaim.seq > confirmed.seq) ? liveClaim : confirmed;
|
|
26700
|
+
return expected?.snapshot === snapshot;
|
|
26663
26701
|
}
|
|
26664
26702
|
function createClaimToken() {
|
|
26665
26703
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
26666
26704
|
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
|
|
26667
26705
|
}
|
|
26668
|
-
function claimCartReport(storeSlug, cartValue, itemCount) {
|
|
26706
|
+
function claimCartReport(storeSlug, seq, cartValue, itemCount) {
|
|
26669
26707
|
const snapshot = cartSnapshot(cartValue, itemCount);
|
|
26670
26708
|
const token = createClaimToken();
|
|
26671
|
-
setItem(inFlightCartKey(storeSlug), { snapshot, token, at: Date.now() });
|
|
26709
|
+
setItem(inFlightCartKey(storeSlug), { snapshot, seq, token, at: Date.now() });
|
|
26672
26710
|
return (delivered) => {
|
|
26673
|
-
if (
|
|
26674
|
-
|
|
26675
|
-
|
|
26711
|
+
if (delivered) {
|
|
26712
|
+
const confirmed = readConfirmedCart(storeSlug);
|
|
26713
|
+
if (!confirmed || seq > confirmed.seq) setItem(confirmedCartKey(storeSlug), { snapshot, seq });
|
|
26714
|
+
}
|
|
26715
|
+
if (readInFlightCart(storeSlug)?.token === token) removeItem(inFlightCartKey(storeSlug));
|
|
26676
26716
|
};
|
|
26677
26717
|
}
|
|
26678
26718
|
var cartSendTail = Promise.resolve();
|
|
@@ -26713,42 +26753,50 @@ async function postPing(body) {
|
|
|
26713
26753
|
async function trackPageView(cartValue, itemCount) {
|
|
26714
26754
|
if (!isInitialized()) return;
|
|
26715
26755
|
if (typeof document === "undefined") return;
|
|
26756
|
+
const config2 = getConfig();
|
|
26716
26757
|
const carriesCart = cartValue !== void 0 || itemCount !== void 0;
|
|
26758
|
+
const reservedOrder = carriesCart ? reserveCartOrder(config2.storeSlug) : void 0;
|
|
26717
26759
|
if (isPlatformPageTrackerActive()) {
|
|
26718
|
-
if (
|
|
26760
|
+
if (reservedOrder) await enqueueCartSend(async () => sendCartActivity(await reservedOrder, cartValue, itemCount));
|
|
26719
26761
|
return;
|
|
26720
26762
|
}
|
|
26721
|
-
const config2 = getConfig();
|
|
26722
26763
|
const sendView = async () => {
|
|
26723
|
-
const
|
|
26764
|
+
const order2 = reservedOrder ? await reservedOrder : void 0;
|
|
26765
|
+
const settle2 = order2 ? claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount) : void 0;
|
|
26724
26766
|
const delivered = await postPing({
|
|
26725
26767
|
referer: document.referrer || void 0,
|
|
26726
26768
|
cart_value: cartValue,
|
|
26727
26769
|
item_count: itemCount,
|
|
26770
|
+
cart_seq: order2?.seq,
|
|
26771
|
+
cart_seq_scope: order2?.scope,
|
|
26728
26772
|
connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
|
|
26729
26773
|
path: resolveStorefrontPagePath(),
|
|
26730
26774
|
utm: resolveUtmParameters(config2.storeSlug)
|
|
26731
26775
|
});
|
|
26732
26776
|
settle2?.(delivered);
|
|
26733
26777
|
};
|
|
26734
|
-
await (
|
|
26778
|
+
await (reservedOrder ? enqueueCartSend(sendView) : sendView());
|
|
26735
26779
|
}
|
|
26736
26780
|
async function trackCartActivity(cartValue, itemCount) {
|
|
26737
26781
|
if (!isInitialized()) return;
|
|
26738
26782
|
if (typeof document === "undefined") return;
|
|
26739
26783
|
const snapshot = cartSnapshot(cartValue, itemCount);
|
|
26784
|
+
const reservedOrder = reserveCartOrder(getConfig().storeSlug);
|
|
26740
26785
|
await enqueueCartSend(async (isLatest) => {
|
|
26786
|
+
const order2 = await reservedOrder;
|
|
26741
26787
|
if (!isLatest() || isCartReportedOrInFlight(getConfig().storeSlug, snapshot)) return;
|
|
26742
|
-
await sendCartActivity(cartValue, itemCount);
|
|
26788
|
+
await sendCartActivity(order2, cartValue, itemCount);
|
|
26743
26789
|
});
|
|
26744
26790
|
}
|
|
26745
|
-
async function sendCartActivity(cartValue, itemCount) {
|
|
26791
|
+
async function sendCartActivity(order2, cartValue, itemCount) {
|
|
26746
26792
|
const config2 = getConfig();
|
|
26747
|
-
const settle2 = claimCartReport(config2.storeSlug, cartValue, itemCount);
|
|
26793
|
+
const settle2 = claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount);
|
|
26748
26794
|
const delivered = await postPing({
|
|
26749
26795
|
event_type: "presence",
|
|
26750
26796
|
cart_value: cartValue,
|
|
26751
26797
|
item_count: itemCount,
|
|
26798
|
+
cart_seq: order2.seq,
|
|
26799
|
+
cart_seq_scope: order2.scope,
|
|
26752
26800
|
connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
|
|
26753
26801
|
path: resolveStorefrontPagePath()
|
|
26754
26802
|
});
|