@rohal12/spindle 0.59.9 → 0.59.10

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.
@@ -147,6 +147,7 @@ async function startNewPlaythroughNow(
147
147
  await backend.setMeta(playthroughCountKey(ifid), num);
148
148
  });
149
149
  await backend.setMeta(currentPlaythroughKey(ifid), id);
150
+ saveSessionPlaythrough(ifid, id);
150
151
  return id;
151
152
  }
152
153
 
@@ -202,6 +203,7 @@ async function adoptPlaythroughNow(ifid: string, id: string): Promise<void> {
202
203
  if ((await backend.getMeta<string>(key)) !== id) {
203
204
  await backend.setMeta(key, id);
204
205
  }
206
+ saveSessionPlaythrough(ifid, id);
205
207
  }
206
208
 
207
209
  /**
@@ -231,9 +233,18 @@ export function establishPlaythrough(
231
233
  ): Promise<{ id: string; knownSaves: Record<string, true> }> {
232
234
  return inOrder(async () => {
233
235
  const backend = await getBackend();
234
- const id =
235
- (await backend.getMeta<string>(currentPlaythroughKey(ifid))) ??
236
- (await startNewPlaythroughNow(ifid));
236
+ // A tab that restores its session continues the session's playthrough;
237
+ // the shared current one is the default for a visit with no session,
238
+ // and may belong to what another tab has done since (#356).
239
+ const own = loadSessionPlaythrough(ifid);
240
+ const ownExists =
241
+ own !== undefined &&
242
+ (await backend.getPlaythroughsByIfid(ifid)).some((p) => p.id === own);
243
+ const id = ownExists
244
+ ? own
245
+ : ((await backend.getMeta<string>(currentPlaythroughKey(ifid))) ??
246
+ (await startNewPlaythroughNow(ifid)));
247
+ saveSessionPlaythrough(ifid, id);
237
248
  return { id, knownSaves: await populateKnownSavesNow(ifid) };
238
249
  });
239
250
  }
@@ -673,15 +684,20 @@ export const loadQuickSave = readingSlot(livePayload);
673
684
  * (see adoptPlaythrough) in the same operation: operations issued after the
674
685
  * load take effect in the loaded playthrough. Resolves to the live payload
675
686
  * and its playthrough, or undefined if the slot is empty.
687
+ *
688
+ * `check` is run on the payload first: when it throws, the load rejects and
689
+ * the game stays in its playthrough (#357).
676
690
  */
677
691
  export function loadSlotSave(
678
692
  ifid: string,
679
693
  slot?: string,
694
+ check?: (payload: SavePayload) => void,
680
695
  ): Promise<{ payload: SavePayload; playthroughId: string } | undefined> {
681
696
  return inOrder(async () => {
682
697
  const record = await slotRecord(ifid, slot);
683
698
  if (!record) return undefined;
684
699
  const payload = decodePayload(record.payload);
700
+ check?.(payload);
685
701
  const { playthroughId } = record.meta;
686
702
  if (playthroughId) await adoptPlaythroughNow(ifid, playthroughId);
687
703
  return { payload, playthroughId };
@@ -795,6 +811,34 @@ export function clearSession(ifid: string): void {
795
811
  }
796
812
  }
797
813
 
814
+ /**
815
+ * The playthrough this tab's game is in, kept with its session: the shared
816
+ * current playthrough (a stored meta value) can be changed by another tab.
817
+ * Written where this tab's storage operations set the current playthrough,
818
+ * so it follows them in call order. Under the session prefix, so clearing
819
+ * all data clears it.
820
+ */
821
+ const SESSION_PLAYTHROUGH_PREFIX = `${SESSION_KEY_PREFIX}playthrough.`;
822
+
823
+ function saveSessionPlaythrough(ifid: string, id: string): void {
824
+ try {
825
+ sessionStorage.setItem(`${SESSION_PLAYTHROUGH_PREFIX}${ifid}`, id);
826
+ } catch {
827
+ // the session cannot be stored: a reload takes the shared playthrough
828
+ }
829
+ }
830
+
831
+ function loadSessionPlaythrough(ifid: string): string | undefined {
832
+ try {
833
+ return (
834
+ sessionStorage.getItem(`${SESSION_PLAYTHROUGH_PREFIX}${ifid}`) ||
835
+ undefined
836
+ );
837
+ } catch {
838
+ return undefined;
839
+ }
840
+ }
841
+
798
842
  // --- Export / Import ---
799
843
 
800
844
  /** The export of a stored save, if there is one. */
package/src/store.ts CHANGED
@@ -965,6 +965,19 @@ export function missingPassage(
965
965
  );
966
966
  }
967
967
 
968
+ /** Throw if `payload` refers to a passage `storyData` does not have. */
969
+ function assertPassagesExist(
970
+ storyData: StoryData | null | undefined,
971
+ payload: SavePayload,
972
+ ): void {
973
+ const missing = missingPassage(storyData, payload);
974
+ if (missing !== undefined) {
975
+ throw new Error(
976
+ `The save refers to the passage "${missing}", which this version of the story does not have`,
977
+ );
978
+ }
979
+ }
980
+
968
981
  const NAMESPACE_KEYS = ['variables', 'temporary', 'transient'] as const;
969
982
 
970
983
  type StoryRecipe = (draft: Draft<StoryState>) => void;
@@ -1449,7 +1462,9 @@ export const useStoryStore = create<StoryState>()(
1449
1462
  // the game on, as usual). An empty slot leaves the playthrough as it
1450
1463
  // is.
1451
1464
  const previous = resolvePlaythroughId();
1452
- const read = loadSlotSave(storyData.ifid, slot);
1465
+ const read = loadSlotSave(storyData.ifid, slot, (payload) =>
1466
+ assertPassagesExist(storyData, payload),
1467
+ );
1453
1468
  const switched = switchToLookedUpPlaythrough(
1454
1469
  Promise.all([previous, read.catch(() => undefined)]).then(
1455
1470
  ([prev, loaded]) => loaded?.playthroughId || prev,
@@ -1646,12 +1661,7 @@ export const useStoryStore = create<StoryState>()(
1646
1661
  }
1647
1662
  // Before anything is replaced: an incompatible payload leaves the
1648
1663
  // running game as it is
1649
- const missing = missingPassage(get().storyData, payload);
1650
- if (missing !== undefined) {
1651
- throw new Error(
1652
- `The save refers to the passage "${missing}", which this version of the story does not have`,
1653
- );
1654
- }
1664
+ assertPassagesExist(get().storyData, payload);
1655
1665
  latestStateApplied = replacement;
1656
1666
 
1657
1667
  emit('beforeload', slot);