saltfish 0.2.52 → 0.2.54

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.
@@ -951,6 +951,280 @@ const playerStateMachineConfig = {
951
951
  }
952
952
  }
953
953
  };
954
+ const STORAGE_KEYS = {
955
+ PROGRESS: "saltfish_progress",
956
+ SESSION: "saltfish_session",
957
+ ANONYMOUS_USER: "saltfish_anonymous_user_data"
958
+ };
959
+ const TIMING = {
960
+ // 5 seconds
961
+ STEP_TIMEOUT: 6e4,
962
+ // Session persistence
963
+ SESSION_EXPIRY: 30 * 60 * 1e3
964
+ // 30 minutes in milliseconds
965
+ };
966
+ const API = {
967
+ BASE_URL: "https://player.saltfish.ai",
968
+ ENDPOINTS: {
969
+ VALIDATE_TOKEN: "/validate-token",
970
+ USERS: "/clients/{token}/users/{userId}"
971
+ }
972
+ };
973
+ const CSS_CLASSES = {
974
+ PLAYER: "sf-player",
975
+ PLAYER_MINIMIZED: "sf-player--minimized",
976
+ CONTROLS_CONTAINER: "sf-controls-container",
977
+ LOGO: "sf-player__logo",
978
+ // Legacy error class
979
+ ERROR_DISPLAY: "sf-error-display",
980
+ ERROR_DISPLAY_VISIBLE: "sf-error-display--visible",
981
+ ERROR_DISPLAY_CONTENT: "sf-error-display__content",
982
+ ERROR_DISPLAY_MESSAGE: "sf-error-display__message",
983
+ LOADING_SPINNER: "sf-loading-spinner"
984
+ };
985
+ class StorageManager {
986
+ constructor() {
987
+ __publicField(this, "isLocalStorageAvailable");
988
+ this.isLocalStorageAvailable = this.checkLocalStorageAvailability();
989
+ if (!this.isLocalStorageAvailable) ;
990
+ }
991
+ /**
992
+ * Check if localStorage is available and working
993
+ */
994
+ checkLocalStorageAvailability() {
995
+ if (typeof window === "undefined") {
996
+ return false;
997
+ }
998
+ try {
999
+ const testKey = "__storage_test__";
1000
+ localStorage.setItem(testKey, "test");
1001
+ localStorage.removeItem(testKey);
1002
+ return true;
1003
+ } catch (error2) {
1004
+ return false;
1005
+ }
1006
+ }
1007
+ /**
1008
+ * Safely get an item from localStorage with JSON parsing
1009
+ */
1010
+ safeGetItem(key) {
1011
+ if (!this.isLocalStorageAvailable) {
1012
+ return null;
1013
+ }
1014
+ try {
1015
+ const item = localStorage.getItem(key);
1016
+ if (item === null) {
1017
+ return null;
1018
+ }
1019
+ return JSON.parse(item);
1020
+ } catch (error2) {
1021
+ this.safeClearItem(key);
1022
+ return null;
1023
+ }
1024
+ }
1025
+ /**
1026
+ * Safely set an item in localStorage with JSON stringification
1027
+ */
1028
+ safeSetItem(key, value) {
1029
+ if (!this.isLocalStorageAvailable) {
1030
+ return false;
1031
+ }
1032
+ try {
1033
+ localStorage.setItem(key, JSON.stringify(value));
1034
+ return true;
1035
+ } catch (error2) {
1036
+ if (error2 instanceof DOMException && error2.code === 22) {
1037
+ this.clearOldData();
1038
+ try {
1039
+ localStorage.setItem(key, JSON.stringify(value));
1040
+ return true;
1041
+ } catch (retryError) {
1042
+ }
1043
+ }
1044
+ return false;
1045
+ }
1046
+ }
1047
+ /**
1048
+ * Safely clear an item from localStorage
1049
+ */
1050
+ safeClearItem(key) {
1051
+ if (!this.isLocalStorageAvailable) {
1052
+ return;
1053
+ }
1054
+ try {
1055
+ localStorage.removeItem(key);
1056
+ } catch (error2) {
1057
+ }
1058
+ }
1059
+ /**
1060
+ * Clear old data to free up storage space
1061
+ */
1062
+ clearOldData() {
1063
+ this.safeClearItem(STORAGE_KEYS.SESSION);
1064
+ }
1065
+ // =============================================================================
1066
+ // Progress Data Methods
1067
+ // =============================================================================
1068
+ /**
1069
+ * Get playlist progress data for a specific user
1070
+ * If userId doesn't match stored userId, returns null (stale data)
1071
+ * @param userId - The user ID to validate against stored data
1072
+ */
1073
+ getProgress(userId) {
1074
+ const stored = this.safeGetItem(STORAGE_KEYS.PROGRESS);
1075
+ if (!stored) {
1076
+ return null;
1077
+ }
1078
+ if (userId && stored.userId && stored.userId !== userId) {
1079
+ log(`[StorageManager] Progress belongs to different user (${stored.userId}), ignoring`);
1080
+ return null;
1081
+ }
1082
+ return stored.playlists || null;
1083
+ }
1084
+ /**
1085
+ * Set playlist progress data for a specific user
1086
+ * Clears existing data if userId changes
1087
+ * @param progress - The progress data to save
1088
+ * @param userId - The user ID to associate with this progress
1089
+ */
1090
+ setProgress(progress, userId) {
1091
+ const stored = {
1092
+ userId,
1093
+ playlists: progress
1094
+ };
1095
+ return this.safeSetItem(STORAGE_KEYS.PROGRESS, stored);
1096
+ }
1097
+ /**
1098
+ * Clear all progress data
1099
+ */
1100
+ clearProgress() {
1101
+ this.safeClearItem(STORAGE_KEYS.PROGRESS);
1102
+ }
1103
+ // =============================================================================
1104
+ // Session Data Methods
1105
+ // =============================================================================
1106
+ /**
1107
+ * Get session data
1108
+ */
1109
+ getSession() {
1110
+ return this.safeGetItem(STORAGE_KEYS.SESSION);
1111
+ }
1112
+ /**
1113
+ * Set session data
1114
+ */
1115
+ setSession(session) {
1116
+ return this.safeSetItem(STORAGE_KEYS.SESSION, session);
1117
+ }
1118
+ /**
1119
+ * Clear session data
1120
+ */
1121
+ clearSession() {
1122
+ this.safeClearItem(STORAGE_KEYS.SESSION);
1123
+ }
1124
+ // =============================================================================
1125
+ // Anonymous User Data Methods
1126
+ // =============================================================================
1127
+ /**
1128
+ * Get anonymous user data
1129
+ */
1130
+ getAnonymousUserData() {
1131
+ return this.safeGetItem(STORAGE_KEYS.ANONYMOUS_USER);
1132
+ }
1133
+ /**
1134
+ * Set anonymous user data
1135
+ */
1136
+ setAnonymousUserData(data) {
1137
+ return this.safeSetItem(STORAGE_KEYS.ANONYMOUS_USER, data);
1138
+ }
1139
+ /**
1140
+ * Clear anonymous user data
1141
+ */
1142
+ clearAnonymousUserData() {
1143
+ this.safeClearItem(STORAGE_KEYS.ANONYMOUS_USER);
1144
+ }
1145
+ // =============================================================================
1146
+ // Anonymous User ID Methods
1147
+ // =============================================================================
1148
+ /**
1149
+ * Get anonymous user ID
1150
+ */
1151
+ getAnonymousUserId() {
1152
+ if (!this.isLocalStorageAvailable) {
1153
+ return null;
1154
+ }
1155
+ try {
1156
+ return localStorage.getItem("saltfish_anonymous_user_id");
1157
+ } catch (error2) {
1158
+ return null;
1159
+ }
1160
+ }
1161
+ /**
1162
+ * Set anonymous user ID
1163
+ */
1164
+ setAnonymousUserId(userId) {
1165
+ if (!this.isLocalStorageAvailable) {
1166
+ return false;
1167
+ }
1168
+ try {
1169
+ localStorage.setItem("saltfish_anonymous_user_id", userId);
1170
+ return true;
1171
+ } catch (error2) {
1172
+ return false;
1173
+ }
1174
+ }
1175
+ /**
1176
+ * Clear anonymous user ID
1177
+ */
1178
+ clearAnonymousUserId() {
1179
+ if (!this.isLocalStorageAvailable) {
1180
+ return;
1181
+ }
1182
+ try {
1183
+ localStorage.removeItem("saltfish_anonymous_user_id");
1184
+ } catch (error2) {
1185
+ }
1186
+ }
1187
+ // =============================================================================
1188
+ // Utility Methods
1189
+ // =============================================================================
1190
+ /**
1191
+ * Clear all storage data
1192
+ */
1193
+ clearAll() {
1194
+ this.clearProgress();
1195
+ this.clearSession();
1196
+ this.clearAnonymousUserData();
1197
+ this.clearAnonymousUserId();
1198
+ }
1199
+ /**
1200
+ * Get storage availability status
1201
+ */
1202
+ isStorageAvailable() {
1203
+ return this.isLocalStorageAvailable;
1204
+ }
1205
+ /**
1206
+ * Get storage usage information (if available)
1207
+ */
1208
+ getStorageInfo() {
1209
+ const keys = [];
1210
+ if (this.isLocalStorageAvailable) {
1211
+ try {
1212
+ for (let i = 0; i < localStorage.length; i++) {
1213
+ const key = localStorage.key(i);
1214
+ if (key && key.startsWith("saltfish_")) {
1215
+ keys.push(key);
1216
+ }
1217
+ }
1218
+ } catch (error2) {
1219
+ }
1220
+ }
1221
+ return {
1222
+ available: this.isLocalStorageAvailable,
1223
+ keys
1224
+ };
1225
+ }
1226
+ }
1227
+ const storageManager = new StorageManager();
954
1228
  const createInitialContext = () => ({
955
1229
  currentStep: null,
956
1230
  error: null
@@ -1071,7 +1345,7 @@ const saltfishStore = createStore()(
1071
1345
  if (manifest && manifest.steps.some((step) => step.id === stepId)) {
1072
1346
  const targetStep = manifest.steps.find((step) => step.id === stepId);
1073
1347
  set2((state) => {
1074
- var _a;
1348
+ var _a, _b, _c;
1075
1349
  state.currentStepId = stepId;
1076
1350
  if (targetStep) {
1077
1351
  state.currentState = transitionState({
@@ -1087,6 +1361,11 @@ const saltfishStore = createStore()(
1087
1361
  lastStepId: stepId,
1088
1362
  lastVisited: (/* @__PURE__ */ new Date()).toISOString()
1089
1363
  };
1364
+ const playlistPersistence = ((_b = state.playlistOptions) == null ? void 0 : _b.persistence) ?? true;
1365
+ if (playlistPersistence) {
1366
+ const userId = (_c = state.user) == null ? void 0 : _c.id;
1367
+ storageManager.setProgress(state.progress, userId);
1368
+ }
1090
1369
  });
1091
1370
  }
1092
1371
  },
@@ -1185,6 +1464,7 @@ const saltfishStore = createStore()(
1185
1464
  // New action to update progress when transitioning to completion waiting state
1186
1465
  updateProgressWithCompletion: (playlistId, currentStepId) => {
1187
1466
  set2((state) => {
1467
+ var _a, _b;
1188
1468
  state.currentState = transitionState({ type: "COMPLETE_PLAYLIST_WAITING_FOR_INTERACTION" });
1189
1469
  state.progress[playlistId] = {
1190
1470
  ...state.progress[playlistId],
@@ -1192,6 +1472,11 @@ const saltfishStore = createStore()(
1192
1472
  lastVisited: (/* @__PURE__ */ new Date()).toISOString(),
1193
1473
  completedWaitingForInteraction: true
1194
1474
  };
1475
+ const playlistPersistence = ((_a = state.playlistOptions) == null ? void 0 : _a.persistence) ?? true;
1476
+ if (playlistPersistence) {
1477
+ const userId = (_b = state.user) == null ? void 0 : _b.id;
1478
+ storageManager.setProgress(state.progress, userId);
1479
+ }
1195
1480
  });
1196
1481
  },
1197
1482
  setABTests: (abTests) => {
@@ -1232,37 +1517,6 @@ const useSaltfishStore = {
1232
1517
  subscribe: saltfishStore.subscribe,
1233
1518
  destroy: saltfishStore.destroy
1234
1519
  };
1235
- const STORAGE_KEYS = {
1236
- PROGRESS: "saltfish_progress",
1237
- SESSION: "saltfish_session",
1238
- ANONYMOUS_USER: "saltfish_anonymous_user_data"
1239
- };
1240
- const TIMING = {
1241
- // 5 seconds
1242
- STEP_TIMEOUT: 6e4,
1243
- // Session persistence
1244
- SESSION_EXPIRY: 30 * 60 * 1e3
1245
- // 30 minutes in milliseconds
1246
- };
1247
- const API = {
1248
- BASE_URL: "https://player.saltfish.ai",
1249
- ENDPOINTS: {
1250
- VALIDATE_TOKEN: "/validate-token",
1251
- USERS: "/clients/{token}/users/{userId}"
1252
- }
1253
- };
1254
- const CSS_CLASSES = {
1255
- PLAYER: "sf-player",
1256
- PLAYER_MINIMIZED: "sf-player--minimized",
1257
- CONTROLS_CONTAINER: "sf-controls-container",
1258
- LOGO: "sf-player__logo",
1259
- // Legacy error class
1260
- ERROR_DISPLAY: "sf-error-display",
1261
- ERROR_DISPLAY_VISIBLE: "sf-error-display--visible",
1262
- ERROR_DISPLAY_CONTENT: "sf-error-display__content",
1263
- ERROR_DISPLAY_MESSAGE: "sf-error-display__message",
1264
- LOADING_SPINNER: "sf-loading-spinner"
1265
- };
1266
1520
  class ErrorHandler {
1267
1521
  /**
1268
1522
  * Handles an error with consistent logging, reporting, and response
@@ -2270,7 +2524,7 @@ class PlaylistOrchestrator {
2270
2524
  * Start a playlist with given options
2271
2525
  */
2272
2526
  async startPlaylist(playlistId, options) {
2273
- var _a, _b, _c;
2527
+ var _a, _b, _c, _d;
2274
2528
  try {
2275
2529
  const needsManagerRecreation = this.isInitialized() && !this.managers.uiManager.getPlayerElement();
2276
2530
  if (!this.isInitialized() && this.playerInitializationService) {
@@ -2392,11 +2646,35 @@ class PlaylistOrchestrator {
2392
2646
  }
2393
2647
  this.managers.uiManager.updatePosition();
2394
2648
  store.sendStateMachineEvent({ type: "LOAD_MANIFEST" });
2649
+ const playlistPersistence = options == null ? void 0 : options.persistence;
2650
+ if (typeof window !== "undefined") {
2651
+ const userId = (_d = store.user) == null ? void 0 : _d.id;
2652
+ const progressFromStorage = this.managers.storageManager.getProgress(userId);
2653
+ if (progressFromStorage && progressFromStorage[playlistId]) {
2654
+ store.loadPlaylistProgress(playlistId, progressFromStorage[playlistId]);
2655
+ } else if (store.progress[playlistId]) {
2656
+ log(`PlaylistOrchestrator: Clearing stale progress for ${playlistId} from store.progress`);
2657
+ const cleanedProgress = { ...store.progress };
2658
+ delete cleanedProgress[playlistId];
2659
+ useSaltfishStore.setState((state) => {
2660
+ state.progress = cleanedProgress;
2661
+ });
2662
+ }
2663
+ }
2395
2664
  this.managers.analyticsManager.trackPlaylistStart(playlistId);
2396
2665
  this.managers.cursorManager.resetFirstAnimation();
2397
2666
  log(`[PlaylistOrchestrator.startPlaylist] Using validated manifest path: ${manifestPathToLoad}`);
2398
- await this.managers.playlistManager.load(manifestPathToLoad, finalOptions);
2667
+ await this.managers.playlistManager.load(manifestPathToLoad, { ...finalOptions, persistence: playlistPersistence });
2399
2668
  const updatedStore = useSaltfishStore.getState();
2669
+ if (updatedStore.manifest) {
2670
+ const manifestPersistence = finalOptions.persistence ?? updatedStore.manifest.isPersistent ?? true;
2671
+ const currentOptions = updatedStore.playlistOptions || {};
2672
+ updatedStore.setPlaylistOptions({
2673
+ ...currentOptions,
2674
+ ...finalOptions,
2675
+ persistence: manifestPersistence
2676
+ });
2677
+ }
2400
2678
  if (finalOptions.startNodeId && updatedStore.manifest) {
2401
2679
  const targetStep = updatedStore.manifest.steps.find((step) => step.id === finalOptions.startNodeId);
2402
2680
  if (targetStep) {
@@ -2819,6 +3097,7 @@ class StateMachineActionHandler {
2819
3097
  }
2820
3098
  }
2821
3099
  handleTrackPlaylistComplete() {
3100
+ var _a, _b;
2822
3101
  const store = useSaltfishStore.getState();
2823
3102
  if (store.manifest && this.managers.eventManager) {
2824
3103
  const playlistId = store.manifest.id;
@@ -2829,6 +3108,13 @@ class StateMachineActionHandler {
2829
3108
  title: store.manifest.name
2830
3109
  }
2831
3110
  });
3111
+ const playlistPersistence = ((_a = store.playlistOptions) == null ? void 0 : _a.persistence) ?? true;
3112
+ if (playlistPersistence && store.progress[playlistId]) {
3113
+ const updatedProgress = { ...store.progress };
3114
+ delete updatedProgress[playlistId];
3115
+ const userId = (_b = store.user) == null ? void 0 : _b.id;
3116
+ this.managers.storageManager.setProgress(updatedProgress, userId);
3117
+ }
2832
3118
  }
2833
3119
  }
2834
3120
  handleError(context) {
@@ -4761,6 +5047,7 @@ class VideoManager {
4761
5047
  * @param url - URL of the video to load
4762
5048
  */
4763
5049
  async loadVideo(url) {
5050
+ var _a;
4764
5051
  const activeVideo = this.getActiveVideo();
4765
5052
  if (!activeVideo) {
4766
5053
  return;
@@ -4777,9 +5064,20 @@ class VideoManager {
4777
5064
  if (this.controls) {
4778
5065
  this.controls.reset();
4779
5066
  }
5067
+ const store2 = useSaltfishStore.getState();
5068
+ const isPersistenceEnabled = ((_a = store2.playlistOptions) == null ? void 0 : _a.persistence) ?? true;
4780
5069
  if (this.currentVideoUrl === url && activeVideo.src && (activeVideo.src === url || activeVideo.src.endsWith(url))) {
5070
+ if (isPersistenceEnabled) {
5071
+ const savedPosition = this.playbackPositions.get(url);
5072
+ if (savedPosition !== void 0 && savedPosition > 0 && Math.abs(activeVideo.currentTime - savedPosition) > 0.5) {
5073
+ activeVideo.currentTime = savedPosition;
5074
+ }
5075
+ }
4781
5076
  return;
4782
5077
  }
5078
+ if (isPersistenceEnabled && this.currentVideoUrl && activeVideo.currentTime > 0) {
5079
+ this.playbackPositions.set(this.currentVideoUrl, activeVideo.currentTime);
5080
+ }
4783
5081
  const inactiveVideo = this.getInactiveVideo();
4784
5082
  if (inactiveVideo && this.nextVideoUrl === url) {
4785
5083
  this.swapVideos();
@@ -4826,8 +5124,16 @@ class VideoManager {
4826
5124
  if (!activeVideo) {
4827
5125
  return;
4828
5126
  }
4829
- clearTimeout(loadTimeout);
4830
- activeVideo.currentTime = 0;
5127
+ clearTimeout(loadTimeout);
5128
+ if (isPersistenceEnabled) {
5129
+ const savedPosition = this.playbackPositions.get(url);
5130
+ if (savedPosition !== void 0 && savedPosition > 0) {
5131
+ const safePosition = Math.min(savedPosition, activeVideo.duration - 0.5);
5132
+ activeVideo.currentTime = safePosition;
5133
+ }
5134
+ } else {
5135
+ activeVideo.currentTime = 0;
5136
+ }
4831
5137
  this.transcriptManager.updateVideoElement(activeVideo);
4832
5138
  if (this.controls) {
4833
5139
  this.controls.updateVideoElement(activeVideo);
@@ -4916,6 +5222,7 @@ class VideoManager {
4916
5222
  * Plays the video
4917
5223
  */
4918
5224
  play() {
5225
+ var _a;
4919
5226
  const activeVideo = this.getActiveVideo();
4920
5227
  if (!activeVideo) {
4921
5228
  console.error("VideoManager: No active video element found");
@@ -4928,6 +5235,14 @@ class VideoManager {
4928
5235
  if (activeVideo.ended) {
4929
5236
  activeVideo.currentTime = 0;
4930
5237
  }
5238
+ const store = useSaltfishStore.getState();
5239
+ const isPersistenceEnabled = ((_a = store.playlistOptions) == null ? void 0 : _a.persistence) ?? true;
5240
+ if (isPersistenceEnabled && this.currentVideoUrl) {
5241
+ const savedPosition = this.playbackPositions.get(this.currentVideoUrl);
5242
+ if (savedPosition && Math.abs(activeVideo.currentTime - savedPosition) > 0.5) {
5243
+ activeVideo.currentTime = savedPosition;
5244
+ }
5245
+ }
4931
5246
  activeVideo.loop = false;
4932
5247
  if (!activeVideo.paused) {
4933
5248
  if (this.controls) {
@@ -4944,7 +5259,6 @@ class VideoManager {
4944
5259
  this.controls.startProgressTracking();
4945
5260
  }
4946
5261
  } else {
4947
- const store = useSaltfishStore.getState();
4948
5262
  store.setAutoplayFallback();
4949
5263
  if (this.isMobileDevice()) {
4950
5264
  activeVideo.playsInline = true;
@@ -4960,8 +5274,8 @@ class VideoManager {
4960
5274
  }
4961
5275
  }).catch(() => {
4962
5276
  console.warn("VideoManager: Autoplay handler threw error - browser has strict autoplay policy");
4963
- const store = useSaltfishStore.getState();
4964
- store.setAutoplayFallback();
5277
+ const store2 = useSaltfishStore.getState();
5278
+ store2.setAutoplayFallback();
4965
5279
  });
4966
5280
  }
4967
5281
  /**
@@ -6439,6 +6753,7 @@ class InteractionManager {
6439
6753
  __publicField(this, "scrollIndicator", null);
6440
6754
  __publicField(this, "domEventListeners", /* @__PURE__ */ new Map());
6441
6755
  __publicField(this, "storeUnsubscribe", null);
6756
+ __publicField(this, "storageManager", new StorageManager());
6442
6757
  /**
6443
6758
  * Handles the video90PercentReached event
6444
6759
  * @param _event - The custom event dispatched when video reaches 90%
@@ -6658,6 +6973,7 @@ class InteractionManager {
6658
6973
  * @param buttonConfig - The button configuration
6659
6974
  */
6660
6975
  async handleButtonClick(event, buttonConfig) {
6976
+ var _a, _b;
6661
6977
  log(`InteractionManager: Button click detected on button "${buttonConfig.id}"`);
6662
6978
  event.preventDefault();
6663
6979
  event.stopPropagation();
@@ -6682,6 +6998,13 @@ class InteractionManager {
6682
6998
  };
6683
6999
  }
6684
7000
  });
7001
+ const playlistPersistence = ((_a = currentStore.playlistOptions) == null ? void 0 : _a.persistence) ?? true;
7002
+ if (playlistPersistence) {
7003
+ const userId = (_b = currentStore.user) == null ? void 0 : _b.id;
7004
+ const updatedStore = useSaltfishStore.getState();
7005
+ this.storageManager.setProgress(updatedStore.progress, userId);
7006
+ log(`InteractionManager: Saved progress for step ${buttonConfig.action.target} before URL redirect`);
7007
+ }
6685
7008
  }
6686
7009
  log(`InteractionManager: Redirecting to ${buttonConfig.action.url}`);
6687
7010
  window.location.href = buttonConfig.action.url;
@@ -7199,254 +7522,12 @@ class AnalyticsManager {
7199
7522
  }
7200
7523
  }
7201
7524
  }
7202
- class StorageManager {
7203
- constructor() {
7204
- __publicField(this, "isLocalStorageAvailable");
7205
- this.isLocalStorageAvailable = this.checkLocalStorageAvailability();
7206
- if (!this.isLocalStorageAvailable) ;
7207
- }
7208
- /**
7209
- * Check if localStorage is available and working
7210
- */
7211
- checkLocalStorageAvailability() {
7212
- if (typeof window === "undefined") {
7213
- return false;
7214
- }
7215
- try {
7216
- const testKey = "__storage_test__";
7217
- localStorage.setItem(testKey, "test");
7218
- localStorage.removeItem(testKey);
7219
- return true;
7220
- } catch (error2) {
7221
- return false;
7222
- }
7223
- }
7224
- /**
7225
- * Safely get an item from localStorage with JSON parsing
7226
- */
7227
- safeGetItem(key) {
7228
- if (!this.isLocalStorageAvailable) {
7229
- return null;
7230
- }
7231
- try {
7232
- const item = localStorage.getItem(key);
7233
- if (item === null) {
7234
- return null;
7235
- }
7236
- return JSON.parse(item);
7237
- } catch (error2) {
7238
- this.safeClearItem(key);
7239
- return null;
7240
- }
7241
- }
7242
- /**
7243
- * Safely set an item in localStorage with JSON stringification
7244
- */
7245
- safeSetItem(key, value) {
7246
- if (!this.isLocalStorageAvailable) {
7247
- return false;
7248
- }
7249
- try {
7250
- localStorage.setItem(key, JSON.stringify(value));
7251
- return true;
7252
- } catch (error2) {
7253
- if (error2 instanceof DOMException && error2.code === 22) {
7254
- this.clearOldData();
7255
- try {
7256
- localStorage.setItem(key, JSON.stringify(value));
7257
- return true;
7258
- } catch (retryError) {
7259
- }
7260
- }
7261
- return false;
7262
- }
7263
- }
7264
- /**
7265
- * Safely clear an item from localStorage
7266
- */
7267
- safeClearItem(key) {
7268
- if (!this.isLocalStorageAvailable) {
7269
- return;
7270
- }
7271
- try {
7272
- localStorage.removeItem(key);
7273
- } catch (error2) {
7274
- }
7275
- }
7276
- /**
7277
- * Clear old data to free up storage space
7278
- */
7279
- clearOldData() {
7280
- this.safeClearItem(STORAGE_KEYS.SESSION);
7281
- }
7282
- // =============================================================================
7283
- // Progress Data Methods
7284
- // =============================================================================
7285
- /**
7286
- * Get playlist progress data for a specific user
7287
- * If userId doesn't match stored userId, returns null (stale data)
7288
- * @param userId - The user ID to validate against stored data
7289
- */
7290
- getProgress(userId) {
7291
- const stored = this.safeGetItem(STORAGE_KEYS.PROGRESS);
7292
- if (!stored) {
7293
- return null;
7294
- }
7295
- if (userId && stored.userId && stored.userId !== userId) {
7296
- log(`[StorageManager] Progress belongs to different user (${stored.userId}), ignoring`);
7297
- return null;
7298
- }
7299
- return stored.playlists || null;
7300
- }
7301
- /**
7302
- * Set playlist progress data for a specific user
7303
- * Clears existing data if userId changes
7304
- * @param progress - The progress data to save
7305
- * @param userId - The user ID to associate with this progress
7306
- */
7307
- setProgress(progress, userId) {
7308
- const stored = {
7309
- userId,
7310
- playlists: progress
7311
- };
7312
- return this.safeSetItem(STORAGE_KEYS.PROGRESS, stored);
7313
- }
7314
- /**
7315
- * Clear all progress data
7316
- */
7317
- clearProgress() {
7318
- this.safeClearItem(STORAGE_KEYS.PROGRESS);
7319
- }
7320
- // =============================================================================
7321
- // Session Data Methods
7322
- // =============================================================================
7323
- /**
7324
- * Get session data
7325
- */
7326
- getSession() {
7327
- return this.safeGetItem(STORAGE_KEYS.SESSION);
7328
- }
7329
- /**
7330
- * Set session data
7331
- */
7332
- setSession(session) {
7333
- return this.safeSetItem(STORAGE_KEYS.SESSION, session);
7334
- }
7335
- /**
7336
- * Clear session data
7337
- */
7338
- clearSession() {
7339
- this.safeClearItem(STORAGE_KEYS.SESSION);
7340
- }
7341
- // =============================================================================
7342
- // Anonymous User Data Methods
7343
- // =============================================================================
7344
- /**
7345
- * Get anonymous user data
7346
- */
7347
- getAnonymousUserData() {
7348
- return this.safeGetItem(STORAGE_KEYS.ANONYMOUS_USER);
7349
- }
7350
- /**
7351
- * Set anonymous user data
7352
- */
7353
- setAnonymousUserData(data) {
7354
- return this.safeSetItem(STORAGE_KEYS.ANONYMOUS_USER, data);
7355
- }
7356
- /**
7357
- * Clear anonymous user data
7358
- */
7359
- clearAnonymousUserData() {
7360
- this.safeClearItem(STORAGE_KEYS.ANONYMOUS_USER);
7361
- }
7362
- // =============================================================================
7363
- // Anonymous User ID Methods
7364
- // =============================================================================
7365
- /**
7366
- * Get anonymous user ID
7367
- */
7368
- getAnonymousUserId() {
7369
- if (!this.isLocalStorageAvailable) {
7370
- return null;
7371
- }
7372
- try {
7373
- return localStorage.getItem("saltfish_anonymous_user_id");
7374
- } catch (error2) {
7375
- return null;
7376
- }
7377
- }
7378
- /**
7379
- * Set anonymous user ID
7380
- */
7381
- setAnonymousUserId(userId) {
7382
- if (!this.isLocalStorageAvailable) {
7383
- return false;
7384
- }
7385
- try {
7386
- localStorage.setItem("saltfish_anonymous_user_id", userId);
7387
- return true;
7388
- } catch (error2) {
7389
- return false;
7390
- }
7391
- }
7392
- /**
7393
- * Clear anonymous user ID
7394
- */
7395
- clearAnonymousUserId() {
7396
- if (!this.isLocalStorageAvailable) {
7397
- return;
7398
- }
7399
- try {
7400
- localStorage.removeItem("saltfish_anonymous_user_id");
7401
- } catch (error2) {
7402
- }
7403
- }
7404
- // =============================================================================
7405
- // Utility Methods
7406
- // =============================================================================
7407
- /**
7408
- * Clear all storage data
7409
- */
7410
- clearAll() {
7411
- this.clearProgress();
7412
- this.clearSession();
7413
- this.clearAnonymousUserData();
7414
- this.clearAnonymousUserId();
7415
- }
7416
- /**
7417
- * Get storage availability status
7418
- */
7419
- isStorageAvailable() {
7420
- return this.isLocalStorageAvailable;
7421
- }
7422
- /**
7423
- * Get storage usage information (if available)
7424
- */
7425
- getStorageInfo() {
7426
- const keys = [];
7427
- if (this.isLocalStorageAvailable) {
7428
- try {
7429
- for (let i = 0; i < localStorage.length; i++) {
7430
- const key = localStorage.key(i);
7431
- if (key && key.startsWith("saltfish_")) {
7432
- keys.push(key);
7433
- }
7434
- }
7435
- } catch (error2) {
7436
- }
7437
- }
7438
- return {
7439
- available: this.isLocalStorageAvailable,
7440
- keys
7441
- };
7442
- }
7443
- }
7444
7525
  class SessionManager {
7445
- constructor(storageManager) {
7526
+ constructor(storageManager2) {
7446
7527
  __publicField(this, "sessionId");
7447
7528
  __publicField(this, "currentRunId", null);
7448
7529
  __publicField(this, "storageManager");
7449
- this.storageManager = storageManager || new StorageManager();
7530
+ this.storageManager = storageManager2 || new StorageManager();
7450
7531
  this.sessionId = this.getOrCreateSession();
7451
7532
  log(`SessionManager: Initialized with sessionId: ${this.sessionId}`);
7452
7533
  }
@@ -8768,11 +8849,11 @@ class PlaylistLoader {
8768
8849
  * @returns Promise resolving to manifest and determined start step
8769
8850
  */
8770
8851
  async loadManifest(options) {
8771
- const { manifestPath, playlistOptions } = options;
8852
+ const { manifestPath, playlistOptions, savedProgress } = options;
8772
8853
  try {
8773
8854
  const manifest = await this.fetchManifest(manifestPath);
8774
8855
  this.validateManifest(manifest);
8775
- const startStepId = this.determineStartStep(manifest, playlistOptions);
8856
+ const startStepId = this.determineStartStep(manifest, playlistOptions, savedProgress);
8776
8857
  log(`PlaylistLoader: Successfully loaded manifest '${manifest.id}' with start step '${startStepId}'`);
8777
8858
  return {
8778
8859
  manifest,
@@ -8858,14 +8939,15 @@ class PlaylistLoader {
8858
8939
  }
8859
8940
  }
8860
8941
  /**
8861
- * Determines which step to start from based on options
8862
- * Note: Persistence has been removed - playlists always start from beginning unless startNodeId is specified
8863
- * The 4-second rule in PlayerInitializationService handles immediate resume during page transitions
8942
+ * Determines which step to start from based on persistence and saved progress
8864
8943
  * @param manifest - The loaded playlist manifest
8865
8944
  * @param options - Playlist configuration options
8945
+ * @param savedProgress - Previously saved progress data
8866
8946
  * @returns The step ID to start from
8867
8947
  */
8868
- determineStartStep(manifest, options) {
8948
+ determineStartStep(manifest, options, savedProgress) {
8949
+ const isPersistenceEnabled = options.persistence ?? manifest.isPersistent ?? true;
8950
+ const manifestIdForProgress = manifest.id;
8869
8951
  let startStepId = manifest.startStep;
8870
8952
  if (options.startNodeId) {
8871
8953
  const customStep = manifest.steps.find((step) => step.id === options.startNodeId);
@@ -8875,6 +8957,19 @@ class PlaylistLoader {
8875
8957
  } else {
8876
8958
  log(`PlaylistLoader: Custom start node '${options.startNodeId}' not found, using default start step`);
8877
8959
  }
8960
+ } else if (isPersistenceEnabled && savedProgress && savedProgress[manifestIdForProgress]) {
8961
+ const progressData = savedProgress[manifestIdForProgress];
8962
+ if (progressData.status === "completed") {
8963
+ startStepId = manifest.startStep;
8964
+ } else {
8965
+ const lastStepId = progressData.currentStepId || progressData.lastStepId;
8966
+ if (lastStepId) {
8967
+ const savedStep = manifest.steps.find((step) => step.id === lastStepId);
8968
+ if (savedStep) {
8969
+ startStepId = lastStepId;
8970
+ }
8971
+ }
8972
+ }
8878
8973
  }
8879
8974
  return startStepId;
8880
8975
  }
@@ -8885,13 +8980,13 @@ class PlaylistManager {
8885
8980
  * @param eventManager - Optional event manager to subscribe to events
8886
8981
  * @param storageManager - Optional storage manager for localStorage operations
8887
8982
  */
8888
- constructor(eventManager, storageManager) {
8983
+ constructor(eventManager, storageManager2) {
8889
8984
  __publicField(this, "eventManager", null);
8890
8985
  __publicField(this, "isUpdatingWatchedPlaylists", false);
8891
8986
  __publicField(this, "playlistLoader");
8892
8987
  __publicField(this, "storageManager");
8893
8988
  this.playlistLoader = new PlaylistLoader();
8894
- this.storageManager = storageManager || new StorageManager();
8989
+ this.storageManager = storageManager2 || new StorageManager();
8895
8990
  if (eventManager) {
8896
8991
  this.setEventManager(eventManager);
8897
8992
  }
@@ -8962,7 +9057,8 @@ class PlaylistManager {
8962
9057
  const currentWatchedPlaylists = currentUserData.watchedPlaylists || {};
8963
9058
  const updatedPlaylistData = {
8964
9059
  status,
8965
- currentStepId: currentStepId || store.currentStepId || null,
9060
+ // Don't save currentStepId for completed playlists - they should restart from beginning
9061
+ currentStepId: status === "completed" ? null : currentStepId || store.currentStepId || null,
8966
9062
  timestamp: Date.now(),
8967
9063
  // Use timestamp for consistency with checkAndResumeInProgressPlaylist
8968
9064
  lastProgressAt: Date.now()
@@ -9042,12 +9138,15 @@ class PlaylistManager {
9042
9138
  * @param options - Playlist configuration options
9043
9139
  */
9044
9140
  async load(playlistId, options) {
9141
+ var _a, _b;
9045
9142
  try {
9046
9143
  const store = useSaltfishStore.getState();
9144
+ const isAnonymous = ((_a = store.user) == null ? void 0 : _a.__isAnonymous) === true;
9145
+ const savedProgress = isAnonymous ? store.progress : ((_b = store.userData) == null ? void 0 : _b.watchedPlaylists) || store.progress;
9047
9146
  const loadResult = await this.playlistLoader.loadManifest({
9048
9147
  manifestPath: playlistId,
9049
9148
  playlistOptions: options,
9050
- savedProgress: store.progress
9149
+ savedProgress
9051
9150
  });
9052
9151
  const { manifest, startStepId } = loadResult;
9053
9152
  const firstStep = manifest.steps.find((step) => step.id === startStepId);
@@ -9973,13 +10072,13 @@ class ManagerFactory {
9973
10072
  * Create all manager instances with proper dependencies
9974
10073
  */
9975
10074
  createManagers() {
9976
- const storageManager = new StorageManager();
9977
- const sessionManager = new SessionManager(storageManager);
10075
+ const storageManager2 = new StorageManager();
10076
+ const sessionManager = new SessionManager(storageManager2);
9978
10077
  const shadowDOMManager = new ShadowDOMManager();
9979
10078
  const videoManager = new VideoManager();
9980
10079
  const eventManager = new EventManager();
9981
10080
  const analyticsManager = new AnalyticsManager(eventManager);
9982
- const playlistManager = new PlaylistManager(eventManager, storageManager);
10081
+ const playlistManager = new PlaylistManager(eventManager, storageManager2);
9983
10082
  const abTestManager = new ABTestManager(eventManager);
9984
10083
  const cursorManager = new CursorManager();
9985
10084
  const interactionManager = new InteractionManager();
@@ -10003,7 +10102,7 @@ class ManagerFactory {
10003
10102
  playlistManager,
10004
10103
  stepTimeoutManager,
10005
10104
  uiManager,
10006
- storageManager
10105
+ storageManager: storageManager2
10007
10106
  };
10008
10107
  return managers;
10009
10108
  }
@@ -10224,7 +10323,7 @@ const SaltfishPlayer$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de
10224
10323
  __proto__: null,
10225
10324
  SaltfishPlayer
10226
10325
  }, Symbol.toStringTag, { value: "Module" }));
10227
- const version = "0.2.52";
10326
+ const version = "0.2.54";
10228
10327
  const packageJson = {
10229
10328
  version
10230
10329
  };