@rohal12/spindle 0.39.1 → 0.40.0

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.39.1",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
package/src/index.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { render } from 'preact';
2
2
  import { App } from './components/App';
3
3
  import { parseStoryData } from './parser';
4
- import { useStoryStore, fireStoryInit } from './store';
4
+ import { useStoryStore, fireStoryInit, enterRuntimePhase } from './store';
5
5
  import { installStoryAPI, getReadyPromise } from './story-api';
6
6
  import { resetIdCounters } from './action-registry';
7
7
  import { executeStoryInit } from './story-init';
@@ -99,6 +99,9 @@ function boot() {
99
99
 
100
100
  useStoryStore.getState().init(storyData, defaults);
101
101
 
102
+ // Enter runtime phase — handlers registered from here on are cleaned on restart
103
+ enterRuntimePhase();
104
+
102
105
  // Execute StoryInit passage if it exists
103
106
  executeStoryInit();
104
107
 
package/src/store.ts CHANGED
@@ -177,6 +177,41 @@ function resetModuleState(base: Record<string, unknown>): void {
177
177
  serializedHistory = [];
178
178
  }
179
179
 
180
+ // ---------------------------------------------------------------------------
181
+ // Runtime handler cleanup (auto-unsub on restart)
182
+ // ---------------------------------------------------------------------------
183
+
184
+ let runtimeUnsubs: Array<() => void> = [];
185
+ let inRuntimePhase = false;
186
+
187
+ /**
188
+ * Track an unsubscribe function for automatic cleanup on restart.
189
+ * No-op if called during the startup phase (before enterRuntimePhase).
190
+ */
191
+ export function trackRuntimeUnsub(unsub: () => void): void {
192
+ if (inRuntimePhase) {
193
+ runtimeUnsubs.push(unsub);
194
+ }
195
+ }
196
+
197
+ /** Mark the start of the runtime phase. Called before executeStoryInit(). */
198
+ export function enterRuntimePhase(): void {
199
+ inRuntimePhase = true;
200
+ }
201
+
202
+ /** Call all tracked unsubs and reset the runtime phase. */
203
+ function cleanupRuntimeHandlers(): void {
204
+ for (const unsub of runtimeUnsubs) unsub();
205
+ runtimeUnsubs = [];
206
+ inRuntimePhase = false;
207
+ }
208
+
209
+ /** Test-only: reset runtime phase state between tests. */
210
+ export function _resetRuntimePhase(): void {
211
+ runtimeUnsubs = [];
212
+ inRuntimePhase = false;
213
+ }
214
+
180
215
  // ---------------------------------------------------------------------------
181
216
  // storyinit callbacks (direct invocation — avoids Zustand subscription issues)
182
217
  // ---------------------------------------------------------------------------
@@ -526,6 +561,9 @@ export const useStoryStore = create<StoryState>()(
526
561
 
527
562
  const keepDeferred = get().renderDeferred;
528
563
 
564
+ // Clean up all runtime-phase handlers (after beforerestart has fired)
565
+ cleanupRuntimeHandlers();
566
+
529
567
  resetPRNG();
530
568
  resetTriggers();
531
569
  const initialVars = deepClone(variableDefaults);
@@ -550,6 +588,10 @@ export const useStoryStore = create<StoryState>()(
550
588
  });
551
589
 
552
590
  lastNavigationVars = get().variables;
591
+
592
+ // Re-enter runtime phase before StoryInit so new handlers are tracked
593
+ enterRuntimePhase();
594
+
553
595
  executeStoryInit();
554
596
  clearSession(storyData.ifid);
555
597
  fireStoryInit();
package/src/story-api.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { useStoryStore, onStoryInit, onBeforeRestart } from './store';
1
+ import {
2
+ useStoryStore,
3
+ onStoryInit,
4
+ onBeforeRestart,
5
+ trackRuntimeUnsub,
6
+ } from './store';
2
7
  import type { Passage } from './parser';
3
8
  import { settings } from './settings';
4
9
  import type {
@@ -371,30 +376,38 @@ function createStoryAPI(): StoryAPI {
371
376
  on(event: string, callback: (...args: any[]) => void): () => void {
372
377
  if (event === 'navigate') {
373
378
  let prev = useStoryStore.getState().currentPassage;
374
- return useStoryStore.subscribe((state) => {
379
+ const unsub = useStoryStore.subscribe((state) => {
375
380
  if (state.currentPassage !== prev) {
376
381
  const from = prev;
377
382
  prev = state.currentPassage;
378
383
  (callback as NavigateCallback)(state.currentPassage, from);
379
384
  }
380
385
  });
386
+ trackRuntimeUnsub(unsub);
387
+ return unsub;
381
388
  }
382
389
 
383
390
  if (event === 'beforerestart') {
384
- return onBeforeRestart(callback as BeforeRestartCallback);
391
+ const unsub = onBeforeRestart(callback as BeforeRestartCallback);
392
+ trackRuntimeUnsub(unsub);
393
+ return unsub;
385
394
  }
386
395
 
387
396
  if (event === 'storyinit') {
388
- return onStoryInit(callback as StoryInitCallback);
397
+ const unsub = onStoryInit(callback as StoryInitCallback);
398
+ trackRuntimeUnsub(unsub);
399
+ return unsub;
389
400
  }
390
401
 
391
402
  if (event === 'actionsChanged') {
392
- return onActionsChanged(callback as ActionsChangedCallback);
403
+ const unsub = onActionsChanged(callback as ActionsChangedCallback);
404
+ trackRuntimeUnsub(unsub);
405
+ return unsub;
393
406
  }
394
407
 
395
408
  if (event === 'variableChanged') {
396
409
  let prevVars = { ...useStoryStore.getState().variables };
397
- return useStoryStore.subscribe((state) => {
410
+ const unsub = useStoryStore.subscribe((state) => {
398
411
  const changed: Record<string, { from: unknown; to: unknown }> = {};
399
412
  let hasChanges = false;
400
413
  const allKeys = new Set([
@@ -412,6 +425,8 @@ function createStoryAPI(): StoryAPI {
412
425
  (callback as VariableChangedCallback)(changed);
413
426
  }
414
427
  });
428
+ trackRuntimeUnsub(unsub);
429
+ return unsub;
415
430
  }
416
431
 
417
432
  throw new Error(`spindle: Unknown event "${event}".`);