@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 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 persist(next) {
27
- if (destroyed || replaying) {
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
- void progressStore.save(next).catch((error) => {
31
- input.onError?.(error);
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(pendingSnapshot);
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
- persist(progress);
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: () => progressStore.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
- persist(progress);
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 (destroyed || replaying) {
307
+ if (progress !== skippedProgress || replaying) {
196
308
  return;
197
309
  }
198
- try {
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(snapshot);
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;