saltfish 0.3.60 → 0.3.62

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,15 +1156,30 @@ const _StorageManager = class _StorageManager {
1155
1156
  this.safeClearItem(STORAGE_KEYS.ANONYMOUS_USER);
1156
1157
  }
1157
1158
  // =============================================================================
1158
- // Anonymous User ID Methods
1159
+ // Pending Navigation Methods (Cross-page URL transitions)
1159
1160
  // =============================================================================
1160
1161
  /**
1161
- * Get anonymous user ID from the anonymous user data object
1162
- * This reads from the same key as getAnonymousUserData() for consistency
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
1163
1164
  */
1164
- getAnonymousUserId() {
1165
- const data = this.getAnonymousUserData();
1166
- return (data == null ? void 0 : data.userId) || null;
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);
1167
1183
  }
1168
1184
  // =============================================================================
1169
1185
  // Utility Methods
@@ -1175,33 +1191,7 @@ const _StorageManager = class _StorageManager {
1175
1191
  this.clearProgress();
1176
1192
  this.clearSession();
1177
1193
  this.clearAnonymousUserData();
1178
- }
1179
- /**
1180
- * Get storage availability status
1181
- */
1182
- isStorageAvailable() {
1183
- return this.isLocalStorageAvailable;
1184
- }
1185
- /**
1186
- * Get storage usage information (if available)
1187
- */
1188
- getStorageInfo() {
1189
- const keys = [];
1190
- if (this.isLocalStorageAvailable) {
1191
- try {
1192
- for (let i = 0; i < localStorage.length; i++) {
1193
- const key = localStorage.key(i);
1194
- if (key && key.startsWith("saltfish_")) {
1195
- keys.push(key);
1196
- }
1197
- }
1198
- } catch (error2) {
1199
- }
1200
- }
1201
- return {
1202
- available: this.isLocalStorageAvailable,
1203
- keys
1204
- };
1194
+ this.clearPendingNavigation();
1205
1195
  }
1206
1196
  };
1207
1197
  __publicField(_StorageManager, "instance", null);
@@ -1874,6 +1864,47 @@ class ShareLinkService {
1874
1864
  }
1875
1865
  }
