@mpgd/tutorial 0.1.0 → 0.1.2
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/director.js +131 -21
- package/dist/driver.d.ts +1 -0
- package/dist/driver.js +1225 -107
- package/dist/platform-storage.js +14 -3
- package/dist/testing.js +9 -1
- package/package.json +2 -2
package/dist/director.js
CHANGED
|
@@ -16,22 +16,114 @@ export function createTutorialDirector(input) {
|
|
|
16
16
|
let currentScene = input.initialScene ?? definition.initialScene;
|
|
17
17
|
let publishing = false;
|
|
18
18
|
const pendingSnapshots = [];
|
|
19
|
+
const persistenceQueue = [];
|
|
20
|
+
let persistenceBusy = false;
|
|
21
|
+
let persistenceIdle = null;
|
|
22
|
+
let resolvePersistenceIdle = null;
|
|
23
|
+
let publicationVersion = 0;
|
|
24
|
+
let snapshot = createSnapshot(definition, progress, replaying, suspended, currentScene);
|
|
19
25
|
if (stored === null && progress !== null) {
|
|
20
|
-
persist(progress);
|
|
26
|
+
void persist(progress);
|
|
21
27
|
}
|
|
22
|
-
let snapshot = createSnapshot(definition, progress, replaying, suspended, currentScene);
|
|
23
28
|
function now() {
|
|
24
29
|
return clock().toISOString();
|
|
25
30
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
31
|
+
function reportError(error) {
|
|
32
|
+
try {
|
|
33
|
+
input.onError?.(error);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Error reporting must not interrupt tutorial state transitions.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function invokeProgressSave(next) {
|
|
40
|
+
try {
|
|
41
|
+
return progressStore.save(next).catch((error) => {
|
|
42
|
+
reportError(error);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
reportError(error);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function finishPersistenceCycle() {
|
|
51
|
+
const resolveIdle = resolvePersistenceIdle;
|
|
52
|
+
resolvePersistenceIdle = null;
|
|
53
|
+
persistenceIdle = null;
|
|
54
|
+
resolveIdle?.();
|
|
55
|
+
}
|
|
56
|
+
function pumpPersistence() {
|
|
57
|
+
if (persistenceBusy) {
|
|
28
58
|
return;
|
|
29
59
|
}
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
while (true) {
|
|
61
|
+
const pending = persistenceQueue.shift();
|
|
62
|
+
if (pending === undefined) {
|
|
63
|
+
finishPersistenceCycle();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
persistenceBusy = true;
|
|
67
|
+
const operation = invokeProgressSave(pending.progress);
|
|
68
|
+
if (operation === null) {
|
|
69
|
+
persistenceBusy = false;
|
|
70
|
+
pending.resolve();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
void operation.then(() => {
|
|
74
|
+
persistenceBusy = false;
|
|
75
|
+
pending.resolve();
|
|
76
|
+
pumpPersistence();
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function enqueuePersistence(next) {
|
|
82
|
+
const completion = new Promise((resolve) => {
|
|
83
|
+
persistenceQueue.push({ progress: next, resolve });
|
|
32
84
|
});
|
|
85
|
+
if (persistenceIdle === null) {
|
|
86
|
+
const idle = new Promise((resolve) => {
|
|
87
|
+
resolvePersistenceIdle = resolve;
|
|
88
|
+
});
|
|
89
|
+
void (persistenceIdle = idle);
|
|
90
|
+
}
|
|
91
|
+
pumpPersistence();
|
|
92
|
+
return completion;
|
|
93
|
+
}
|
|
94
|
+
function persist(next) {
|
|
95
|
+
if (destroyed || replaying) {
|
|
96
|
+
return Promise.resolve();
|
|
97
|
+
}
|
|
98
|
+
return enqueuePersistence(next);
|
|
99
|
+
}
|
|
100
|
+
async function flush() {
|
|
101
|
+
const pendingPersistence = persistenceIdle;
|
|
102
|
+
if (pendingPersistence !== null) {
|
|
103
|
+
await pendingPersistence;
|
|
104
|
+
}
|
|
105
|
+
await progressStore.flush();
|
|
106
|
+
}
|
|
107
|
+
function persistAndPublish(next) {
|
|
108
|
+
const versionBeforePersistence = publicationVersion;
|
|
109
|
+
void persist(next);
|
|
110
|
+
if (publicationVersion === versionBeforePersistence) {
|
|
111
|
+
publish();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function notifyListener(listener, nextSnapshot) {
|
|
115
|
+
try {
|
|
116
|
+
listener(nextSnapshot);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
reportError(error);
|
|
120
|
+
}
|
|
33
121
|
}
|
|
34
122
|
function publish() {
|
|
123
|
+
if (destroyed) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
publicationVersion += 1;
|
|
35
127
|
const publishedSnapshot = createSnapshot(definition, progress, replaying, suspended, currentScene);
|
|
36
128
|
pendingSnapshots.push(publishedSnapshot);
|
|
37
129
|
if (publishing) {
|
|
@@ -48,7 +140,7 @@ export function createTutorialDirector(input) {
|
|
|
48
140
|
const recipients = [...listeners];
|
|
49
141
|
for (const subscription of recipients) {
|
|
50
142
|
if (listeners.has(subscription)) {
|
|
51
|
-
subscription.listener
|
|
143
|
+
notifyListener(subscription.listener, pendingSnapshot);
|
|
52
144
|
}
|
|
53
145
|
}
|
|
54
146
|
}
|
|
@@ -99,44 +191,63 @@ export function createTutorialDirector(input) {
|
|
|
99
191
|
replayRestorePending = false;
|
|
100
192
|
durableBeforeReplay = null;
|
|
101
193
|
}
|
|
102
|
-
|
|
103
|
-
publish();
|
|
194
|
+
persistAndPublish(progress);
|
|
104
195
|
}
|
|
105
196
|
return {
|
|
106
197
|
acknowledge(stepId) {
|
|
198
|
+
if (destroyed) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
107
201
|
const step = getCurrentStep();
|
|
108
202
|
if (step?.id === stepId && step.advance.kind === 'acknowledge') {
|
|
109
203
|
advanceCurrent();
|
|
110
204
|
}
|
|
111
205
|
},
|
|
112
206
|
destroy() {
|
|
207
|
+
if (destroyed) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
113
210
|
destroyed = true;
|
|
114
211
|
listeners.clear();
|
|
212
|
+
pendingSnapshots.length = 0;
|
|
115
213
|
},
|
|
116
|
-
flush
|
|
214
|
+
flush,
|
|
117
215
|
getSnapshot: () => snapshot,
|
|
118
216
|
observeAction(action) {
|
|
217
|
+
if (destroyed) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
119
220
|
const step = getCurrentStep();
|
|
120
221
|
if (canAdvance(step) && step.advance.kind === 'action' && step.advance.action === action) {
|
|
121
222
|
advanceCurrent();
|
|
122
223
|
}
|
|
123
224
|
},
|
|
124
225
|
observeScene(scene) {
|
|
226
|
+
if (destroyed) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
125
229
|
currentScene = scene;
|
|
126
230
|
const step = getCurrentStep();
|
|
127
231
|
if (progress?.status === 'active' && step?.scene === scene && progress.checkpoint !== scene) {
|
|
128
232
|
progress = { ...progress, checkpoint: scene, updatedAt: now() };
|
|
129
|
-
|
|
233
|
+
persistAndPublish(progress);
|
|
234
|
+
return;
|
|
130
235
|
}
|
|
131
236
|
publish();
|
|
132
237
|
},
|
|
133
238
|
observeSignal(signal) {
|
|
239
|
+
if (destroyed) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
134
242
|
const step = getCurrentStep();
|
|
135
243
|
if (canAdvance(step) && step.advance.kind === 'signal' && step.advance.signal === signal) {
|
|
136
244
|
advanceCurrent();
|
|
137
245
|
}
|
|
138
246
|
},
|
|
139
247
|
async replay(options = {}) {
|
|
248
|
+
if (destroyed) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
140
251
|
const replayProgress = options.fromStepId === undefined
|
|
141
252
|
? createInitialTutorialProgress(definition, now())
|
|
142
253
|
: createTutorialProgressAtStep(definition, options.fromStepId, now());
|
|
@@ -152,14 +263,14 @@ export function createTutorialDirector(input) {
|
|
|
152
263
|
publish();
|
|
153
264
|
},
|
|
154
265
|
setSuspended(nextSuspended) {
|
|
155
|
-
if (suspended === nextSuspended) {
|
|
266
|
+
if (destroyed || suspended === nextSuspended) {
|
|
156
267
|
return;
|
|
157
268
|
}
|
|
158
269
|
suspended = nextSuspended;
|
|
159
270
|
publish();
|
|
160
271
|
},
|
|
161
272
|
async skip() {
|
|
162
|
-
if (progress === null) {
|
|
273
|
+
if (destroyed || progress === null) {
|
|
163
274
|
return;
|
|
164
275
|
}
|
|
165
276
|
if (replaying && replayRestorePending) {
|
|
@@ -182,6 +293,7 @@ export function createTutorialDirector(input) {
|
|
|
182
293
|
status: 'skipped',
|
|
183
294
|
updatedAt: timestamp,
|
|
184
295
|
};
|
|
296
|
+
const skippedProgress = progress;
|
|
185
297
|
replaying = false;
|
|
186
298
|
durableBeforeReplay = null;
|
|
187
299
|
replayRestorePending = false;
|
|
@@ -192,20 +304,18 @@ export function createTutorialDirector(input) {
|
|
|
192
304
|
finally {
|
|
193
305
|
durableFallbackDuringPublish = undefined;
|
|
194
306
|
}
|
|
195
|
-
if (
|
|
307
|
+
if (progress !== skippedProgress || replaying) {
|
|
196
308
|
return;
|
|
197
309
|
}
|
|
198
|
-
|
|
199
|
-
await progressStore.save(progress);
|
|
200
|
-
}
|
|
201
|
-
catch (error) {
|
|
202
|
-
input.onError?.(error);
|
|
203
|
-
}
|
|
310
|
+
await enqueuePersistence(skippedProgress);
|
|
204
311
|
},
|
|
205
312
|
subscribe(listener) {
|
|
313
|
+
if (destroyed) {
|
|
314
|
+
return () => undefined;
|
|
315
|
+
}
|
|
206
316
|
const subscription = { listener };
|
|
207
317
|
listeners.add(subscription);
|
|
208
|
-
listener
|
|
318
|
+
notifyListener(listener, snapshot);
|
|
209
319
|
return () => listeners.delete(subscription);
|
|
210
320
|
},
|
|
211
321
|
};
|
package/dist/driver.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface DriverTutorialPresenter<TStep extends TutorialStep = TutorialSt
|
|
|
35
35
|
}
|
|
36
36
|
export declare function createDriverTutorialPresenter<TStep extends TutorialStep>(input: DriverTutorialPresenterInput<TStep>): DriverTutorialPresenter<TStep>;
|
|
37
37
|
export declare function resolveVisibleTutorialTarget(input: {
|
|
38
|
+
readonly preferActivatable?: boolean;
|
|
38
39
|
readonly root?: Document | HTMLElement;
|
|
39
40
|
readonly target: string;
|
|
40
41
|
readonly targetAttribute?: string;
|