@shoppexio/storefront 1.0.87 → 1.0.89

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.89
4
+
5
+ - `getShopReviewsPage(cursor, { product })` fails with `code: "reviews_product_filter_unsupported"` (exported as `PRODUCT_FILTER_UNSUPPORTED`) when the API answers without echoing the filter it applied, instead of returning the whole shop's reviews as if they were the product's. `ShopReviewsPage.product` carries the echoed id.
6
+
7
+ ## 1.0.88
8
+
9
+ - 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.
10
+ - 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.
11
+ - 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.
12
+ - Page views and heartbeats without a cart are unchanged and carry no sequence.
13
+
3
14
  ## 1.0.87
4
15
 
5
16
  - `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
@@ -29,6 +29,7 @@ __export(src_exports, {
29
29
  NetworkError: () => NetworkError,
30
30
  NotInitializedError: () => NotInitializedError,
31
31
  PAYABLE_AMOUNT_DECIMAL_PLACES: () => PAYABLE_AMOUNT_DECIMAL_PLACES,
32
+ PRODUCT_FILTER_UNSUPPORTED: () => PRODUCT_FILTER_UNSUPPORTED,
32
33
  ShoppexError: () => ShoppexError,
33
34
  ValidationError: () => ValidationError,
34
35
  acceptResellerInvite: () => acceptResellerInvite,
@@ -25924,6 +25925,7 @@ function normalizeFeedback(raw, index) {
25924
25925
  created_at
25925
25926
  };
25926
25927
  }
25928
+ var PRODUCT_FILTER_UNSUPPORTED = "reviews_product_filter_unsupported";
25927
25929
  async function getShopReviewsPage(cursor, options = {}) {
25928
25930
  const config2 = getConfig();
25929
25931
  const query = new URLSearchParams();
@@ -25949,6 +25951,15 @@ async function getShopReviewsPage(cursor, options = {}) {
25949
25951
  }
25950
25952
  );
25951
25953
  if (response.success && response.data) {
25954
+ if (product && response.data.product !== product) {
25955
+ return {
25956
+ success: false,
25957
+ message: "Reviews for a single product are not available from this store yet.",
25958
+ code: PRODUCT_FILTER_UNSUPPORTED,
25959
+ status: 200,
25960
+ responseReceived: true
25961
+ };
25962
+ }
25952
25963
  return {
25953
25964
  success: true,
25954
25965
  data: {
@@ -26634,6 +26645,7 @@ function isPlatformPageTrackerActive() {
26634
26645
  // ../sdk/src/modules/analytics.ts
26635
26646
  var CONFIRMED_CART_STORAGE_PREFIX = "reported_cart_";
26636
26647
  var IN_FLIGHT_CART_STORAGE_PREFIX = "reporting_cart_";
26648
+ var CART_SEQUENCE_STORAGE_PREFIX = "presence_cart_seq_";
26637
26649
  var CART_CLAIM_TTL_MS = 1e4;
26638
26650
  function confirmedCartKey(storeSlug) {
26639
26651
  return `${CONFIRMED_CART_STORAGE_PREFIX}${storeSlug}`;
@@ -26641,33 +26653,77 @@ function confirmedCartKey(storeSlug) {
26641
26653
  function inFlightCartKey(storeSlug) {
26642
26654
  return `${IN_FLIGHT_CART_STORAGE_PREFIX}${storeSlug}`;
26643
26655
  }
26656
+ function cartSequenceKey(storeSlug) {
26657
+ return `${CART_SEQUENCE_STORAGE_PREFIX}${storeSlug}`;
26658
+ }
26644
26659
  function cartSnapshot(cartValue, itemCount) {
26645
26660
  return `${cartValue ?? ""}|${itemCount ?? ""}`;
26646
26661
  }
26662
+ function isCartSequence(value) {
26663
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
26664
+ }
26665
+ var CART_SEQUENCE_SCOPE_MAX_LENGTH = 64;
26666
+ function readCartCounter(storeSlug) {
26667
+ const counter = getItem(cartSequenceKey(storeSlug));
26668
+ if (!counter || typeof counter.scope !== "string" || counter.scope.length === 0 || counter.scope.length > CART_SEQUENCE_SCOPE_MAX_LENGTH || !isCartSequence(counter.seq)) {
26669
+ return null;
26670
+ }
26671
+ return counter;
26672
+ }
26673
+ var lastCartOrder = null;
26674
+ function nextCartOrder(storeSlug) {
26675
+ const stored = readCartCounter(storeSlug);
26676
+ const scope = stored?.scope ?? lastCartOrder?.scope ?? createClaimToken();
26677
+ const floor = Math.max(stored?.seq ?? 0, lastCartOrder?.scope === scope ? lastCartOrder.seq : 0);
26678
+ const order2 = { scope, seq: Math.max(Date.now(), floor + 1) };
26679
+ lastCartOrder = order2;
26680
+ setItem(cartSequenceKey(storeSlug), order2);
26681
+ return order2;
26682
+ }
26683
+ function cartSequenceLocks() {
26684
+ if (typeof navigator === "undefined") return null;
26685
+ const locks = navigator.locks;
26686
+ return locks && typeof locks.request === "function" ? locks : null;
26687
+ }
26688
+ function reserveCartOrder(storeSlug) {
26689
+ const take = () => nextCartOrder(storeSlug);
26690
+ const locks = cartSequenceLocks();
26691
+ if (!locks) return Promise.resolve(take());
26692
+ return locks.request(cartSequenceKey(storeSlug), take).catch(take);
26693
+ }
26694
+ function readConfirmedCart(storeSlug) {
26695
+ const confirmed = getItem(confirmedCartKey(storeSlug));
26696
+ if (!confirmed || typeof confirmed.snapshot !== "string" || !isCartSequence(confirmed.seq)) return null;
26697
+ return confirmed;
26698
+ }
26647
26699
  function readInFlightCart(storeSlug) {
26648
26700
  const claim2 = getItem(inFlightCartKey(storeSlug));
26649
- if (!claim2 || typeof claim2.snapshot !== "string" || typeof claim2.token !== "string" || typeof claim2.at !== "number") {
26701
+ if (!claim2 || typeof claim2.snapshot !== "string" || !isCartSequence(claim2.seq) || typeof claim2.token !== "string" || typeof claim2.at !== "number") {
26650
26702
  return null;
26651
26703
  }
26652
26704
  return claim2;
26653
26705
  }
26654
26706
  function isCartReportedOrInFlight(storeSlug, snapshot) {
26655
- if (getItem(confirmedCartKey(storeSlug)) === snapshot) return true;
26707
+ const confirmed = readConfirmedCart(storeSlug);
26656
26708
  const claim2 = readInFlightCart(storeSlug);
26657
- return claim2 !== null && claim2.snapshot === snapshot && Date.now() - claim2.at < CART_CLAIM_TTL_MS;
26709
+ const liveClaim = claim2 && Date.now() - claim2.at < CART_CLAIM_TTL_MS ? claim2 : null;
26710
+ const expected = liveClaim && (!confirmed || liveClaim.seq > confirmed.seq) ? liveClaim : confirmed;
26711
+ return expected?.snapshot === snapshot;
26658
26712
  }
26659
26713
  function createClaimToken() {
26660
26714
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
26661
26715
  return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
26662
26716
  }
26663
- function claimCartReport(storeSlug, cartValue, itemCount) {
26717
+ function claimCartReport(storeSlug, seq, cartValue, itemCount) {
26664
26718
  const snapshot = cartSnapshot(cartValue, itemCount);
26665
26719
  const token = createClaimToken();
26666
- setItem(inFlightCartKey(storeSlug), { snapshot, token, at: Date.now() });
26720
+ setItem(inFlightCartKey(storeSlug), { snapshot, seq, token, at: Date.now() });
26667
26721
  return (delivered) => {
26668
- if (readInFlightCart(storeSlug)?.token !== token) return;
26669
- if (delivered) setItem(confirmedCartKey(storeSlug), snapshot);
26670
- removeItem(inFlightCartKey(storeSlug));
26722
+ if (delivered) {
26723
+ const confirmed = readConfirmedCart(storeSlug);
26724
+ if (!confirmed || seq > confirmed.seq) setItem(confirmedCartKey(storeSlug), { snapshot, seq });
26725
+ }
26726
+ if (readInFlightCart(storeSlug)?.token === token) removeItem(inFlightCartKey(storeSlug));
26671
26727
  };
26672
26728
  }
26673
26729
  var cartSendTail = Promise.resolve();
@@ -26708,42 +26764,50 @@ async function postPing(body) {
26708
26764
  async function trackPageView(cartValue, itemCount) {
26709
26765
  if (!isInitialized()) return;
26710
26766
  if (typeof document === "undefined") return;
26767
+ const config2 = getConfig();
26711
26768
  const carriesCart = cartValue !== void 0 || itemCount !== void 0;
26769
+ const reservedOrder = carriesCart ? reserveCartOrder(config2.storeSlug) : void 0;
26712
26770
  if (isPlatformPageTrackerActive()) {
26713
- if (carriesCart) await enqueueCartSend(() => sendCartActivity(cartValue, itemCount));
26771
+ if (reservedOrder) await enqueueCartSend(async () => sendCartActivity(await reservedOrder, cartValue, itemCount));
26714
26772
  return;
26715
26773
  }
26716
- const config2 = getConfig();
26717
26774
  const sendView = async () => {
26718
- const settle2 = carriesCart ? claimCartReport(config2.storeSlug, cartValue, itemCount) : void 0;
26775
+ const order2 = reservedOrder ? await reservedOrder : void 0;
26776
+ const settle2 = order2 ? claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount) : void 0;
26719
26777
  const delivered = await postPing({
26720
26778
  referer: document.referrer || void 0,
26721
26779
  cart_value: cartValue,
26722
26780
  item_count: itemCount,
26781
+ cart_seq: order2?.seq,
26782
+ cart_seq_scope: order2?.scope,
26723
26783
  connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
26724
26784
  path: resolveStorefrontPagePath(),
26725
26785
  utm: resolveUtmParameters(config2.storeSlug)
26726
26786
  });
26727
26787
  settle2?.(delivered);
26728
26788
  };
26729
- await (carriesCart ? enqueueCartSend(sendView) : sendView());
26789
+ await (reservedOrder ? enqueueCartSend(sendView) : sendView());
26730
26790
  }
26731
26791
  async function trackCartActivity(cartValue, itemCount) {
26732
26792
  if (!isInitialized()) return;
26733
26793
  if (typeof document === "undefined") return;
26734
26794
  const snapshot = cartSnapshot(cartValue, itemCount);
26795
+ const reservedOrder = reserveCartOrder(getConfig().storeSlug);
26735
26796
  await enqueueCartSend(async (isLatest) => {
26797
+ const order2 = await reservedOrder;
26736
26798
  if (!isLatest() || isCartReportedOrInFlight(getConfig().storeSlug, snapshot)) return;
26737
- await sendCartActivity(cartValue, itemCount);
26799
+ await sendCartActivity(order2, cartValue, itemCount);
26738
26800
  });
26739
26801
  }
26740
- async function sendCartActivity(cartValue, itemCount) {
26802
+ async function sendCartActivity(order2, cartValue, itemCount) {
26741
26803
  const config2 = getConfig();
26742
- const settle2 = claimCartReport(config2.storeSlug, cartValue, itemCount);
26804
+ const settle2 = claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount);
26743
26805
  const delivered = await postPing({
26744
26806
  event_type: "presence",
26745
26807
  cart_value: cartValue,
26746
26808
  item_count: itemCount,
26809
+ cart_seq: order2.seq,
26810
+ cart_seq_scope: order2.scope,
26747
26811
  connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
26748
26812
  path: resolveStorefrontPagePath()
26749
26813
  });
@@ -27054,6 +27118,7 @@ if (typeof window !== "undefined") {
27054
27118
  NetworkError,
27055
27119
  NotInitializedError,
27056
27120
  PAYABLE_AMOUNT_DECIMAL_PLACES,
27121
+ PRODUCT_FILTER_UNSUPPORTED,
27057
27122
  ShoppexError,
27058
27123
  ValidationError,
27059
27124
  acceptResellerInvite,