saltfish 0.3.70 → 0.3.73

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.
@@ -1905,6 +1905,72 @@ const THRESHOLDS = {
1905
1905
  /** Minimum scroll distance in pixels to trigger scroll events */
1906
1906
  SCROLL_THRESHOLD_PX: 10
1907
1907
  };
1908
+ function stripShareIdFromUrl(url) {
1909
+ try {
1910
+ let cleanedUrl = url.replace(/[?&]saltfish-share-id=[^&#]*/g, (match, offset) => {
1911
+ if (match.startsWith("?")) {
1912
+ const afterMatch = url.substring(offset + match.length);
1913
+ if (afterMatch.startsWith("&")) {
1914
+ return "?";
1915
+ }
1916
+ return "";
1917
+ }
1918
+ return "";
1919
+ });
1920
+ cleanedUrl = cleanedUrl.replace(/\?&/g, "?");
1921
+ cleanedUrl = cleanedUrl.replace(/&&/g, "&");
1922
+ cleanedUrl = cleanedUrl.replace(/\?$/g, "");
1923
+ cleanedUrl = cleanedUrl.replace(/&$/g, "");
1924
+ return cleanedUrl;
1925
+ } catch (error2) {
1926
+ return url;
1927
+ }
1928
+ }
1929
+ function validateUrlRequirement(urlRequirement) {
1930
+ const { pattern, matchType } = urlRequirement;
1931
+ if (!pattern) {
1932
+ return true;
1933
+ }
1934
+ const currentUrl = stripShareIdFromUrl(window.location.href);
1935
+ const currentPath = window.location.pathname;
1936
+ if (matchType === "regex") {
1937
+ try {
1938
+ const regex = new RegExp(pattern);
1939
+ const fullUrlMatch = regex.test(currentUrl);
1940
+ const pathMatch = regex.test(currentPath);
1941
+ const matches2 = fullUrlMatch || pathMatch;
1942
+ log(`urlValidation: Result (regex) - matches: ${matches2}`);
1943
+ return matches2;
1944
+ } catch (error2) {
1945
+ return false;
1946
+ }
1947
+ }
1948
+ if (matchType === "contains") {
1949
+ const matches2 = currentUrl.includes(pattern) || currentPath.includes(pattern);
1950
+ return matches2;
1951
+ }
1952
+ const matches = currentUrl === pattern || currentPath === pattern;
1953
+ return matches;
1954
+ }
1955
+ async function validateUrlRequirementWithRetry(urlRequirement, maxRetries = 20, retryDelay = 100) {
1956
+ if (!urlRequirement) {
1957
+ return true;
1958
+ }
1959
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1960
+ if (validateUrlRequirement(urlRequirement)) {
1961
+ return true;
1962
+ }
1963
+ if (attempt < maxRetries - 1) {
1964
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
1965
+ const saltfishPlayer = window._saltfishPlayer;
1966
+ if (!saltfishPlayer) {
1967
+ return false;
1968
+ }
1969
+ }
1970
+ }
1971
+ log(`urlValidation: Expected pattern: '${urlRequirement.pattern}' (matchType: ${urlRequirement.matchType})`);
1972
+ return false;
1973
+ }
1908
1974
  class PlayerInitializationService {
1909
1975
  constructor(managers) {
1910
1976
  __publicField(this, "managers");
@@ -2282,7 +2348,7 @@ class PlayerInitializationService {
2282
2348
  if (!pattern || typeof window === "undefined") {
2283
2349
  return false;
2284
2350
  }
2285
- const currentUrl = window.location.href;
2351
+ const currentUrl = stripShareIdFromUrl(window.location.href);
2286
2352
  const currentPath = window.location.pathname;
2287
2353
  const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2288
2354
  const regexPattern = escapedPattern.replace(/\\\*/g, ".*");
@@ -2917,7 +2983,6 @@ class PlaylistOrchestrator {
2917
2983
  });
2918
2984
  }
2919
2985
  }
2920
- this.managers.analyticsManager.trackPlaylistStart(playlistId);
2921
2986
  this.managers.cursorManager.resetFirstAnimation();
2922
2987
  log(`[PlaylistOrchestrator.startPlaylist] Using validated manifest path: ${manifestPathToLoad}`);
2923
2988
  await this.managers.playlistManager.load(manifestPathToLoad, { ...finalOptions, persistence: playlistPersistence });
