saltfish 0.3.55 → 0.3.57

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.
@@ -2754,6 +2754,7 @@ class PlaylistOrchestrator {
2754
2754
  }
2755
2755
  const runId = this.managers.sessionManager.startNewRun();
2756
2756
  log(`PlaylistOrchestrator: Starting playlist ${playlistId} with runId: ${runId}`);
2757
+ this.managers.triggerManager.markPlaylistAsTriggered(playlistId);
2757
2758
  const userDataLoadedPromise = (_c = this.userManagementService) == null ? void 0 : _c.getUserDataLoadedPromise();
2758
2759
  if (userDataLoadedPromise) {
2759
2760
  log("[PlaylistOrchestrator.startPlaylist] Waiting for user data to load before checking A/B test assignments");
@@ -6685,6 +6686,121 @@ const THRESHOLDS = {
6685
6686
  /** Minimum scroll distance in pixels to trigger scroll events */
6686
6687
  SCROLL_THRESHOLD_PX: 10
6687
6688
  };
6689
+ const DEFAULT_CONFIG = {
6690
+ tolerance: 0.3,
6691
+ // 30% size difference allowed (70% match required)
6692
+ minSize: 1
6693
+ // Minimum 1x1px (reject only zero-size elements)
6694
+ };
6695
+ function getBasicRejectionReason(element, config) {
6696
+ const rect = element.getBoundingClientRect();
6697
+ if (rect.width === 0 || rect.height === 0) {
6698
+ return "zero size";
6699
+ }
6700
+ if (rect.width < config.minSize || rect.height < config.minSize) {
6701
+ return `too small (${rect.width.toFixed(0)}x${rect.height.toFixed(0)})`;
6702
+ }
6703
+ return null;
6704
+ }
6705
+ function calculateSizeScore(element, expected) {
6706
+ const rect = element.getBoundingClientRect();
6707
+ const widthRatio = Math.min(rect.width, expected.width) / Math.max(rect.width, expected.width);
6708
+ const heightRatio = Math.min(rect.height, expected.height) / Math.max(rect.height, expected.height);
6709
+ return (widthRatio + heightRatio) / 2 * 100;
6710
+ }
6711
+ function findValidElement(selector, expectedSize, config = DEFAULT_CONFIG) {
6712
+ const elements = document.querySelectorAll(selector);
6713
+ if (elements.length === 0) {
6714
+ return null;
6715
+ }
6716
+ if (!expectedSize) {
6717
+ for (const element of elements) {
6718
+ const rejection = getBasicRejectionReason(element, config);
6719
+ if (!rejection) {
6720
+ return element;
6721
+ }
6722
+ }
6723
+ return elements[0];
6724
+ }
6725
+ const threshold = (1 - config.tolerance) * 100;
6726
+ const scored = [];
6727
+ for (const element of elements) {
6728
+ const rejection = getBasicRejectionReason(element, config);
6729
+ if (rejection) {
6730
+ scored.push({ element, score: -1, rejection });
6731
+ continue;
6732
+ }
6733
+ const score = calculateSizeScore(element, expectedSize);
6734
+ if (score < threshold) {
6735
+ const rect = element.getBoundingClientRect();
6736
+ scored.push({
6737
+ element,
6738
+ score,
6739
+ rejection: `size mismatch: expected ~${expectedSize.width.toFixed(0)}x${expectedSize.height.toFixed(0)}, got ${rect.width.toFixed(0)}x${rect.height.toFixed(0)}`
6740
+ });
6741
+ } else {
6742
+ scored.push({ element, score });
6743
+ }
6744
+ }
6745
+ log(`ElementValidator: ${scored.length} element(s) for '${selector}' (expected ~${expectedSize.width.toFixed(0)}x${expectedSize.height.toFixed(0)})`);
6746
+ for (let i = 0; i < scored.length; i++) {
6747
+ const s = scored[i];
6748
+ const rect = s.element.getBoundingClientRect();
6749
+ const status = s.rejection ? `REJECTED (${s.rejection})` : `OK (${s.score.toFixed(0)}%)`;
6750
+ log(` [${i}] ${rect.width.toFixed(0)}x${rect.height.toFixed(0)} - ${status}`);
6751
+ }
6752
+ const valid = scored.filter((s) => !s.rejection);
6753
+ if (valid.length === 0) {
6754
+ return null;
6755
+ }
6756
+ valid.sort((a, b) => b.score - a.score);
6757
+ log(`ElementValidator: Selected element with ${valid[0].score.toFixed(0)}% match`);
6758
+ return valid[0].element;
6759
+ }
6760
+ function findAllValidElements(selector, expectedSize, config = DEFAULT_CONFIG) {
6761
+ const elements = document.querySelectorAll(selector);
6762
+ if (elements.length === 0) {
6763
+ return [];
6764
+ }
6765
+ if (!expectedSize) {
6766
+ const valid2 = [];
6767
+ for (const element of elements) {
6768
+ const rejection = getBasicRejectionReason(element, config);
6769
+ if (!rejection) {
6770
+ valid2.push(element);
6771
+ }
6772
+ }
6773
+ return valid2.length > 0 ? valid2 : Array.from(elements);
6774
+ }
6775
+ const threshold = (1 - config.tolerance) * 100;
6776
+ const valid = [];
6777
+ for (const element of elements) {
6778
+ const rejection = getBasicRejectionReason(element, config);
6779
+ if (rejection) {
6780
+ continue;
6781
+ }
6782
+ const score = calculateSizeScore(element, expectedSize);
6783
+ if (score >= threshold) {
6784
+ valid.push(element);
6785
+ }
6786
+ }
6787
+ if (valid.length > 0) {
6788
+ log(`ElementValidator: Found ${valid.length} valid element(s) for '${selector}'`);
6789
+ }
6790
+ return valid;
6791
+ }
6792
+ function isElementValid(element, expectedSize, config = DEFAULT_CONFIG) {
6793
+ const rejection = getBasicRejectionReason(element, config);
6794
+ if (rejection) {
6795
+ return false;
6796
+ }
6797
+ if (!expectedSize) {
6798
+ return true;
6799
+ }
6800
+ const threshold = (1 - config.tolerance) * 100;
6801
+ const score = calculateSizeScore(element, expectedSize);
6802
+ return score >= threshold;
6803
+ }
6688
6804
  class CursorManager {
6689
6805
  constructor() {
6690
6806
  __publicField(this, "cursor", null);
@@ -6858,40 +6974,61 @@ class CursorManager {
6858
6974
  * Sets up a MutationObserver to wait for an element to appear in the DOM
6859
6975
  * @param selector - CSS selector to wait for
6860
6976
  * @param callback - Callback to execute when element is found
6977
+ * @param expectedSize - Optional expected size for validation
6861
6978
  */
6862
- waitForElement(selector, callback) {
6979
+ waitForElement(selector, callback, expectedSize) {
6863
6980
  if (this.targetMutationObserver) {
6864
6981
  this.targetMutationObserver.disconnect();
6865
6982
  this.targetMutationObserver = null;
6866
6983
  }
6867
- this.targetMutationObserver = new MutationObserver(async (_, observer) => {
6868
- if (this.isAutoplayBlocked()) {
6869
- observer.disconnect();
6984
+ const MAX_RETRIES = 10;
6985
+ let retryCount = 0;
6986
+ let periodicCheckId = null;
6987
+ const cleanupWatchers = () => {
6988
+ if (this.targetMutationObserver) {
6989
+ this.targetMutationObserver.disconnect();
6870
6990
  this.targetMutationObserver = null;
6991
+ }
6992
+ if (periodicCheckId !== null) {
6993
+ clearInterval(periodicCheckId);
6994
+ periodicCheckId = null;
6995
+ }
6996
+ };
6997
+ const tryFindElement = async () => {
6998
+ retryCount++;
6999
+ if (retryCount > MAX_RETRIES) {
7000
+ console.warn(`CursorManager: Stopped waiting for element '${selector}' after ${MAX_RETRIES} attempts`);
7001
+ cleanupWatchers();
7002
+ return null;
7003
+ }
7004
+ return this.findElementAndScrollIntoView(selector, expectedSize);
7005
+ };
7006
+ this.targetMutationObserver = new MutationObserver(async () => {
7007
+ if (this.isAutoplayBlocked()) {
7008
+ cleanupWatchers();
6871
7009
  return;
6872
7010
  }
6873
- const el = await this.findElementAndScrollIntoView(selector);
7011
+ const el = await tryFindElement();
6874
7012
  if (el) {
6875
- observer.disconnect();
6876
- this.targetMutationObserver = null;
7013
+ cleanupWatchers();
6877
7014
  callback(el);
6878
7015
  }
6879
7016
  });
6880
7017
  this.targetMutationObserver.observe(document.body, { childList: true, subtree: true });
6881
- const periodicCheck = setInterval(() => {
7018
+ periodicCheckId = setInterval(() => {
6882
7019
  if (!this.targetMutationObserver || this.isAutoplayBlocked()) {
6883
- clearInterval(periodicCheck);
6884
- if (this.targetMutationObserver) {
6885
- this.targetMutationObserver.disconnect();
6886
- this.targetMutationObserver = null;
6887
- }
7020
+ cleanupWatchers();
7021
+ return;
7022
+ }
7023
+ retryCount++;
7024
+ if (retryCount > MAX_RETRIES) {
7025
+ console.warn(`CursorManager: Stopped waiting for element '${selector}' after ${MAX_RETRIES} attempts`);
7026
+ cleanupWatchers();
6888
7027
  return;
6889
7028
  }
6890
- const found = this.findElement(selector);
7029
+ const found = this.findElement(selector, expectedSize);
6891
7030
  if (found) {
6892
- clearInterval(periodicCheck);
6893
- this.targetMutationObserver.disconnect();
6894
- this.targetMutationObserver = null;
7031
+ cleanupWatchers();
6895
7032
  callback(found);
6896
7033
  }
6897
7034
  }, 1e3);
@@ -6943,11 +7080,20 @@ class CursorManager {
6943
7080
  }
6944
7081
  /**
6945
7082
  * Helper function to find an element in the document
6946
- * Prioritizes elements that are visible in the viewport
7083
+ * Uses size validation when expectedSize is provided
7084
+ * Falls back to viewport-based selection when no expectedSize or when validation passes
6947
7085
  * @param selector - CSS selector
7086
+ * @param expectedSize - Optional expected size for validation
6948
7087
  * @returns - The found element or null
6949
7088
  */
6950
- findElement(selector) {
7089
+ findElement(selector, expectedSize) {
7090
+ if (expectedSize) {
7091
+ const validElement = findValidElement(selector, expectedSize);
7092
+ if (validElement) {
7093
+ return validElement;
7094
+ }
7095
+ return null;
7096
+ }
6951
7097
  const elements = document.querySelectorAll(selector);
6952
7098
  if (elements.length === 0) {
6953
7099
  return null;
@@ -7087,10 +7233,11 @@ class CursorManager {
7087
7233
  /**
7088
7234
  * Finds an element and scrolls it into view if necessary
7089
7235
  * @param selector - CSS selector
7236
+ * @param expectedSize - Optional expected size for validation
7090
7237
  * @returns - Promise that resolves with the element or null
7091
7238
  */
7092
- async findElementAndScrollIntoView(selector) {
7093
- const element = this.findElement(selector);
7239
+ async findElementAndScrollIntoView(selector, expectedSize) {
7240
+ const element = this.findElement(selector, expectedSize);
7094
7241
  if (!element) {
7095
7242
  return null;
7096
7243
  }
@@ -7099,10 +7246,29 @@ class CursorManager {
7099
7246
  }
7100
7247
  return element;
7101
7248
  }
7249
+ /**
7250
+ * Cleans up any existing cursor elements from the DOM
7251
+ * This prevents duplicate elements when switching playlists
7252
+ */
7253
+ cleanupExistingElements() {
7254
+ const existingCursors = document.querySelectorAll(".sf-cursor");
7255
+ existingCursors.forEach((el) => el.remove());
7256
+ const existingLabels = document.querySelectorAll(".sf-cursor-label");
7257
+ existingLabels.forEach((el) => el.remove());
7258
+ const existingSelections = document.querySelectorAll(".sf-selection");
7259
+ existingSelections.forEach((el) => el.remove());
7260
+ const existingFlashlights = document.querySelectorAll(".sf-flashlight-overlay");
7261
+ existingFlashlights.forEach((el) => el.remove());
7262
+ this.cursor = null;
7263
+ this.labelElement = null;
7264
+ this.selectionElement = null;
7265
+ this.flashlightOverlay = null;
7266
+ }
7102
7267
  /**
7103
7268
  * Creates the virtual cursor element
7104
7269
  */
7105
7270
  create() {
7271
+ this.cleanupExistingElements();
7106
7272
  this.injectCursorStyles();
7107
7273
  this.cursor = document.createElement("div");
7108
7274
  this.cursor.className = "sf-cursor";
@@ -7411,12 +7577,12 @@ class CursorManager {
7411
7577
  if (this.isAutoplayBlocked()) {
7412
7578
  return;
7413
7579
  }
7414
- const targetElement = await this.findElementAndScrollIntoView(animation.targetSelector);
7580
+ const targetElement = await this.findElementAndScrollIntoView(animation.targetSelector, animation.expectedSize);
7415
7581
  if (!targetElement) {
7416
7582
  console.warn("CursorManager: Target element not found in animate:", animation.targetSelector);
7417
7583
  this.setShouldShowCursor(false);
7418
7584
  this.hideCursorElements();
7419
- this.waitForElement(animation.targetSelector, () => this.animate(animation));
7585
+ this.waitForElement(animation.targetSelector, () => this.animate(animation), animation.expectedSize);
7420
7586
  return;
7421
7587
  }
7422
7588
  this.setShouldShowCursor(true);
@@ -9090,7 +9256,7 @@ class TransitionManager {
9090
9256
  element.addEventListener("click", handler, { capture: true });
9091
9257
  });
9092
9258
  };
9093
- const initialElements = document.querySelectorAll(selector);
9259
+ const initialElements = transition.expectedSize ? findAllValidElements(selector, transition.expectedSize) : Array.from(document.querySelectorAll(selector));
9094
9260
  addClickHandlersToElements(initialElements);
9095
9261
  mutationObserver = new MutationObserver((mutationsList) => {
9096
9262
  for (const mutation of mutationsList) {
@@ -9099,12 +9265,17 @@ class TransitionManager {
9099
9265
  if (node.nodeType === Node.ELEMENT_NODE) {
9100
9266
  const elementNode = node;
9101
9267
  if (elementNode.matches(selector)) {
9102
- addClickHandlersToElements([elementNode]);
9268
+ if (!transition.expectedSize || isElementValid(elementNode, transition.expectedSize)) {
9269
+ addClickHandlersToElements([elementNode]);
9270
+ }
9103
9271
  }
9104
9272
  const matchingDescendants = elementNode.querySelectorAll(selector);
9105
9273
  if (matchingDescendants.length > 0) {
9106
- log(`TransitionManager: Found ${matchingDescendants.length} descendants matching '${selector}'`);
9107
- addClickHandlersToElements(matchingDescendants);
9274
+ const validDescendants = transition.expectedSize ? Array.from(matchingDescendants).filter((el) => isElementValid(el, transition.expectedSize)) : Array.from(matchingDescendants);
9275
+ if (validDescendants.length > 0) {
9276
+ log(`TransitionManager: Found ${validDescendants.length} valid descendants matching '${selector}'`);
9277
+ addClickHandlersToElements(validDescendants);
9278
+ }
9108
9279
  }
9109
9280
  }
9110
9281
  });
@@ -9436,7 +9607,7 @@ class TransitionManager {
9436
9607
  mutationObserver = null;
9437
9608
  }
9438
9609
  };
9439
- const initialElement = document.querySelector(selector);
9610
+ const initialElement = transition.expectedSize ? findValidElement(selector, transition.expectedSize) : document.querySelector(selector);
9440
9611
  if (initialElement) {
9441
9612
  setupIntersectionObserver(initialElement);
9442
9613
  } else {
@@ -9447,10 +9618,12 @@ class TransitionManager {
9447
9618
  if (node.nodeType === Node.ELEMENT_NODE) {
9448
9619
  const elementNode = node;
9449
9620
  if (elementNode.matches(selector)) {
9450
- setupIntersectionObserver(elementNode);
9451
- return;
9621
+ if (!transition.expectedSize || isElementValid(elementNode, transition.expectedSize)) {
9622
+ setupIntersectionObserver(elementNode);
9623
+ return;
9624
+ }
9452
9625
  }
9453
- const matchingDescendant = elementNode.querySelector(selector);
9626
+ const matchingDescendant = transition.expectedSize ? findValidElement(selector, transition.expectedSize) : elementNode.querySelector(selector);
9454
9627
  if (matchingDescendant) {
9455
9628
  setupIntersectionObserver(matchingDescendant);
9456
9629
  return;
@@ -9471,7 +9644,7 @@ class TransitionManager {
9471
9644
  }
9472
9645
  return;
9473
9646
  }
9474
- const element = document.querySelector(selector);
9647
+ const element = transition.expectedSize ? findValidElement(selector, transition.expectedSize) : document.querySelector(selector);
9475
9648
  if (element && element.offsetWidth > 0 && element.offsetHeight > 0) {
9476
9649
  if (periodicCheck) {
9477
9650
  clearInterval(periodicCheck);
@@ -9930,7 +10103,11 @@ class TriggerManager {
9930
10103
  this.triggeredPlaylists.forEach((playlist) => {
9931
10104
  var _a;
9932
10105
  if ((_a = playlist.triggers) == null ? void 0 : _a.elementClicked) {
9933
- this.setupElementClickListener(playlist.id, playlist.triggers.elementClicked);
10106
+ this.setupElementClickListener(
10107
+ playlist.id,
10108
+ playlist.triggers.elementClicked,
10109
+ playlist.triggers.elementClickedExpectedSize
10110
+ );
9934
10111
  }
9935
10112
  });
9936
10113
  }
@@ -9938,10 +10115,11 @@ class TriggerManager {
9938
10115
  * Sets up a click event listener for a specific playlist and selector
9939
10116
  * @param playlistId - The playlist ID
9940
10117
  * @param selector - CSS selector for the target element
10118
+ * @param expectedSize - Optional expected size for validation
9941
10119
  */
9942
- setupElementClickListener(playlistId, selector) {
10120
+ setupElementClickListener(playlistId, selector, expectedSize) {
9943
10121
  try {
9944
- const element = document.querySelector(selector);
10122
+ const element = expectedSize ? findValidElement(selector, expectedSize) : document.querySelector(selector);
9945
10123
  if (!element) {
9946
10124
  log(`TriggerManager: Element not found for selector '${selector}' (playlist: ${playlistId})`);
9947
10125
  return;
@@ -9990,7 +10168,11 @@ class TriggerManager {
9990
10168
  this.triggeredPlaylists.forEach((playlist) => {
9991
10169
  var _a;
9992
10170
  if ((_a = playlist.triggers) == null ? void 0 : _a.elementVisible) {
9993
- this.setupElementVisibleObserver(playlist.id, playlist.triggers.elementVisible);
10171
+ this.setupElementVisibleObserver(
10172
+ playlist.id,
10173
+ playlist.triggers.elementVisible,
10174
+ playlist.triggers.elementVisibleExpectedSize
10175
+ );
9994
10176
  }
9995
10177
  });
9996
10178
  }
@@ -9998,8 +10180,9 @@ class TriggerManager {
9998
10180
  * Sets up a visibility observer for a specific playlist and selector
9999
10181
  * @param playlistId - The playlist ID
10000
10182
  * @param selector - CSS selector for the target element
10183
+ * @param expectedSize - Optional expected size for validation
10001
10184
  */
10002
- setupElementVisibleObserver(playlistId, selector) {
10185
+ setupElementVisibleObserver(playlistId, selector, expectedSize) {
10003
10186
  try {
10004
10187
  const observerId = `${playlistId}-${selector}`;
10005
10188
  if (this.elementVisibleObservers.has(observerId)) {
@@ -10057,7 +10240,7 @@ class TriggerManager {
10057
10240
  });
10058
10241
  }
10059
10242
  };
10060
- const initialElement = document.querySelector(selector);
10243
+ const initialElement = expectedSize ? findValidElement(selector, expectedSize) : document.querySelector(selector);
10061
10244
  if (initialElement) {
10062
10245
  log(`TriggerManager: Found element matching '${selector}' immediately`);
10063
10246
  setupIntersectionObserver(initialElement);
@@ -10070,10 +10253,12 @@ class TriggerManager {
10070
10253
  if (node.nodeType === Node.ELEMENT_NODE) {
10071
10254
  const elementNode = node;
10072
10255
  if (elementNode.matches(selector)) {
10073
- log(`TriggerManager: Added node matches '${selector}'`);
10074
- setupIntersectionObserver(elementNode);
10256
+ if (!expectedSize || isElementValid(elementNode, expectedSize)) {
10257
+ log(`TriggerManager: Added node matches '${selector}'`);
10258
+ setupIntersectionObserver(elementNode);
10259
+ }
10075
10260
  } else {
10076
- const matchingDescendant = elementNode.querySelector(selector);
10261
+ const matchingDescendant = expectedSize ? findValidElement(selector, expectedSize) : elementNode.querySelector(selector);
10077
10262
  if (matchingDescendant) {
10078
10263
  log(`TriggerManager: Found descendant matching '${selector}'`);
10079
10264
  setupIntersectionObserver(matchingDescendant);
@@ -10166,6 +10351,16 @@ class TriggerManager {
10166
10351
  getTriggeredPlaylists() {
10167
10352
  return Array.from(this.triggeredPlaylistsSet);
10168
10353
  }
10354
+ /**
10355
+ * Marks a playlist as triggered to prevent re-triggering during URL changes
10356
+ * This should be called when a playlist is started programmatically
10357
+ * @param playlistId - The playlist ID to mark as triggered
10358
+ */
10359
+ markPlaylistAsTriggered(playlistId) {
10360
+ if (!this.triggeredPlaylistsSet.has(playlistId)) {
10361
+ this.triggeredPlaylistsSet.add(playlistId);
10362
+ }
10363
+ }
10169
10364
  /**
10170
10365
  * Cleanup method to be called on destroy
10171
10366
  */
@@ -11977,7 +12172,7 @@ const SaltfishPlayer$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de
11977
12172
  __proto__: null,
11978
12173
  SaltfishPlayer
11979
12174
  }, Symbol.toStringTag, { value: "Module" }));
11980
- const version = "0.3.55";
12175
+ const version = "0.3.57";
11981
12176
  const packageJson = {
11982
12177
  version
11983
12178
  };