@shoppexio/storefront 1.0.87 → 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,12 @@
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
+
3
10
  ## 1.0.87
4
11
 
5
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.
package/dist/index.cjs CHANGED
@@ -26634,6 +26634,7 @@ function isPlatformPageTrackerActive() {
26634
26634
  // ../sdk/src/modules/analytics.ts
26635
26635
  var CONFIRMED_CART_STORAGE_PREFIX = "reported_cart_";
26636
26636
  var IN_FLIGHT_CART_STORAGE_PREFIX = "reporting_cart_";
26637
+ var CART_SEQUENCE_STORAGE_PREFIX = "presence_cart_seq_";
26637
26638
  var CART_CLAIM_TTL_MS = 1e4;
26638
26639
  function confirmedCartKey(storeSlug) {
26639
26640
  return `${CONFIRMED_CART_STORAGE_PREFIX}${storeSlug}`;
@@ -26641,33 +26642,77 @@ function confirmedCartKey(storeSlug) {
26641
26642
  function inFlightCartKey(storeSlug) {
26642
26643
  return `${IN_FLIGHT_CART_STORAGE_PREFIX}${storeSlug}`;
26643
26644
  }
26645
+ function cartSequenceKey(storeSlug) {
26646
+ return `${CART_SEQUENCE_STORAGE_PREFIX}${storeSlug}`;
26647
+ }
26644
26648
  function cartSnapshot(cartValue, itemCount) {
26645
26649
  return `${cartValue ?? ""}|${itemCount ?? ""}`;
26646
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
+ }
26647
26688
  function readInFlightCart(storeSlug) {
26648
26689
  const claim2 = getItem(inFlightCartKey(storeSlug));
26649
- 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") {
26650
26691
  return null;
26651
26692
  }
26652
26693
  return claim2;
26653
26694
  }
26654
26695
  function isCartReportedOrInFlight(storeSlug, snapshot) {
26655
- if (getItem(confirmedCartKey(storeSlug)) === snapshot) return true;
26696
+ const confirmed = readConfirmedCart(storeSlug);
26656
26697
  const claim2 = readInFlightCart(storeSlug);
26657
- 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;
26658
26701
  }
26659
26702
  function createClaimToken() {
26660
26703
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
26661
26704
  return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
26662
26705
  }
26663
- function claimCartReport(storeSlug, cartValue, itemCount) {
26706
+ function claimCartReport(storeSlug, seq, cartValue, itemCount) {
26664
26707
  const snapshot = cartSnapshot(cartValue, itemCount);
26665
26708
  const token = createClaimToken();
26666
- setItem(inFlightCartKey(storeSlug), { snapshot, token, at: Date.now() });
26709
+ setItem(inFlightCartKey(storeSlug), { snapshot, seq, token, at: Date.now() });
26667
26710
  return (delivered) => {
26668
- if (readInFlightCart(storeSlug)?.token !== token) return;
26669
- if (delivered) setItem(confirmedCartKey(storeSlug), snapshot);
26670
- 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));
26671
26716
  };
26672
26717
  }
26673
26718
  var cartSendTail = Promise.resolve();
@@ -26708,42 +26753,50 @@ async function postPing(body) {
26708
26753
  async function trackPageView(cartValue, itemCount) {
26709
26754
  if (!isInitialized()) return;
26710
26755
  if (typeof document === "undefined") return;
26756
+ const config2 = getConfig();
26711
26757
  const carriesCart = cartValue !== void 0 || itemCount !== void 0;
26758
+ const reservedOrder = carriesCart ? reserveCartOrder(config2.storeSlug) : void 0;
26712
26759
  if (isPlatformPageTrackerActive()) {
26713
- if (carriesCart) await enqueueCartSend(() => sendCartActivity(cartValue, itemCount));
26760
+ if (reservedOrder) await enqueueCartSend(async () => sendCartActivity(await reservedOrder, cartValue, itemCount));
26714
26761
  return;
26715
26762
  }
26716
- const config2 = getConfig();
26717
26763
  const sendView = async () => {
26718
- 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;
26719
26766
  const delivered = await postPing({
26720
26767
  referer: document.referrer || void 0,
26721
26768
  cart_value: cartValue,
26722
26769
  item_count: itemCount,
26770
+ cart_seq: order2?.seq,
26771
+ cart_seq_scope: order2?.scope,
26723
26772
  connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
26724
26773
  path: resolveStorefrontPagePath(),
26725
26774
  utm: resolveUtmParameters(config2.storeSlug)
26726
26775
  });
26727
26776
  settle2?.(delivered);
26728
26777
  };
26729
- await (carriesCart ? enqueueCartSend(sendView) : sendView());
26778
+ await (reservedOrder ? enqueueCartSend(sendView) : sendView());
26730
26779
  }
26731
26780
  async function trackCartActivity(cartValue, itemCount) {
26732
26781
  if (!isInitialized()) return;
26733
26782
  if (typeof document === "undefined") return;
26734
26783
  const snapshot = cartSnapshot(cartValue, itemCount);
26784
+ const reservedOrder = reserveCartOrder(getConfig().storeSlug);
26735
26785
  await enqueueCartSend(async (isLatest) => {
26786
+ const order2 = await reservedOrder;
26736
26787
  if (!isLatest() || isCartReportedOrInFlight(getConfig().storeSlug, snapshot)) return;
26737
- await sendCartActivity(cartValue, itemCount);
26788
+ await sendCartActivity(order2, cartValue, itemCount);
26738
26789
  });
26739
26790
  }
26740
- async function sendCartActivity(cartValue, itemCount) {
26791
+ async function sendCartActivity(order2, cartValue, itemCount) {
26741
26792
  const config2 = getConfig();
26742
- const settle2 = claimCartReport(config2.storeSlug, cartValue, itemCount);
26793
+ const settle2 = claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount);
26743
26794
  const delivered = await postPing({
26744
26795
  event_type: "presence",
26745
26796
  cart_value: cartValue,
26746
26797
  item_count: itemCount,
26798
+ cart_seq: order2.seq,
26799
+ cart_seq_scope: order2.scope,
26747
26800
  connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
26748
26801
  path: resolveStorefrontPagePath()
26749
26802
  });