@rohal12/spindle 0.37.0 → 0.38.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/components/macros/PassageDisplay.tsx +13 -10
- package/src/index.tsx +13 -4
- package/src/store.ts +39 -10
- package/src/story-api.ts +37 -8
package/package.json
CHANGED
|
@@ -71,6 +71,7 @@ defineMacro({
|
|
|
71
71
|
render(_props, ctx) {
|
|
72
72
|
const currentPassage = useStoryStore((s) => s.currentPassage);
|
|
73
73
|
const storyData = useStoryStore((s) => s.storyData);
|
|
74
|
+
const renderDeferred = useStoryStore((s) => s.renderDeferred);
|
|
74
75
|
|
|
75
76
|
// Render-gating: displayedPassage controls which passage is visually shown.
|
|
76
77
|
// The store updates immediately, but the visual swap is deferred by the
|
|
@@ -245,12 +246,14 @@ defineMacro({
|
|
|
245
246
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
246
247
|
}, [currentPassage]);
|
|
247
248
|
|
|
248
|
-
//
|
|
249
|
-
const
|
|
250
|
-
? storyData?.passages.get(
|
|
251
|
-
:
|
|
249
|
+
// When render is deferred, show StoryLoading passage or nothing
|
|
250
|
+
const effectivePassage = renderDeferred
|
|
251
|
+
? (storyData?.passages.get('StoryLoading') ?? null)
|
|
252
|
+
: displayedPassage
|
|
253
|
+
? storyData?.passages.get(displayedPassage)
|
|
254
|
+
: null;
|
|
252
255
|
|
|
253
|
-
if (!
|
|
256
|
+
if (!effectivePassage && displayedPassage && !renderDeferred) {
|
|
254
257
|
return (
|
|
255
258
|
<div class="error">
|
|
256
259
|
Error: Passage “{displayedPassage}” not found.
|
|
@@ -265,7 +268,7 @@ defineMacro({
|
|
|
265
268
|
id={ctx.id ?? 'story'}
|
|
266
269
|
class={ctx.className ?? 'story'}
|
|
267
270
|
>
|
|
268
|
-
{readyPassage && (
|
|
271
|
+
{!renderDeferred && readyPassage && (
|
|
269
272
|
<div
|
|
270
273
|
key={`ready-${currentPassage}`}
|
|
271
274
|
hidden
|
|
@@ -277,11 +280,11 @@ defineMacro({
|
|
|
277
280
|
class="passage-container"
|
|
278
281
|
ref={containerRef}
|
|
279
282
|
>
|
|
280
|
-
{
|
|
283
|
+
{effectivePassage && (
|
|
281
284
|
<Passage
|
|
282
|
-
passage={
|
|
283
|
-
key={displayedPassage}
|
|
284
|
-
dataTransition={resolvedTypeRef.current}
|
|
285
|
+
passage={effectivePassage}
|
|
286
|
+
key={renderDeferred ? 'loading' : displayedPassage}
|
|
287
|
+
dataTransition={renderDeferred ? 'none' : resolvedTypeRef.current}
|
|
285
288
|
/>
|
|
286
289
|
)}
|
|
287
290
|
</div>
|
package/src/index.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { render } from 'preact';
|
|
2
2
|
import { App } from './components/App';
|
|
3
3
|
import { parseStoryData } from './parser';
|
|
4
|
-
import { useStoryStore } from './store';
|
|
5
|
-
import { installStoryAPI } from './story-api';
|
|
4
|
+
import { useStoryStore, fireStoryInit } from './store';
|
|
5
|
+
import { installStoryAPI, getReadyPromise } from './story-api';
|
|
6
6
|
import { resetIdCounters } from './action-registry';
|
|
7
7
|
import { executeStoryInit } from './story-init';
|
|
8
8
|
import { checkTriggers, reinitTriggerState } from './triggers';
|
|
@@ -101,7 +101,6 @@ function boot() {
|
|
|
101
101
|
|
|
102
102
|
// Execute StoryInit passage if it exists
|
|
103
103
|
executeStoryInit();
|
|
104
|
-
useStoryStore.getState().bumpInitCount();
|
|
105
104
|
|
|
106
105
|
// Restore session state if the page was refreshed
|
|
107
106
|
const sessionPayload = loadSession(storyData.ifid);
|
|
@@ -109,6 +108,9 @@ function boot() {
|
|
|
109
108
|
useStoryStore.getState().loadFromPayload(sessionPayload);
|
|
110
109
|
}
|
|
111
110
|
|
|
111
|
+
// Fire storyinit after all state is settled (defaults + StoryInit + session)
|
|
112
|
+
fireStoryInit();
|
|
113
|
+
|
|
112
114
|
// Pass 1: Pre-scan all widget passages to discover block widgets.
|
|
113
115
|
// Register them as block macros BEFORE any tokenize/buildAST calls,
|
|
114
116
|
// so that widget bodies using other block widgets parse correctly
|
|
@@ -198,7 +200,14 @@ function boot() {
|
|
|
198
200
|
|
|
199
201
|
render(<App />, root);
|
|
200
202
|
|
|
201
|
-
|
|
203
|
+
const pending = getReadyPromise();
|
|
204
|
+
if (pending) {
|
|
205
|
+
pending.then(() => {
|
|
206
|
+
document.dispatchEvent(new CustomEvent(':storyready'));
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
document.dispatchEvent(new CustomEvent(':storyready'));
|
|
210
|
+
}
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
if (document.readyState === 'loading') {
|
package/src/store.ts
CHANGED
|
@@ -41,6 +41,7 @@ const SPECIAL_PASSAGES = new Set([
|
|
|
41
41
|
'StoryInit',
|
|
42
42
|
'StoryInterface',
|
|
43
43
|
'StoryVariables',
|
|
44
|
+
'StoryLoading',
|
|
44
45
|
'SaveTitle',
|
|
45
46
|
'PassageReady',
|
|
46
47
|
'PassageHeader',
|
|
@@ -176,6 +177,26 @@ function resetModuleState(base: Record<string, unknown>): void {
|
|
|
176
177
|
serializedHistory = [];
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// storyinit callbacks (direct invocation — avoids Zustand subscription issues)
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
type StoryInitListener = () => void;
|
|
185
|
+
let storyInitListeners: StoryInitListener[] = [];
|
|
186
|
+
|
|
187
|
+
/** Register a callback to run after StoryInit completes (boot + every restart). */
|
|
188
|
+
export function onStoryInit(cb: StoryInitListener): () => void {
|
|
189
|
+
storyInitListeners.push(cb);
|
|
190
|
+
return () => {
|
|
191
|
+
storyInitListeners = storyInitListeners.filter((l) => l !== cb);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Fire all storyinit listeners. Called after all state resets are complete. */
|
|
196
|
+
export function fireStoryInit(): void {
|
|
197
|
+
for (const cb of storyInitListeners) cb();
|
|
198
|
+
}
|
|
199
|
+
|
|
179
200
|
// ---------------------------------------------------------------------------
|
|
180
201
|
// Store
|
|
181
202
|
// ---------------------------------------------------------------------------
|
|
@@ -207,15 +228,14 @@ export interface StoryState {
|
|
|
207
228
|
renderCounts: Record<string, number>;
|
|
208
229
|
knownSaves: Record<string, true>;
|
|
209
230
|
playthroughId: string;
|
|
210
|
-
initCount: number;
|
|
211
231
|
maxHistory: number;
|
|
212
232
|
saveError: string | null;
|
|
213
233
|
loadError: string | null;
|
|
214
234
|
transitionConfig: TransitionConfig | null;
|
|
215
235
|
nextTransition: TransitionConfig | null;
|
|
216
236
|
nobr: boolean;
|
|
237
|
+
renderDeferred: boolean;
|
|
217
238
|
|
|
218
|
-
bumpInitCount: () => void;
|
|
219
239
|
setMaxHistory: (limit: number) => void;
|
|
220
240
|
init: (
|
|
221
241
|
storyData: StoryData,
|
|
@@ -245,6 +265,8 @@ export interface StoryState {
|
|
|
245
265
|
setTransition: (config: TransitionConfig | null) => void;
|
|
246
266
|
setNextTransition: (config: TransitionConfig | null) => void;
|
|
247
267
|
consumeNextTransition: () => TransitionConfig | null;
|
|
268
|
+
deferRender: () => void;
|
|
269
|
+
clearDeferredRender: () => void;
|
|
248
270
|
}
|
|
249
271
|
|
|
250
272
|
export const useStoryStore = create<StoryState>()(
|
|
@@ -260,19 +282,13 @@ export const useStoryStore = create<StoryState>()(
|
|
|
260
282
|
renderCounts: {},
|
|
261
283
|
knownSaves: {},
|
|
262
284
|
playthroughId: '',
|
|
263
|
-
initCount: 0,
|
|
264
285
|
maxHistory: 40,
|
|
265
286
|
saveError: null,
|
|
266
287
|
loadError: null,
|
|
267
288
|
transitionConfig: null,
|
|
268
289
|
nextTransition: null,
|
|
269
290
|
nobr: false,
|
|
270
|
-
|
|
271
|
-
bumpInitCount: () => {
|
|
272
|
-
set((state) => {
|
|
273
|
-
state.initCount++;
|
|
274
|
-
});
|
|
275
|
-
},
|
|
291
|
+
renderDeferred: false,
|
|
276
292
|
|
|
277
293
|
setMaxHistory: (limit: number) => {
|
|
278
294
|
set((state) => {
|
|
@@ -499,12 +515,13 @@ export const useStoryStore = create<StoryState>()(
|
|
|
499
515
|
state.historyIndex = 0;
|
|
500
516
|
state.visitCounts = { [startPassage.name]: 1 };
|
|
501
517
|
state.renderCounts = { [startPassage.name]: 1 };
|
|
518
|
+
state.renderDeferred = false;
|
|
502
519
|
});
|
|
503
520
|
|
|
504
521
|
lastNavigationVars = get().variables;
|
|
505
522
|
executeStoryInit();
|
|
506
|
-
get().bumpInitCount();
|
|
507
523
|
clearSession(storyData.ifid);
|
|
524
|
+
fireStoryInit();
|
|
508
525
|
|
|
509
526
|
// Start a new playthrough on restart
|
|
510
527
|
startNewPlaythrough(storyData.ifid)
|
|
@@ -765,5 +782,17 @@ export const useStoryStore = create<StoryState>()(
|
|
|
765
782
|
}
|
|
766
783
|
return current;
|
|
767
784
|
},
|
|
785
|
+
|
|
786
|
+
deferRender: () => {
|
|
787
|
+
set((state) => {
|
|
788
|
+
state.renderDeferred = true;
|
|
789
|
+
});
|
|
790
|
+
},
|
|
791
|
+
|
|
792
|
+
clearDeferredRender: () => {
|
|
793
|
+
set((state) => {
|
|
794
|
+
state.renderDeferred = false;
|
|
795
|
+
});
|
|
796
|
+
},
|
|
768
797
|
})),
|
|
769
798
|
);
|
package/src/story-api.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useStoryStore } from './store';
|
|
1
|
+
import { useStoryStore, onStoryInit } from './store';
|
|
2
2
|
import type { Passage } from './parser';
|
|
3
3
|
import { settings } from './settings';
|
|
4
4
|
import type {
|
|
@@ -50,6 +50,24 @@ import type { TransitionConfig } from './transition';
|
|
|
50
50
|
export type { StoryAction };
|
|
51
51
|
export type { MacroMetadata };
|
|
52
52
|
|
|
53
|
+
// Deferred-render promise lifecycle.
|
|
54
|
+
// deferRender() creates a promise; ready() resolves it.
|
|
55
|
+
// index.tsx reads getReadyPromise() AFTER render(), which is after both
|
|
56
|
+
// author JS and storyinit have run, so it always gets the final promise.
|
|
57
|
+
let readyResolve: (() => void) | null = null;
|
|
58
|
+
let readyPromise: Promise<void> | null = null;
|
|
59
|
+
|
|
60
|
+
/** Returns the current deferred-render promise, or null if not deferred. */
|
|
61
|
+
export function getReadyPromise(): Promise<void> | null {
|
|
62
|
+
return readyPromise;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Test-only: reset module-level promise state. */
|
|
66
|
+
export function _resetReadyState(): void {
|
|
67
|
+
readyResolve = null;
|
|
68
|
+
readyPromise = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
type NavigateCallback = (to: string, from: string) => void;
|
|
54
72
|
type StoryInitCallback = () => void;
|
|
55
73
|
type ActionsChangedCallback = () => void;
|
|
@@ -121,6 +139,8 @@ export interface StoryAPI {
|
|
|
121
139
|
setCSS(enabled: boolean): void;
|
|
122
140
|
setTransition(config: TransitionConfig | null): void;
|
|
123
141
|
setNextTransition(config: TransitionConfig | null): void;
|
|
142
|
+
deferRender(): void;
|
|
143
|
+
ready(): void;
|
|
124
144
|
random(): number;
|
|
125
145
|
randomInt(min: number, max: number): number;
|
|
126
146
|
readonly config: {
|
|
@@ -359,13 +379,7 @@ function createStoryAPI(): StoryAPI {
|
|
|
359
379
|
}
|
|
360
380
|
|
|
361
381
|
if (event === 'storyinit') {
|
|
362
|
-
|
|
363
|
-
return useStoryStore.subscribe((state) => {
|
|
364
|
-
if (state.initCount !== prevCount) {
|
|
365
|
-
prevCount = state.initCount;
|
|
366
|
-
(callback as StoryInitCallback)();
|
|
367
|
-
}
|
|
368
|
-
});
|
|
382
|
+
return onStoryInit(callback as StoryInitCallback);
|
|
369
383
|
}
|
|
370
384
|
|
|
371
385
|
if (event === 'actionsChanged') {
|
|
@@ -458,6 +472,21 @@ function createStoryAPI(): StoryAPI {
|
|
|
458
472
|
useStoryStore.getState().setNextTransition(config);
|
|
459
473
|
},
|
|
460
474
|
|
|
475
|
+
deferRender(): void {
|
|
476
|
+
useStoryStore.getState().deferRender();
|
|
477
|
+
readyPromise = new Promise<void>((resolve) => {
|
|
478
|
+
readyResolve = resolve;
|
|
479
|
+
});
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
ready(): void {
|
|
483
|
+
if (!readyResolve) return;
|
|
484
|
+
useStoryStore.getState().clearDeferredRender();
|
|
485
|
+
readyResolve();
|
|
486
|
+
readyResolve = null;
|
|
487
|
+
readyPromise = null;
|
|
488
|
+
},
|
|
489
|
+
|
|
461
490
|
random(): number {
|
|
462
491
|
return random();
|
|
463
492
|
},
|