@rohal12/spindle 0.39.0 → 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/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/index.tsx +4 -1
- package/src/store.ts +54 -1
- package/src/story-api.ts +21 -6
package/package.json
CHANGED
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
|
// ---------------------------------------------------------------------------
|
|
@@ -516,7 +551,19 @@ export const useStoryStore = create<StoryState>()(
|
|
|
516
551
|
const startPassage = storyData.passagesById.get(storyData.startNode);
|
|
517
552
|
if (!startPassage) return;
|
|
518
553
|
|
|
554
|
+
// Reset renderDeferred before firing beforerestart so we can detect
|
|
555
|
+
// if a handler called deferRender() during the callback.
|
|
556
|
+
set((state) => {
|
|
557
|
+
state.renderDeferred = false;
|
|
558
|
+
});
|
|
559
|
+
|
|
519
560
|
fireBeforeRestart();
|
|
561
|
+
|
|
562
|
+
const keepDeferred = get().renderDeferred;
|
|
563
|
+
|
|
564
|
+
// Clean up all runtime-phase handlers (after beforerestart has fired)
|
|
565
|
+
cleanupRuntimeHandlers();
|
|
566
|
+
|
|
520
567
|
resetPRNG();
|
|
521
568
|
resetTriggers();
|
|
522
569
|
const initialVars = deepClone(variableDefaults);
|
|
@@ -535,10 +582,16 @@ export const useStoryStore = create<StoryState>()(
|
|
|
535
582
|
state.historyIndex = 0;
|
|
536
583
|
state.visitCounts = { [startPassage.name]: 1 };
|
|
537
584
|
state.renderCounts = { [startPassage.name]: 1 };
|
|
538
|
-
|
|
585
|
+
if (!keepDeferred) {
|
|
586
|
+
state.renderDeferred = false;
|
|
587
|
+
}
|
|
539
588
|
});
|
|
540
589
|
|
|
541
590
|
lastNavigationVars = get().variables;
|
|
591
|
+
|
|
592
|
+
// Re-enter runtime phase before StoryInit so new handlers are tracked
|
|
593
|
+
enterRuntimePhase();
|
|
594
|
+
|
|
542
595
|
executeStoryInit();
|
|
543
596
|
clearSession(storyData.ifid);
|
|
544
597
|
fireStoryInit();
|
package/src/story-api.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
391
|
+
const unsub = onBeforeRestart(callback as BeforeRestartCallback);
|
|
392
|
+
trackRuntimeUnsub(unsub);
|
|
393
|
+
return unsub;
|
|
385
394
|
}
|
|
386
395
|
|
|
387
396
|
if (event === 'storyinit') {
|
|
388
|
-
|
|
397
|
+
const unsub = onStoryInit(callback as StoryInitCallback);
|
|
398
|
+
trackRuntimeUnsub(unsub);
|
|
399
|
+
return unsub;
|
|
389
400
|
}
|
|
390
401
|
|
|
391
402
|
if (event === 'actionsChanged') {
|
|
392
|
-
|
|
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
|
-
|
|
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}".`);
|