@@ -3012,51 +3077,6 @@ class PlaylistOrchestrator {
3012
3077
  destroy() {
3013
3078
  }
3014
3079
  }
3015
- function validateUrlRequirement(urlRequirement) {
3016
- const { pattern, matchType } = urlRequirement;
3017
- if (!pattern) {
3018
- return true;
3019
- }
3020
- const currentUrl = window.location.href;
3021
- const currentPath = window.location.pathname;
3022
- if (matchType === "regex") {
3023
- try {
3024
- const regex = new RegExp(pattern);
3025
- const fullUrlMatch = regex.test(currentUrl);
3026
- const pathMatch = regex.test(currentPath);
3027
- const matches2 = fullUrlMatch || pathMatch;
3028
- log(`urlValidation: Result (regex) - matches: ${matches2}`);
3029
- return matches2;
3030
- } catch (error2) {
3031
- return false;
3032
- }
3033
- }
3034
- if (matchType === "contains") {
3035
- const matches2 = currentUrl.includes(pattern) || currentPath.includes(pattern);
3036
- return matches2;
3037
- }
3038
- const matches = currentUrl === pattern || currentPath === pattern;
3039
- return matches;
3040
- }
3041
- async function validateUrlRequirementWithRetry(urlRequirement, maxRetries = 20, retryDelay = 100) {
3042
- if (!urlRequirement) {
3043
- return true;
3044
- }
3045
- for (let attempt = 0; attempt < maxRetries; attempt++) {
3046
- if (validateUrlRequirement(urlRequirement)) {
3047
- return true;
3048
- }
3049
- if (attempt < maxRetries - 1) {
3050
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
3051
- const saltfishPlayer = window._saltfishPlayer;
3052
- if (!saltfishPlayer) {
3053
- return false;
3054
- }
3055
- }
3056
- }
3057
- log(`urlValidation: Expected pattern: '${urlRequirement.pattern}' (matchType: ${urlRequirement.matchType})`);
3058
- return false;
3059
- }
3060
3080
  class StateMachineActionHandler {
3061
3081
  constructor(managers) {
3062
3082
  __publicField(this, "managers");
@@ -3729,6 +3749,39 @@ function setupEventUpdater(eventManager) {
3729
3749
  let prevPreviousState = null;
3730
3750
  let prevIsMinimized = null;
3731
3751
  let prevStepId = null;
3752
+ let pendingStartEvents = { playlistData: null, stepData: null };
3753
+ let autoplayConfirmationTimer = null;
3754
+ const firePendingEvents = () => {
3755
+ if (autoplayConfirmationTimer) {
3756
+ clearTimeout(autoplayConfirmationTimer);
3757
+ autoplayConfirmationTimer = null;
3758
+ }
3759
+ if (pendingStartEvents.playlistData) {
3760
+ eventManager.trigger("playlistStarted", {
3761
+ timestamp: Date.now(),
3762
+ playlist: pendingStartEvents.playlistData
3763
+ });
3764
+ }
3765
+ if (pendingStartEvents.stepData) {
3766
+ eventManager.trigger("stepStarted", {
3767
+ timestamp: Date.now(),
3768
+ step: { id: pendingStartEvents.stepData.id, title: pendingStartEvents.stepData.title },
3769
+ playlist: { id: pendingStartEvents.stepData.playlistId, title: pendingStartEvents.stepData.playlistTitle }
3770
+ });
3771
+ }
3772
+ pendingStartEvents = { playlistData: null, stepData: null };
3773
+ };
3774
+ const startAutoplayConfirmationTimer = () => {
3775
+ if (autoplayConfirmationTimer) {
3776
+ clearTimeout(autoplayConfirmationTimer);
3777
+ }
3778
+ autoplayConfirmationTimer = setTimeout(() => {
3779
+ if (pendingStartEvents.playlistData || pendingStartEvents.stepData) {
3780
+ firePendingEvents();
3781
+ }
3782
+ autoplayConfirmationTimer = null;
3783
+ }, 500);
3784
+ };
3732
3785
  const unsubscribe = useSaltfishStore.subscribe(
3733
3786
  (state) => {
3734
3787
  var _a;
@@ -3753,30 +3806,55 @@ function setupEventUpdater(eventManager) {
3753
3806
  log(`EventUpdater: Processing state change from '${eventData.previousState}' to '${eventData.currentState}'`, {
3754
3807
  manifestId: (_a = state.manifest) == null ? void 0 : _a.id
3755
3808
  });
3756
- handleStateTransitionEvents(eventData, state, eventManager);
3809
+ if (eventData.previousState === "playing") {
3810
+ if (eventData.currentState === "autoplayBlocked" || eventData.currentState === "idleMode") {
3811
+ if (autoplayConfirmationTimer) {
3812
+ clearTimeout(autoplayConfirmationTimer);
3813
+ autoplayConfirmationTimer = null;
3814
+ }
3815
+ if (pendingStartEvents.playlistData || pendingStartEvents.stepData) {
3816
+ pendingStartEvents = { playlistData: null, stepData: null };
3817
+ }
3818
+ } else if (pendingStartEvents.playlistData || pendingStartEvents.stepData) {
3819
+ firePendingEvents();
3820
+ }
3821
+ }
3822
+ handleStateTransitionEvents(eventData, state, eventManager, pendingStartEvents);
3757
3823
  handleMinimizeEvents(eventData, state, eventManager, actualPrevMinimized);
3758
- handleStepEvents(eventData, state, eventManager, actualPrevStepId);
3824
+ handleStepEvents(eventData, state, eventManager, actualPrevStepId, pendingStartEvents);
3759
3825
  handleErrorEvents(eventData, state, eventManager);
3826
+ if ((pendingStartEvents.playlistData || pendingStartEvents.stepData) && !autoplayConfirmationTimer) {
3827
+ startAutoplayConfirmationTimer();
3828
+ }
3760
3829
  }
3761
3830
  );
3762
3831
  return unsubscribe;
3763
3832
  }
3764
- function handleStateTransitionEvents(eventData, store, eventManager) {
3833
+ function handleStateTransitionEvents(eventData, store, eventManager, pendingStartEvents) {
3765
3834
  const { prevPreviousState, previousState, currentState, currentStepId } = eventData;
3766
3835
  const isNormalPlaylistStart = currentState === "playing" && previousState === "paused" && prevPreviousState === "loading";
3767
- const isDelayedPlaylistStart = currentState === "playing" && (previousState === "autoplayBlocked" || previousState === "idleMode");
3768
- if ((isNormalPlaylistStart || isDelayedPlaylistStart) && store.manifest && currentStepId) {
3836
+ const isUserInitiatedPlaylistStart = currentState === "playing" && (previousState === "autoplayBlocked" || previousState === "idleMode");
3837
+ if (store.manifest && currentStepId) {
3769
3838
  const isStartingNode = currentStepId === store.manifest.startStep;
3770
3839
  if (isStartingNode) {
3771
- log(`EventUpdater: Triggering playlistStarted event for ${store.manifest.id} (starting node: ${currentStepId})`);
3772
- eventManager.trigger("playlistStarted", {
3773
- timestamp: Date.now(),
3774
- playlist: {
3840
+ if (isUserInitiatedPlaylistStart) {
3841
+ log(`EventUpdater: Triggering playlistStarted event for ${store.manifest.id} (user-initiated from ${previousState})`);
3842
+ eventManager.trigger("playlistStarted", {
3843
+ timestamp: Date.now(),
3844
+ playlist: {
3845
+ id: store.manifest.id,
3846
+ title: store.manifest.name
3847
+ }
3848
+ });
3849
+ return;
3850
+ } else if (isNormalPlaylistStart) {
3851
+ log(`EventUpdater: Deferring playlistStarted event for ${store.manifest.id} (pending autoplay confirmation)`);
3852
+ pendingStartEvents.playlistData = {
3775
3853
  id: store.manifest.id,
3776
3854
  title: store.manifest.name
3777
- }
3778
- });
3779
- return;
3855
+ };
3856
+ return;
3857
+ }
3780
3858
  }
3781
3859
  }
3782
3860
  if (previousState === "paused" && currentState === "playing") {
@@ -3810,7 +3888,7 @@ function handleMinimizeEvents(eventData, _store, eventManager, prevIsMinimized)
3810
3888
  });
3811
3889
  }
3812
3890
  }
3813
- function handleStepEvents(eventData, store, eventManager, prevStepId) {
3891
+ function handleStepEvents(eventData, store, eventManager, prevStepId, pendingStartEvents) {
3814
3892
  var _a, _b, _c;
3815
3893
  const { prevPreviousState, previousState, currentStepId, currentState } = eventData;
3816
3894
  const currentStep = store.currentStepId ? (((_a = store.manifest) == null ? void 0 : _a.steps) || []).find((s) => s.id === store.currentStepId) : null;
@@ -3844,26 +3922,38 @@ function handleStepEvents(eventData, store, eventManager, prevStepId) {
3844
3922
  }
3845
3923
  }
3846
3924
  const isStepChange = currentStepId !== prevStepId && currentState === "playing";
3847
- const isAutoplayOrIdleModeTransition = (previousState === "autoplayBlocked" || previousState === "idleMode") && currentState === "playing" && prevStepId === currentStepId;
3848
- const isFirstStepOfPlaylist = prevPreviousState === "loading" && previousState === "paused" && currentState === "playing" && currentStepId;
3849
- const shouldTriggerStepStarted = currentStep && store.manifest && (isStepChange || isAutoplayOrIdleModeTransition || isFirstStepOfPlaylist);
3925
+ const isUserInitiatedStepStart = (previousState === "autoplayBlocked" || previousState === "idleMode") && currentState === "playing" && prevStepId === currentStepId;
3926
+ const isFirstPlayOfPlaylist = prevPreviousState === "loading" && previousState === "paused" && currentState === "playing" && currentStepId;
3927
+ const isStartingNode = store.manifest && currentStepId === store.manifest.startStep;
3928
+ const isFirstStepAutoplayAttempt = isStartingNode && isFirstPlayOfPlaylist;
3929
+ const isNonStartingNodeFirstPlay = !isStartingNode && isFirstPlayOfPlaylist;
3850
3930
  log("EventUpdater.handleStepEvents: Step started check", {
3851
3931
  currentStep: currentStep == null ? void 0 : currentStep.id,
3852
3932
  hasManifest: !!store.manifest
3853
3933
  });
3854
- if (shouldTriggerStepStarted) {
3855
- log(`EventUpdater: Triggering stepStarted for ${currentStep.id}`);
3856
- eventManager.trigger("stepStarted", {
3857
- timestamp: Date.now(),
3858
- step: {
3934
+ if (currentStep && store.manifest) {
3935
+ if (isStepChange || isUserInitiatedStepStart || isNonStartingNodeFirstPlay) {
3936
+ log(`EventUpdater: Triggering stepStarted for ${currentStep.id}`);
3937
+ eventManager.trigger("stepStarted", {
3938
+ timestamp: Date.now(),
3939
+ step: {
3940
+ id: currentStep.id,
3941
+ title: currentStep.title || currentStep.id
3942
+ },
3943
+ playlist: {
3944
+ id: store.manifest.id,
3945
+ title: store.manifest.name
3946
+ }
3947
+ });
3948
+ } else if (isFirstStepAutoplayAttempt) {
3949
+ log(`EventUpdater: Deferring stepStarted for ${currentStep.id} (pending autoplay confirmation)`);
3950
+ pendingStartEvents.stepData = {
3859
3951
  id: currentStep.id,
3860
- title: currentStep.title || currentStep.id
3861
- },
3862
- playlist: {
3863
- id: store.manifest.id,
3864
- title: store.manifest.name
3865
- }
3866
- });
3952
+ title: currentStep.title || currentStep.id,
3953
+ playlistId: store.manifest.id,
3954
+ playlistTitle: store.manifest.name
3955
+ };
3956
+ }
3867
3957
  }
3868
3958
  }
3869
3959
  function handleErrorEvents(eventData, store, eventManager) {
@@ -8939,7 +9029,6 @@ class EventSubscriberManager {
8939
9029
  }
8940
9030
  }
8941
9031
  class AnalyticsManager extends EventSubscriberManager {
8942
- // Default to enabled
8943
9032
  /**
8944
9033
  * Creates a new AnalyticsManager
8945
9034
  * @param eventManager - Optional event manager to subscribe to events
@@ -8953,6 +9042,8 @@ class AnalyticsManager extends EventSubscriberManager {
8953
9042
  __publicField(this, "flushInterval", null);
8954
9043
  __publicField(this, "sessionId", null);
8955
9044
  __publicField(this, "analyticsEnabled", true);
9045
+ // Default to enabled
9046
+ __publicField(this, "boundVisibilityHandler", null);
8956
9047
  }
8957
9048
  /**
8958
9049
  * Subscribe to relevant player events for analytics tracking
@@ -9039,6 +9130,10 @@ class AnalyticsManager extends EventSubscriberManager {
9039
9130
  });
9040
9131
  }
9041
9132
  });
9133
+ this.eventManager.on("playlistStarted", (event) => {
9134
+ log(`AnalyticsManager: Playlist started event received - ${event.playlist.id}`);
9135
+ this.trackPlaylistStart(event.playlist.id);
9136
+ });
9042
9137
  }
9043
9138
  /**
9044
9139
  * Initializes the analytics manager
@@ -9055,6 +9150,8 @@ class AnalyticsManager extends EventSubscriberManager {
9055
9150
  this.flushInterval = window.setInterval(() => {
9056
9151
  this.flushEvents();
9057
9152
  }, ANALYTICS.FLUSH_INTERVAL_MS);
9153
+ this.boundVisibilityHandler = this.handleVisibilityChange.bind(this);
9154
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
9058
9155
  }
9059
9156
  }
9060
9157
  /**
@@ -9251,6 +9348,45 @@ class AnalyticsManager extends EventSubscriberManager {
9251
9348
  this.isSending = false;
9252
9349
  }
9253
9350
  }
9351
+ /**
9352
+ * Handles visibility change events to flush analytics when page is hidden
9353
+ * Uses sendBeacon for reliable delivery during page unload
9354
+ */
9355
+ handleVisibilityChange() {
9356
+ if (document.visibilityState === "hidden") {
9357
+ this.flushWithBeacon();
9358
+ }
9359
+ }
9360
+ /**
9361
+ * Flushes events using sendBeacon API for reliable delivery during page unload
9362
+ * sendBeacon is fire-and-forget and survives page navigation/close
9363
+ */
9364
+ flushWithBeacon() {
9365
+ if (this.eventQueue.length === 0 || !this.config || !this.analyticsEnabled) {
9366
+ return;
9367
+ }
9368
+ const events = [...this.eventQueue];
9369
+ this.eventQueue = [];
9370
+ const payload = {
9371
+ token: this.config.token,
9372
+ sessionId: this.sessionId,
9373
+ user: this.user,
9374
+ events
9375
+ };
9376
+ try {
9377
+ fetch("https://player.saltfish.ai/analytics", {
9378
+ method: "POST",
9379
+ headers: {
9380
+ "Content-Type": "application/json"
9381
+ },
9382
+ body: JSON.stringify(payload),
9383
+ keepalive: true
9384
+ });
9385
+ } catch (error2) {
9386
+ console.warn("AnalyticsManager: fetch keepalive failed, events returned to queue");
9387
+ this.eventQueue = [...events, ...this.eventQueue];
9388
+ }
9389
+ }
9254
9390
  /**
9255
9391
  * Cleans up resources used by the analytics manager
9256
9392
  */
@@ -9262,6 +9398,10 @@ class AnalyticsManager extends EventSubscriberManager {
9262
9398
  clearInterval(this.flushInterval);
9263
9399
  this.flushInterval = null;
9264
9400
  }
9401
+ if (this.boundVisibilityHandler) {
9402
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
9403
+ this.boundVisibilityHandler = null;
9404
+ }
9265
9405
  if (this.eventManager) {
9266
9406
  this.eventManager = null;
9267
9407
  }
@@ -9726,7 +9866,7 @@ class TransitionManager {
9726
9866
  if (!pattern) {
9727
9867
  return false;
9728
9868
  }
9729
- const currentUrl = window.location.href;
9869
+ const currentUrl = stripShareIdFromUrl(window.location.href);
9730
9870
  const currentPath = window.location.pathname;
9731
9871
  const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9732
9872
  const regexPattern = escapedPattern.replace(/\\\*/g, ".*");
@@ -10241,7 +10381,7 @@ class TriggerManager {
10241
10381
  if (!pattern) {
10242
10382
  return true;
10243
10383
  }
10244
- const currentUrl = this.normalizeUrl(window.location.href.split("#")[0]);
10384
+ const currentUrl = this.normalizeUrl(stripShareIdFromUrl(window.location.href.split("#")[0]));
10245
10385
  const currentPath = window.location.pathname;
10246
10386
  if (triggers.urlMatchType === "regex") {
10247
10387
  try {
@@ -12569,7 +12709,7 @@ const SaltfishPlayer$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de
12569
12709
  __proto__: null,
12570
12710
  SaltfishPlayer
12571
12711
  }, Symbol.toStringTag, { value: "Module" }));
12572
- const version = "0.3.70";
12712
+ const version = "0.3.73";
12573
12713
  const packageJson = {
12574
12714
  version
12575
12715
  };