1876
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
+ };
1877
1908
  class PlayerInitializationService {
1878
1909
  constructor(managers) {
1879
1910
  __publicField(this, "managers");
@@ -2038,7 +2069,11 @@ class PlayerInitializationService {
2038
2069
  if (this.userManagementService) {
2039
2070
  this.userManagementService.resolveUserDataLoaded();
2040
2071
  }
2041
- 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 || {});
2042
2077
  if (!resumedPlaylist) {
2043
2078
  const shareData = await this.shareLinkService.shouldAutoStartSharePlaylist();
2044
2079
  if (shareData && this.playlistOrchestrator) {
@@ -2124,8 +2159,12 @@ class PlayerInitializationService {
2124
2159
  watchedPlaylists: anonymousUserData.watchedPlaylists || {}
2125
2160
  }
2126
2161
  });
2162
+ const resumedFromPendingNav = await this.checkAndResumeFromPendingNavigation();
2163
+ if (resumedFromPendingNav) {
2164
+ log("[PlayerInitializationService.loadAnonymousUserData] Resumed from pending URL navigation, skipping other checks");
2165
+ }
2127
2166
  const watchedPlaylists = anonymousUserData.watchedPlaylists || {};
2128
- const resumedPlaylist = await this.checkAndResumeInProgressPlaylist(watchedPlaylists);
2167
+ const resumedPlaylist = resumedFromPendingNav || await this.checkAndResumeInProgressPlaylist(watchedPlaylists);
2129
2168
  if (!resumedPlaylist) {
2130
2169
  const shareData = await this.shareLinkService.shouldAutoStartSharePlaylist();
2131
2170
  if (shareData && this.playlistOrchestrator) {
@@ -2197,6 +2236,60 @@ class PlayerInitializationService {
2197
2236
  }
2198
2237
  return false;
2199
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
+ }
2200
2293
  /**
2201
2294
  * Get or create persistent anonymous user ID
2202
2295
  */
@@ -4283,7 +4376,7 @@ class DeviceDetector {
4283
4376
  return this.cachedDeviceInfo;
4284
4377
  }
4285
4378
  const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
4286
- const isTouchDevice2 = this.detectTouchSupport();
4379
+ const isTouchDevice = this.detectTouchSupport();
4287
4380
  const { width, height } = this.getScreenDimensions();
4288
4381
  const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i;
4289
4382
  const tabletRegex = /iPad|Android(?!.*Mobile)|Tablet|tablet/i;
@@ -4292,14 +4385,14 @@ class DeviceDetector {
4292
4385
  const screenSize = this.getScreenSize(width, height);
4293
4386
  const isMobileByScreen = screenSize === "small" && Math.min(width, height) < 768;
4294
4387
  const isTabletByScreen = screenSize === "medium" && !isMobileByScreen;
4295
- const isMobile2 = isMobileUserAgent || isMobileByScreen && isTouchDevice2;
4296
- const isTablet2 = isTabletUserAgent || isTabletByScreen && isTouchDevice2 && !isMobile2;
4297
- const isDesktop2 = !isMobile2 && !isTablet2;
4388
+ const isMobile = isMobileUserAgent || isMobileByScreen && isTouchDevice;
4389
+ const isTablet = isTabletUserAgent || isTabletByScreen && isTouchDevice && !isMobile;
4390
+ const isDesktop = !isMobile && !isTablet;
4298
4391
  const deviceInfo = {
4299
- isMobile: isMobile2,
4300
- isTablet: isTablet2,
4301
- isDesktop: isDesktop2,
4302
- isTouchDevice: isTouchDevice2,
4392
+ isMobile,
4393
+ isTablet,
4394
+ isDesktop,
4395
+ isTouchDevice,
4303
4396
  screenSize,
4304
4397
  orientation: this.detectOrientation(),
4305
4398
  userAgent
@@ -6648,44 +6741,6 @@ class VideoManager {
6648
6741
  this.soundbarElement = null;
6649
6742
  }
6650
6743
  }
6651
- const ANALYTICS = {
6652
- /** Interval for flushing analytics events to the backend (30 seconds) */
6653
- FLUSH_INTERVAL_MS: 3e4
6654
- };
6655
- const TIMING = {
6656
- // Polling and updates
6657
- /** Video progress polling interval (50ms) */
6658
- VIDEO_PROGRESS_POLL_INTERVAL: 50,
6659
- /** Cursor position update throttle interval (100ms) */
6660
- CURSOR_UPDATE_THROTTLE: 100,
6661
- /** Delay for state processing operations (100ms) */
6662
- STATE_PROCESSING_DELAY_MS: 100,
6663
- // Analytics
6664
- /** Analytics event flush interval (30 seconds) */
6665
- ANALYTICS_FLUSH_INTERVAL: 3e4,
6666
- // Timeouts
6667
- /** User data loading timeout (5 seconds) */
6668
- USER_DATA_TIMEOUT: 5e3,
6669
- /** Step timeout - player will be destroyed if user stays on same step (120 seconds) */
6670
- STEP_TIMEOUT: 12e4,
6671
- /** Retry delay for failed operations (0.5 seconds) */
6672
- RETRY_DELAY_MS: 500,
6673
- // DOM and cursor operations
6674
- /** Delay for DOM stabilization before cursor operations (0.5 seconds) */
6675
- DOM_STABILIZATION_DELAY_MS: 500,
6676
- /** Default cursor animation distance in pixels */
6677
- CURSOR_DEFAULT_DISTANCE: 100,
6678
- // URL monitoring
6679
- /** Interval for checking URL path changes (5 seconds) */
6680
- URL_PATH_CHECK_INTERVAL_MS: 5e3,
6681
- // Session persistence
6682
- /** Session expiry time (30 minutes) */
6683
- SESSION_EXPIRY: 30 * 60 * 1e3
6684
- };
6685
- const THRESHOLDS = {
6686
- /** Minimum scroll distance in pixels to trigger scroll events */
6687
- SCROLL_THRESHOLD_PX: 10
6688
- };
6689
6744
  const DEFAULT_CONFIG = {
6690
6745
  tolerance: 0.3,
6691
6746
  // 30% size difference allowed (70% match required)
@@ -9249,6 +9304,8 @@ class TransitionManager {
9249
9304
  __publicField(this, "isStateMachineValidating", false);
9250
9305
  // Reference to TriggerManager for coordinating playlist triggers
9251
9306
  __publicField(this, "triggerManager", null);
9307
+ // beforeunload handler for cross-page URL transitions
9308
+ __publicField(this, "beforeUnloadHandler", null);
9252
9309
  /**
9253
9310
  * Handles URL changes by checking active URL path transitions and playlist triggers
9254
9311
  */
@@ -9285,6 +9342,7 @@ class TransitionManager {
9285
9342
  }
9286
9343
  const { pattern, nextStepId } = transition.data;
9287
9344
  if (this.isURLPathMatch(pattern)) {
9345
+ StorageManager.getInstance().clearPendingNavigation();
9288
9346
  this.triggerTransition(nextStepId);
9289
9347
  break;
9290
9348
  }
@@ -9475,6 +9533,8 @@ class TransitionManager {
9475
9533
  }
9476
9534
  const pathPattern = transition.target;
9477
9535
  const nextStepId = transition.nextStep;
9536
+ this.savePendingNavigation(pathPattern, nextStepId);
9537
+ this.setupBeforeUnloadHandler(pathPattern, nextStepId);
9478
9538
  const initialMatch = this.isURLPathMatch(pathPattern);
9479
9539
  if (initialMatch) {
9480
9540
  this.triggerTransition(nextStepId);
@@ -9501,6 +9561,7 @@ class TransitionManager {
9501
9561
  const match = this.isURLPathMatch(pathPattern);
9502
9562
  if (match) {
9503
9563
  clearInterval(intervalId);
9564
+ StorageManager.getInstance().clearPendingNavigation();
9504
9565
  this.triggerTransition(nextStepId);
9505
9566
  }
9506
9567
  }, TIMING.URL_PATH_CHECK_INTERVAL_MS);
@@ -9517,6 +9578,47 @@ class TransitionManager {
9517
9578
  }
9518
9579
  });
9519
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
+ }
9520
9622
  /**
9521
9623
  * Checks if the current URL path matches a pattern
9522
9624
  */
@@ -9626,6 +9728,7 @@ class TransitionManager {
9626
9728
  });
9627
9729
  this.activeTransitions.clear();
9628
9730
  this.waitingForInteraction = false;
9731
+ this.removeBeforeUnloadHandler();
9629
9732
  }
9630
9733
  /**
9631
9734
  * Sets the waiting for interaction state
@@ -10862,6 +10965,11 @@ class PlaylistLoader {
10862
10965
  const manifestIdForProgress = manifest.id;
10863
10966
  const wasTriggered = options._triggeredByTriggerManager === true;
10864
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
+ }
10865
10973
  if (options.startNodeId) {
10866
10974
  const customStep = manifest.steps.find((step) => step.id === options.startNodeId);
10867
10975
  if (customStep) {
@@ -10893,6 +11001,61 @@ class PlaylistLoader {
10893
11001
  }
10894
11002
  return startStepId;
10895
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
+ }
10896
11059
  }
10897
11060
  class PlaylistManager extends EventSubscriberManager {
10898
11061
  /**
@@ -12306,7 +12469,7 @@ const SaltfishPlayer$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de
12306
12469
  __proto__: null,
12307
12470
  SaltfishPlayer
12308
12471
  }, Symbol.toStringTag, { value: "Module" }));
12309
- const version = "0.3.60";
12472
+ const version = "0.3.62";
12310
12473
  const packageJson = {
12311
12474
  version
12312
12475
  };