saltfish 0.3.61 → 0.3.63

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.
@@ -955,7 +955,8 @@ const playerStateMachineConfig = {
955
955
  const STORAGE_KEYS = {
956
956
  PROGRESS: "saltfish_progress",
957
957
  SESSION: "saltfish_session",
958
- ANONYMOUS_USER: "saltfish_anonymous_user_data"
958
+ ANONYMOUS_USER: "saltfish_anonymous_user_data",
959
+ PENDING_NAVIGATION: "saltfish_pending_navigation"
959
960
  };
960
961
  const API = {
961
962
  BASE_URL: "https://player.saltfish.ai",
@@ -1155,6 +1156,32 @@ const _StorageManager = class _StorageManager {
1155
1156
  this.safeClearItem(STORAGE_KEYS.ANONYMOUS_USER);
1156
1157
  }
1157
1158
  // =============================================================================
1159
+ // Pending Navigation Methods (Cross-page URL transitions)
1160
+ // =============================================================================
1161
+ /**
1162
+ * Get pending navigation data for cross-page URL transitions
1163
+ * Used when a step has a url-path transition and user navigates causing hard refresh
1164
+ */
1165
+ getPendingNavigation() {
1166
+ return this.safeGetItem(STORAGE_KEYS.PENDING_NAVIGATION);
1167
+ }
1168
+ /**
1169
+ * Set pending navigation data
1170
+ * Called when setting up a url-path transition to enable resuming after hard refresh
1171
+ * @param data - The pending navigation data to save
1172
+ */
1173
+ setPendingNavigation(data) {
1174
+ log(`[StorageManager] Saving pending navigation to step ${data.nextStepId} for pattern ${data.urlPattern}`);
1175
+ return this.safeSetItem(STORAGE_KEYS.PENDING_NAVIGATION, data);
1176
+ }
1177
+ /**
1178
+ * Clear pending navigation data
1179
+ * Called after successful transition or when navigation is no longer valid
1180
+ */
1181
+ clearPendingNavigation() {
1182
+ this.safeClearItem(STORAGE_KEYS.PENDING_NAVIGATION);
1183
+ }
1184
+ // =============================================================================
1158
1185
  // Utility Methods
1159
1186
  // =============================================================================
1160
1187
  /**
@@ -1164,6 +1191,7 @@ const _StorageManager = class _StorageManager {
1164
1191
  this.clearProgress();
1165
1192
  this.clearSession();
1166
1193
  this.clearAnonymousUserData();
1194
+ this.clearPendingNavigation();
1167
1195
  }
1168
1196
  };
1169
1197
  __publicField(_StorageManager, "instance", null);
@@ -1836,6 +1864,47 @@ class ShareLinkService {
1836
1864
  }
1837
1865
  }
1838
1866
  }
1867
+ const ANALYTICS = {
1868
+ /** Interval for flushing analytics events to the backend (30 seconds) */
1869
+ FLUSH_INTERVAL_MS: 3e4
1870
+ };
1871
+ const TIMING = {
1872
+ // Polling and updates
1873
+ /** Video progress polling interval (50ms) */
1874
+ VIDEO_PROGRESS_POLL_INTERVAL: 50,
1875
+ /** Cursor position update throttle interval (100ms) */
1876
+ CURSOR_UPDATE_THROTTLE: 100,
1877
+ /** Delay for state processing operations (100ms) */
1878
+ STATE_PROCESSING_DELAY_MS: 100,
1879
+ // Analytics
1880
+ /** Analytics event flush interval (30 seconds) */
1881
+ ANALYTICS_FLUSH_INTERVAL: 3e4,
1882
+ // Timeouts
1883
+ /** User data loading timeout (5 seconds) */
1884
+ USER_DATA_TIMEOUT: 5e3,
1885
+ /** Step timeout - player will be destroyed if user stays on same step (120 seconds) */
1886
+ STEP_TIMEOUT: 12e4,
1887
+ /** Retry delay for failed operations (0.5 seconds) */
1888
+ RETRY_DELAY_MS: 500,
1889
+ // DOM and cursor operations
1890
+ /** Delay for DOM stabilization before cursor operations (0.5 seconds) */
1891
+ DOM_STABILIZATION_DELAY_MS: 500,
1892
+ /** Default cursor animation distance in pixels */
1893
+ CURSOR_DEFAULT_DISTANCE: 100,
1894
+ // URL monitoring
1895
+ /** Interval for checking URL path changes (5 seconds) */
1896
+ URL_PATH_CHECK_INTERVAL_MS: 5e3,
1897
+ // Session persistence
1898
+ /** Session expiry time (30 minutes) */
1899
+ SESSION_EXPIRY: 30 * 60 * 1e3,
1900
+ // Cross-page navigation
1901
+ /** Pending navigation expiry time (60 seconds) - longer than normal 6s rule for URL transitions */
1902
+ PENDING_NAVIGATION_EXPIRY: 60 * 1e3
1903
+ };
1904
+ const THRESHOLDS = {
1905
+ /** Minimum scroll distance in pixels to trigger scroll events */
1906
+ SCROLL_THRESHOLD_PX: 10
1907
+ };
1839
1908
  class PlayerInitializationService {
1840
1909
  constructor(managers) {
1841
1910
  __publicField(this, "managers");
@@ -2000,7 +2069,11 @@ class PlayerInitializationService {
2000
2069
  if (this.userManagementService) {
2001
2070
  this.userManagementService.resolveUserDataLoaded();
2002
2071
  }
2003
- const resumedPlaylist = await this.checkAndResumeInProgressPlaylist(data.watchedPlaylists || {});
2072
+ const resumedFromPendingNav = await this.checkAndResumeFromPendingNavigation();
2073
+ if (resumedFromPendingNav) {
2074
+ log("[PlayerInitializationService.fetchUserData] Resumed from pending URL navigation, skipping other checks");
2075
+ }
2076
+ const resumedPlaylist = resumedFromPendingNav || await this.checkAndResumeInProgressPlaylist(data.watchedPlaylists || {});
2004
2077
  if (!resumedPlaylist) {
2005
2078
  const shareData = await this.shareLinkService.shouldAutoStartSharePlaylist();
2006
2079
  if (shareData && this.playlistOrchestrator) {
@@ -2086,8 +2159,12 @@ class PlayerInitializationService {
2086
2159
  watchedPlaylists: anonymousUserData.watchedPlaylists || {}
2087
2160
  }
2088
2161
  });
2162
+ const resumedFromPendingNav = await this.checkAndResumeFromPendingNavigation();
2163
+ if (resumedFromPendingNav) {
2164
+ log("[PlayerInitializationService.loadAnonymousUserData] Resumed from pending URL navigation, skipping other checks");
2165
+ }
2089
2166
  const watchedPlaylists = anonymousUserData.watchedPlaylists || {};
2090
- const resumedPlaylist = await this.checkAndResumeInProgressPlaylist(watchedPlaylists);
2167
+ const resumedPlaylist = resumedFromPendingNav || await this.checkAndResumeInProgressPlaylist(watchedPlaylists);
2091
2168
  if (!resumedPlaylist) {
2092
2169
  const shareData = await this.shareLinkService.shouldAutoStartSharePlaylist();
2093
2170
  if (shareData && this.playlistOrchestrator) {
@@ -2159,6 +2236,60 @@ class PlayerInitializationService {
2159
2236
  }
2160
2237
  return false;
2161
2238
  }
2239
+ /**
2240
+ * Check for pending navigation from cross-page URL transitions and auto-start the playlist
2241
+ * This handles the case where user navigated to a new page (hard refresh)
2242
+ * and we need to resume from the step that was waiting for that URL
2243
+ * @returns true if a playlist was started from pending navigation
2244
+ */
2245
+ async checkAndResumeFromPendingNavigation() {
2246
+ const pending = this.managers.storageManager.getPendingNavigation();
2247
+ if (!pending) {
2248
+ return false;
2249
+ }
2250
+ const ageMs = Date.now() - pending.timestamp;
2251
+ if (ageMs > TIMING.PENDING_NAVIGATION_EXPIRY) {
2252
+ this.managers.storageManager.clearPendingNavigation();
2253
+ return false;
2254
+ }
2255
+ if (!this.isURLPathMatch(pending.urlPattern)) {
2256
+ log(`[PlayerInitializationService.checkAndResumeFromPendingNavigation] URL doesn't match pending pattern '${pending.urlPattern}'`);
2257
+ this.managers.storageManager.clearPendingNavigation();
2258
+ return false;
2259
+ }
2260
+ log(`[PlayerInitializationService.checkAndResumeFromPendingNavigation] URL matches! Starting playlist ${pending.playlistId} at step ${pending.nextStepId}`);
2261
+ this.managers.storageManager.clearPendingNavigation();
2262
+ try {
2263
+ if (this.playlistOrchestrator) {
2264
+ await this.playlistOrchestrator.startPlaylist(pending.playlistId, {
2265
+ startNodeId: pending.nextStepId
2266
+ });
2267
+ log(`[PlayerInitializationService.checkAndResumeFromPendingNavigation] Successfully started playlist from pending navigation`);
2268
+ return true;
2269
+ }
2270
+ } catch (error2) {
2271
+ return false;
2272
+ }
2273
+ return false;
2274
+ }
2275
+ /**
2276
+ * Checks if the current URL path matches a pattern
2277
+ * Uses the same logic as TransitionManager for consistency
2278
+ * @param pattern - The URL pattern to match (supports wildcards)
2279
+ * @returns true if the current URL matches the pattern
2280
+ */
2281
+ isURLPathMatch(pattern) {
2282
+ if (!pattern || typeof window === "undefined") {
2283
+ return false;
2284
+ }
2285
+ const currentUrl = window.location.href;
2286
+ const currentPath = window.location.pathname;
2287
+ const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2288
+ const regexPattern = escapedPattern.replace(/\\\*/g, ".*");
2289
+ const regex = new RegExp(regexPattern);
2290
+ const match = regex.test(currentUrl) || regex.test(currentPath);
2291
+ return match;
2292
+ }
2162
2293
  /**
2163
2294
  * Get or create persistent anonymous user ID
2164
2295
  */
@@ -6610,44 +6741,6 @@ class VideoManager {
6610
6741
  this.soundbarElement = null;
6611
6742
  }
6612
6743
  }
6613
- const ANALYTICS = {
6614
- /** Interval for flushing analytics events to the backend (30 seconds) */
6615
- FLUSH_INTERVAL_MS: 3e4
6616
- };
6617
- const TIMING = {
6618
- // Polling and updates
6619
- /** Video progress polling interval (50ms) */
6620
- VIDEO_PROGRESS_POLL_INTERVAL: 50,
6621
- /** Cursor position update throttle interval (100ms) */
6622
- CURSOR_UPDATE_THROTTLE: 100,
6623
- /** Delay for state processing operations (100ms) */
6624
- STATE_PROCESSING_DELAY_MS: 100,
6625
- // Analytics
6626
- /** Analytics event flush interval (30 seconds) */
6627
- ANALYTICS_FLUSH_INTERVAL: 3e4,
6628
- // Timeouts
6629
- /** User data loading timeout (5 seconds) */
6630
- USER_DATA_TIMEOUT: 5e3,
6631
- /** Step timeout - player will be destroyed if user stays on same step (120 seconds) */
6632
- STEP_TIMEOUT: 12e4,
6633
- /** Retry delay for failed operations (0.5 seconds) */
6634
- RETRY_DELAY_MS: 500,
6635
- // DOM and cursor operations
6636
- /** Delay for DOM stabilization before cursor operations (0.5 seconds) */
6637
- DOM_STABILIZATION_DELAY_MS: 500,
6638
- /** Default cursor animation distance in pixels */
6639
- CURSOR_DEFAULT_DISTANCE: 100,
6640
- // URL monitoring
6641
- /** Interval for checking URL path changes (5 seconds) */
6642
- URL_PATH_CHECK_INTERVAL_MS: 5e3,
6643
- // Session persistence
6644
- /** Session expiry time (30 minutes) */
6645
- SESSION_EXPIRY: 30 * 60 * 1e3
6646
- };
6647
- const THRESHOLDS = {
6648
- /** Minimum scroll distance in pixels to trigger scroll events */
6649
- SCROLL_THRESHOLD_PX: 10
6650
- };
6651
6744
  const DEFAULT_CONFIG = {
6652
6745
  tolerance: 0.3,
6653
6746
  // 30% size difference allowed (70% match required)
@@ -9211,6 +9304,8 @@ class TransitionManager {
9211
9304
  __publicField(this, "isStateMachineValidating", false);
9212
9305
  // Reference to TriggerManager for coordinating playlist triggers
9213
9306
  __publicField(this, "triggerManager", null);
9307
+ // beforeunload handler for cross-page URL transitions
9308
+ __publicField(this, "beforeUnloadHandler", null);
9214
9309
  /**
9215
9310
  * Handles URL changes by checking active URL path transitions and playlist triggers
9216
9311
  */
@@ -9247,6 +9342,7 @@ class TransitionManager {
9247
9342
  }
9248
9343
  const { pattern, nextStepId } = transition.data;
9249
9344
  if (this.isURLPathMatch(pattern)) {
9345
+ StorageManager.getInstance().clearPendingNavigation();
9250
9346
  this.triggerTransition(nextStepId);
9251
9347
  break;
9252
9348
  }
@@ -9437,6 +9533,8 @@ class TransitionManager {
9437
9533
  }
9438
9534
  const pathPattern = transition.target;
9439
9535
  const nextStepId = transition.nextStep;
9536
+ this.savePendingNavigation(pathPattern, nextStepId);
9537
+ this.setupBeforeUnloadHandler(pathPattern, nextStepId);
9440
9538
  const initialMatch = this.isURLPathMatch(pathPattern);
9441
9539
  if (initialMatch) {
9442
9540
  this.triggerTransition(nextStepId);
@@ -9463,6 +9561,7 @@ class TransitionManager {
9463
9561
  const match = this.isURLPathMatch(pathPattern);
9464
9562
  if (match) {
9465
9563
  clearInterval(intervalId);
9564
+ StorageManager.getInstance().clearPendingNavigation();
9466
9565
  this.triggerTransition(nextStepId);
9467
9566
  }
9468
9567
  }, TIMING.URL_PATH_CHECK_INTERVAL_MS);
@@ -9479,6 +9578,47 @@ class TransitionManager {
9479
9578
  }
9480
9579
  });
9481
9580
  }
9581
+ /**
9582
+ * Saves pending navigation data for cross-page URL transitions
9583
+ * This enables resuming from the correct step after a hard page refresh
9584
+ * @param urlPattern - The URL pattern to match
9585
+ * @param nextStepId - The step ID to navigate to
9586
+ */
9587
+ savePendingNavigation(urlPattern, nextStepId) {
9588
+ const store = getSaltfishStore();
9589
+ if (!store.manifest) {
9590
+ return;
9591
+ }
9592
+ const storageManager2 = StorageManager.getInstance();
9593
+ storageManager2.setPendingNavigation({
9594
+ playlistId: store.manifest.id,
9595
+ nextStepId,
9596
+ urlPattern,
9597
+ timestamp: Date.now()
9598
+ });
9599
+ }
9600
+ /**
9601
+ * Sets up beforeunload handler as backup for cross-page URL transitions
9602
+ * This ensures pending navigation is saved even if the page unloads unexpectedly
9603
+ * @param urlPattern - The URL pattern to match
9604
+ * @param nextStepId - The step ID to navigate to
9605
+ */
9606
+ setupBeforeUnloadHandler(urlPattern, nextStepId) {
9607
+ this.removeBeforeUnloadHandler();
9608
+ this.beforeUnloadHandler = () => {
9609
+ this.savePendingNavigation(urlPattern, nextStepId);
9610
+ };
9611
+ window.addEventListener("beforeunload", this.beforeUnloadHandler);
9612
+ }
9613
+ /**
9614
+ * Removes the beforeunload handler if it exists
9615
+ */
9616
+ removeBeforeUnloadHandler() {
9617
+ if (this.beforeUnloadHandler) {
9618
+ window.removeEventListener("beforeunload", this.beforeUnloadHandler);
9619
+ this.beforeUnloadHandler = null;
9620
+ }
9621
+ }
9482
9622
  /**
9483
9623
  * Checks if the current URL path matches a pattern
9484
9624
  */
@@ -9588,6 +9728,7 @@ class TransitionManager {
9588
9728
  });
9589
9729
  this.activeTransitions.clear();
9590
9730
  this.waitingForInteraction = false;
9731
+ this.removeBeforeUnloadHandler();
9591
9732
  }
9592
9733
  /**
9593
9734
  * Sets the waiting for interaction state
@@ -10824,6 +10965,11 @@ class PlaylistLoader {
10824
10965
  const manifestIdForProgress = manifest.id;
10825
10966
  const wasTriggered = options._triggeredByTriggerManager === true;
10826
10967
  let startStepId = manifest.startStep;
10968
+ const pendingNav = this.checkPendingNavigation(manifest);
10969
+ if (pendingNav) {
10970
+ log(`PlaylistLoader: Resuming from pending URL navigation to step '${pendingNav.nextStepId}'`);
10971
+ return pendingNav.nextStepId;
10972
+ }
10827
10973
  if (options.startNodeId) {
10828
10974
  const customStep = manifest.steps.find((step) => step.id === options.startNodeId);
10829
10975
  if (customStep) {
@@ -10855,6 +11001,61 @@ class PlaylistLoader {
10855
11001
  }
10856
11002
  return startStepId;
10857
11003
  }
11004
+ /**
11005
+ * Checks for pending navigation from a cross-page URL transition
11006
+ * This handles the case where user navigated to a new page (hard refresh)
11007
+ * and we need to resume from the step that was waiting for that URL
11008
+ * @param manifest - The loaded playlist manifest
11009
+ * @returns The pending navigation data if valid, null otherwise
11010
+ */
11011
+ checkPendingNavigation(manifest) {
11012
+ const storageManager2 = StorageManager.getInstance();
11013
+ const pending = storageManager2.getPendingNavigation();
11014
+ if (!pending) {
11015
+ return null;
11016
+ }
11017
+ if (pending.playlistId !== manifest.id) {
11018
+ log(`PlaylistLoader: Pending navigation is for different playlist (${pending.playlistId}), ignoring`);
11019
+ return null;
11020
+ }
11021
+ const ageMs = Date.now() - pending.timestamp;
11022
+ if (ageMs > TIMING.PENDING_NAVIGATION_EXPIRY) {
11023
+ storageManager2.clearPendingNavigation();
11024
+ return null;
11025
+ }
11026
+ if (!this.isURLPathMatch(pending.urlPattern)) {
11027
+ log(`PlaylistLoader: URL doesn't match pending pattern '${pending.urlPattern}', clearing`);
11028
+ storageManager2.clearPendingNavigation();
11029
+ return null;
11030
+ }
11031
+ const targetStep = manifest.steps.find((step) => step.id === pending.nextStepId);
11032
+ if (!targetStep) {
11033
+ log(`PlaylistLoader: Pending navigation target step '${pending.nextStepId}' not found in manifest, clearing`);
11034
+ storageManager2.clearPendingNavigation();
11035
+ return null;
11036
+ }
11037
+ log(`PlaylistLoader: Using pending navigation to step '${pending.nextStepId}' (${Math.round(ageMs / 1e3)}s old)`);
11038
+ storageManager2.clearPendingNavigation();
11039
+ return pending;
11040
+ }
11041
+ /**
11042
+ * Checks if the current URL path matches a pattern
11043
+ * Uses the same logic as TransitionManager for consistency
11044
+ * @param pattern - The URL pattern to match (supports wildcards)
11045
+ * @returns true if the current URL matches the pattern
11046
+ */
11047
+ isURLPathMatch(pattern) {
11048
+ if (!pattern) {
11049
+ return false;
11050
+ }
11051
+ const currentUrl = window.location.href;
11052
+ const currentPath = window.location.pathname;
11053
+ const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11054
+ const regexPattern = escapedPattern.replace(/\\\*/g, ".*");
11055
+ const regex = new RegExp(regexPattern);
11056
+ const match = regex.test(currentUrl) || regex.test(currentPath);
11057
+ return match;
11058
+ }
10858
11059
  }
10859
11060
  class PlaylistManager extends EventSubscriberManager {
10860
11061
  /**
@@ -12268,7 +12469,7 @@ const SaltfishPlayer$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de
12268
12469
  __proto__: null,
12269
12470
  SaltfishPlayer
12270
12471
  }, Symbol.toStringTag, { value: "Module" }));
12271
- const version = "0.3.61";
12472
+ const version = "0.3.63";
12272
12473
  const packageJson = {
12273
12474
  version
12274
12475
  };