@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 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
- if (getItem(confirmedCartKey(storeSlug)) === snapshot) return true;
26696
+ const confirmed = readConfirmedCart(storeSlug);
26661
26697
  const claim2 = readInFlightCart(storeSlug);
26662
- return claim2 !== null && claim2.snapshot === snapshot && Date.now() - claim2.at < CART_CLAIM_TTL_MS;
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 (readInFlightCart(storeSlug)?.token !== token) return;
26674
- if (delivered) setItem(confirmedCartKey(storeSlug), snapshot);
26675
- removeItem(inFlightCartKey(storeSlug));
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 (carriesCart) await enqueueCartSend(() => sendCartActivity(cartValue, itemCount));
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 settle2 = carriesCart ? claimCartReport(config2.storeSlug, cartValue, itemCount) : void 0;
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 (carriesCart ? enqueueCartSend(sendView) : sendView());
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
  });