@rohal12/spindle 0.31.1 → 0.31.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.31.1",
3
+ "version": "0.31.3",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -119,7 +119,7 @@ defineMenubarAction({
119
119
  setup: () => {
120
120
  const load = useStoryStore((s) => s.load);
121
121
  const hasSave = useStoryStore((s) => s.hasSave);
122
- useStoryStore((s) => s.saveVersion);
122
+ useStoryStore((s) => s.knownSaves);
123
123
  return { perform: () => load(), disabled: !hasSave() };
124
124
  },
125
125
  });
@@ -173,7 +173,8 @@ export function defineMacro(config: MacroDefinition): void {
173
173
  }
174
174
 
175
175
  if (config.storeVar) {
176
- const firstToken = props.rawArgs.trim().split(/[\s"']+/)[0] ?? '';
176
+ const firstToken =
177
+ props.rawArgs.trim().split(/\s+/)[0]?.replace(/["']/g, '') ?? '';
177
178
  const varExpr = firstToken.replace(/["']/g, '').replace(/^\$/, '');
178
179
  const segments = varExpr.split('.');
179
180
  ctx.varName = varExpr;
@@ -258,16 +258,25 @@ export async function getSavesGrouped(
258
258
  return result;
259
259
  }
260
260
 
261
- // --- Quick Save ---
261
+ // --- Quick Save / Slot Save ---
262
262
 
263
263
  const AUTOSAVE_KEY_PREFIX = 'autosave.';
264
+ const SLOT_KEY_PREFIX = 'slot.';
265
+ const SLOT_INDEX_KEY_PREFIX = 'slotIndex.';
266
+
267
+ function slotMetaKey(ifid: string, slot?: string): string {
268
+ return slot != null
269
+ ? `${SLOT_KEY_PREFIX}${slot}.${ifid}`
270
+ : `${AUTOSAVE_KEY_PREFIX}${ifid}`;
271
+ }
264
272
 
265
273
  export async function quickSave(
266
274
  ifid: string,
267
275
  playthroughId: string,
268
276
  payload: SavePayload,
277
+ slot?: string,
269
278
  ): Promise<SaveRecord> {
270
- const metaKey = `${AUTOSAVE_KEY_PREFIX}${ifid}`;
279
+ const metaKey = slotMetaKey(ifid, slot);
271
280
  const existingId = await getMeta<string>(metaKey);
272
281
 
273
282
  if (existingId) {
@@ -275,16 +284,30 @@ export async function quickSave(
275
284
  if (updated) return updated;
276
285
  }
277
286
 
278
- // Create new autosave
287
+ // Create new save
279
288
  const record = await createSave(ifid, playthroughId, payload, {
280
- isAutosave: true,
289
+ isAutosave: !slot,
290
+ ...(slot != null ? { slot } : {}),
281
291
  });
282
292
  await setMeta(metaKey, record.meta.id);
293
+
294
+ // Track named slots in the index
295
+ if (slot != null) {
296
+ const indexKey = `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
297
+ const existing = (await getMeta<string[]>(indexKey)) ?? [];
298
+ if (!existing.includes(slot)) {
299
+ await setMeta(indexKey, [...existing, slot]);
300
+ }
301
+ }
302
+
283
303
  return record;
284
304
  }
285
305
 
286
- export async function hasQuickSave(ifid: string): Promise<boolean> {
287
- const metaKey = `${AUTOSAVE_KEY_PREFIX}${ifid}`;
306
+ export async function hasQuickSave(
307
+ ifid: string,
308
+ slot?: string,
309
+ ): Promise<boolean> {
310
+ const metaKey = slotMetaKey(ifid, slot);
288
311
  const existingId = await getMeta<string>(metaKey);
289
312
  if (!existingId) return false;
290
313
  const record = await getSave(existingId);
@@ -293,13 +316,40 @@ export async function hasQuickSave(ifid: string): Promise<boolean> {
293
316
 
294
317
  export async function loadQuickSave(
295
318
  ifid: string,
319
+ slot?: string,
296
320
  ): Promise<SavePayload | undefined> {
297
- const metaKey = `${AUTOSAVE_KEY_PREFIX}${ifid}`;
321
+ const metaKey = slotMetaKey(ifid, slot);
298
322
  const existingId = await getMeta<string>(metaKey);
299
323
  if (!existingId) return undefined;
300
324
  return loadSave(existingId);
301
325
  }
302
326
 
327
+ /**
328
+ * Check IDB for all known saves and return a map of slot keys to true.
329
+ * The default (autosave) slot uses empty string as key.
330
+ */
331
+ export async function populateKnownSaves(
332
+ ifid: string,
333
+ ): Promise<Record<string, true>> {
334
+ const result: Record<string, true> = {};
335
+
336
+ // Check default autosave
337
+ if (await hasQuickSave(ifid)) {
338
+ result[''] = true;
339
+ }
340
+
341
+ // Check named slots from the index
342
+ const indexKey = `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
343
+ const slots = (await getMeta<string[]>(indexKey)) ?? [];
344
+ for (const slot of slots) {
345
+ if (await hasQuickSave(ifid, slot)) {
346
+ result[slot] = true;
347
+ }
348
+ }
349
+
350
+ return result;
351
+ }
352
+
303
353
  // --- Session Persistence (survives F5, cleared on tab close) ---
304
354
 
305
355
  const SESSION_KEY_PREFIX = 'spindle.session.';
package/src/store.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  getCurrentPlaythroughId,
18
18
  quickSave,
19
19
  loadQuickSave,
20
+ populateKnownSaves,
20
21
  saveSession,
21
22
  clearSession,
22
23
  } from './saves/save-manager';
@@ -198,7 +199,7 @@ export interface StoryState {
198
199
  historyIndex: number;
199
200
  visitCounts: Record<string, number>;
200
201
  renderCounts: Record<string, number>;
201
- saveVersion: number;
202
+ knownSaves: Record<string, true>;
202
203
  playthroughId: string;
203
204
  maxHistory: number;
204
205
  saveError: string | null;
@@ -243,7 +244,7 @@ export const useStoryStore = create<StoryState>()(
243
244
  historyIndex: -1,
244
245
  visitCounts: {},
245
246
  renderCounts: {},
246
- saveVersion: 0,
247
+ knownSaves: {},
247
248
  playthroughId: '',
248
249
  maxHistory: 40,
249
250
  saveError: null,
@@ -307,6 +308,14 @@ export const useStoryStore = create<StoryState>()(
307
308
  state.playthroughId = newId;
308
309
  });
309
310
  }
311
+
312
+ // Populate knownSaves from IDB so hasSave() works after reload
313
+ const saves = await populateKnownSaves(ifid);
314
+ if (Object.keys(saves).length > 0) {
315
+ set((state) => {
316
+ state.knownSaves = saves;
317
+ });
318
+ }
310
319
  })
311
320
  .catch((err) =>
312
321
  console.error('spindle: failed to init save system', err),
@@ -487,7 +496,7 @@ export const useStoryStore = create<StoryState>()(
487
496
  );
488
497
  },
489
498
 
490
- save: () => {
499
+ save: (slot?: string) => {
491
500
  const { storyData, playthroughId } = get();
492
501
  if (!storyData) return;
493
502
 
@@ -496,14 +505,17 @@ export const useStoryStore = create<StoryState>()(
496
505
  set((state) => {
497
506
  state.saveError = null;
498
507
  });
499
- quickSave(storyData.ifid, playthroughId, payload)
508
+ quickSave(storyData.ifid, playthroughId, payload, slot)
500
509
  .then(() => {
501
510
  set((state) => {
502
- state.saveVersion++;
511
+ state.knownSaves = {
512
+ ...state.knownSaves,
513
+ [slot ?? '']: true,
514
+ };
503
515
  });
504
516
  })
505
517
  .catch((err) => {
506
- console.error('spindle: failed to quick save', err);
518
+ console.error('spindle: failed to save', err);
507
519
  set((state) => {
508
520
  state.saveError =
509
521
  err instanceof Error ? err.message : 'Failed to save';
@@ -511,20 +523,20 @@ export const useStoryStore = create<StoryState>()(
511
523
  });
512
524
  },
513
525
 
514
- load: () => {
526
+ load: (slot?: string) => {
515
527
  const { storyData } = get();
516
528
  if (!storyData) return;
517
529
 
518
530
  set((state) => {
519
531
  state.loadError = null;
520
532
  });
521
- loadQuickSave(storyData.ifid)
533
+ loadQuickSave(storyData.ifid, slot)
522
534
  .then((payload) => {
523
535
  if (!payload) return;
524
536
  get().loadFromPayload(payload);
525
537
  })
526
538
  .catch((err) => {
527
- console.error('spindle: failed to load quick save', err);
539
+ console.error('spindle: failed to load save', err);
528
540
  set((state) => {
529
541
  state.loadError =
530
542
  err instanceof Error ? err.message : 'Failed to load';
@@ -532,11 +544,10 @@ export const useStoryStore = create<StoryState>()(
532
544
  });
533
545
  },
534
546
 
535
- hasSave: () => {
536
- // Synchronous: return true if a save has been made this session
537
- const { storyData, saveVersion } = get();
547
+ hasSave: (slot?: string) => {
548
+ const { storyData, knownSaves } = get();
538
549
  if (!storyData) return false;
539
- return saveVersion > 0;
550
+ return (slot ?? '') in knownSaves;
540
551
  },
541
552
 
542
553
  getSavePayload: (): SavePayload => {