@docentjs/core 0.1.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/index.js ADDED
@@ -0,0 +1,670 @@
1
+ //#region src/schema/tour.ts
2
+ /**
3
+ * Tour schema — the JSON contract shared by the core engine, every renderer,
4
+ * the visual builder and the hosted service.
5
+ *
6
+ * Everything in this file must stay JSON-serialisable. Functions (hooks) live
7
+ * in `hooks.ts` and are attached at runtime, keyed by step id.
8
+ */
9
+ /** Current schema version. Bump only on breaking changes to this file. */
10
+ const SCHEMA_VERSION = 1;
11
+ //#endregion
12
+ //#region src/define.ts
13
+ /**
14
+ * Identity helper that gives hand-written tours full type inference and
15
+ * fills in the schema version. Returns the same object.
16
+ */
17
+ function defineTour(tour) {
18
+ return {
19
+ ...tour,
20
+ schemaVersion: 1
21
+ };
22
+ }
23
+ //#endregion
24
+ //#region src/engine/route.ts
25
+ /**
26
+ * Route pattern matching for `route` triggers, conditions and step routes.
27
+ *
28
+ * Patterns are path globs:
29
+ * - `/settings` exact
30
+ * - `/users/:id` one segment (named for readability, value ignored)
31
+ * - `/users/*` one segment
32
+ * - `/docs/**` zero or more segments
33
+ *
34
+ * Query strings and hashes are ignored. Trailing slashes are tolerated.
35
+ */
36
+ function segments(path) {
37
+ return (path.split(/[?#]/, 1)[0] ?? "").split("/").filter(Boolean);
38
+ }
39
+ function matchSegments(pattern, path, pi = 0, si = 0) {
40
+ if (pi === pattern.length) return si === path.length;
41
+ const p = pattern[pi];
42
+ if (p === "**") {
43
+ for (let k = si; k <= path.length; k++) if (matchSegments(pattern, path, pi + 1, k)) return true;
44
+ return false;
45
+ }
46
+ if (si === path.length) return false;
47
+ if (p === "*" || p?.startsWith(":") || p === path[si]) return matchSegments(pattern, path, pi + 1, si + 1);
48
+ return false;
49
+ }
50
+ function matchRoute(pattern, path) {
51
+ return matchSegments(segments(pattern), segments(path));
52
+ }
53
+ //#endregion
54
+ //#region src/engine/conditions.ts
55
+ function sameValue(a, b) {
56
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => v === b[i]);
57
+ return a === b;
58
+ }
59
+ function compare(a, b) {
60
+ if (typeof a === "number" && typeof b === "number") return a - b;
61
+ if (typeof a === "string" && typeof b === "string") return a < b ? -1 : a > b ? 1 : 0;
62
+ return null;
63
+ }
64
+ function contains(haystack, needle) {
65
+ if (Array.isArray(haystack)) return typeof needle === "string" && haystack.includes(needle);
66
+ if (typeof haystack === "string") return (typeof needle === "string" || typeof needle === "number") && haystack.includes(String(needle));
67
+ return false;
68
+ }
69
+ function inList(value, list) {
70
+ if (!Array.isArray(list)) return false;
71
+ if (Array.isArray(value)) return value.some((v) => list.includes(v));
72
+ return typeof value === "string" && list.includes(value);
73
+ }
74
+ function evaluateTrait(actual, op, expected) {
75
+ switch (op) {
76
+ case "exists": return actual !== void 0 && actual !== null;
77
+ case "missing": return actual === void 0 || actual === null;
78
+ case "eq": return sameValue(actual, expected);
79
+ case "neq": return !sameValue(actual, expected);
80
+ case "in": return inList(actual, expected);
81
+ case "nin": return !inList(actual, expected);
82
+ case "contains": return contains(actual, expected);
83
+ case "gt":
84
+ case "gte":
85
+ case "lt":
86
+ case "lte": {
87
+ const c = compare(actual, expected);
88
+ if (c === null) return false;
89
+ if (op === "gt") return c > 0;
90
+ if (op === "gte") return c >= 0;
91
+ if (op === "lt") return c < 0;
92
+ return c <= 0;
93
+ }
94
+ }
95
+ }
96
+ function evaluateCondition(condition, env) {
97
+ switch (condition.type) {
98
+ case "trait": return evaluateTrait(env.identity.traits[condition.key], condition.op, condition.value);
99
+ case "route": return env.route !== void 0 && matchRoute(condition.pattern, env.route);
100
+ case "element": return (env.elementExists?.(condition.target) ?? false) === (condition.exists ?? true);
101
+ case "tour": return (env.tourState?.(condition.id) ?? "not-started") === condition.state;
102
+ case "all": return condition.conditions.every((c) => evaluateCondition(c, env));
103
+ case "any": return condition.conditions.some((c) => evaluateCondition(c, env));
104
+ case "not": return !evaluateCondition(condition.condition, env);
105
+ case "custom": return env.custom?.[condition.name]?.(condition.args) ?? false;
106
+ }
107
+ }
108
+ /** All conditions must hold. An empty or missing list holds. */
109
+ function evaluateAll(conditions, env) {
110
+ return (conditions ?? []).every((c) => evaluateCondition(c, env));
111
+ }
112
+ //#endregion
113
+ //#region src/seams.ts
114
+ const ANONYMOUS_IDENTITY = { traits: {} };
115
+ //#endregion
116
+ //#region src/engine/progress.ts
117
+ const STORAGE_PREFIX = "docent:";
118
+ function storageKey(tourId) {
119
+ return `${STORAGE_PREFIX}${tourId}`;
120
+ }
121
+ function tourVersion(tour) {
122
+ return tour.version ?? 1;
123
+ }
124
+ /** Whether a tour should be offered given what the user has already done. */
125
+ function shouldShow(tour, record) {
126
+ if (!record) return true;
127
+ if (record.version < tourVersion(tour)) return true;
128
+ switch (tour.options?.frequency ?? "once") {
129
+ case "always": return true;
130
+ case "until-completed": return record.state !== "completed";
131
+ case "once": return record.state === "not-started" || record.state === "in-progress";
132
+ }
133
+ }
134
+ var ProgressStore = class {
135
+ storage;
136
+ constructor(storage) {
137
+ this.storage = storage;
138
+ }
139
+ async get(tourId) {
140
+ const raw = await this.storage.get(storageKey(tourId));
141
+ if (!raw) return null;
142
+ try {
143
+ const parsed = JSON.parse(raw);
144
+ if (parsed.tourId !== tourId || typeof parsed.version !== "number" || !parsed.state) return null;
145
+ return parsed;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ async set(record) {
151
+ await this.storage.set(storageKey(record.tourId), JSON.stringify(record));
152
+ }
153
+ async clear(tourId) {
154
+ await this.storage.remove(storageKey(tourId));
155
+ }
156
+ };
157
+ /** In-memory adapter. Default when no storage is configured, and handy in tests. */
158
+ function createMemoryStorage() {
159
+ const map = /* @__PURE__ */ new Map();
160
+ return {
161
+ get: (key) => map.get(key) ?? null,
162
+ set: (key, value) => {
163
+ map.set(key, value);
164
+ },
165
+ remove: (key) => {
166
+ map.delete(key);
167
+ }
168
+ };
169
+ }
170
+ //#endregion
171
+ //#region src/engine/events.ts
172
+ function createEvent(type, input) {
173
+ const event = {
174
+ type,
175
+ tourId: input.tour.id,
176
+ tourVersion: tourVersion(input.tour),
177
+ timestamp: (input.now ?? Date.now)(),
178
+ identity: input.identity
179
+ };
180
+ if (input.stepIndex !== void 0 && input.stepIndex >= 0) {
181
+ const step = input.tour.steps[input.stepIndex];
182
+ event.stepIndex = input.stepIndex;
183
+ if (step) event.stepId = step.id;
184
+ }
185
+ return event;
186
+ }
187
+ /** Sink that drops everything. Default when none is configured. */
188
+ const NOOP_SINK = { emit() {} };
189
+ /** Fan out to several sinks. */
190
+ function combineSinks(...sinks) {
191
+ return { emit: (e) => {
192
+ for (const s of sinks) s.emit(e);
193
+ } };
194
+ }
195
+ //#endregion
196
+ //#region src/engine/reducer.ts
197
+ const IDLE_STATE = Object.freeze({
198
+ status: "idle",
199
+ index: -1,
200
+ history: []
201
+ });
202
+ function resolveStepIndex(tour, ref) {
203
+ if (typeof ref === "number") return ref >= 0 && ref < tour.steps.length ? ref : -1;
204
+ return tour.steps.findIndex((s) => s.id === ref);
205
+ }
206
+ /** First eligible index at or after `from` (or at or before, when `dir` is -1). -1 if none. */
207
+ function findEligible(ctx, from, dir = 1) {
208
+ const total = ctx.tour.steps.length;
209
+ for (let i = from; i >= 0 && i < total; i += dir) if (ctx.isEligible(i)) return i;
210
+ return -1;
211
+ }
212
+ function advance(state, ctx, record) {
213
+ const next = findEligible(ctx, state.index + 1);
214
+ if (next === -1) return {
215
+ status: "completed",
216
+ index: state.index,
217
+ history: state.history
218
+ };
219
+ return {
220
+ status: "running",
221
+ index: next,
222
+ history: record ? [...state.history, state.index] : state.history
223
+ };
224
+ }
225
+ function reduce(state, action, ctx) {
226
+ switch (action.type) {
227
+ case "start": {
228
+ const at = action.at === void 0 ? 0 : resolveStepIndex(ctx.tour, action.at);
229
+ if (at === -1) return {
230
+ status: "aborted",
231
+ index: -1,
232
+ history: [],
233
+ reason: "unknown-step"
234
+ };
235
+ const first = findEligible(ctx, at);
236
+ if (first === -1) return {
237
+ status: "aborted",
238
+ index: -1,
239
+ history: [],
240
+ reason: "no-eligible-steps"
241
+ };
242
+ return {
243
+ status: "running",
244
+ index: first,
245
+ history: []
246
+ };
247
+ }
248
+ case "next":
249
+ if (state.status !== "running") return state;
250
+ return advance(state, ctx, true);
251
+ case "stepMissing":
252
+ if (state.status !== "running") return state;
253
+ if (ctx.tour.steps[state.index]?.onMissing === "abort") return {
254
+ ...state,
255
+ status: "aborted",
256
+ reason: "target-missing"
257
+ };
258
+ return advance(state, ctx, false);
259
+ case "stepSkipped":
260
+ if (state.status !== "running") return state;
261
+ return advance(state, ctx, false);
262
+ case "back": {
263
+ if (state.status !== "running") return state;
264
+ const history = [...state.history];
265
+ let prev = history.pop();
266
+ while (prev !== void 0 && !ctx.isEligible(prev)) prev = history.pop();
267
+ if (prev === void 0) return state;
268
+ return {
269
+ status: "running",
270
+ index: prev,
271
+ history
272
+ };
273
+ }
274
+ case "go": {
275
+ if (state.status !== "running") return state;
276
+ const to = resolveStepIndex(ctx.tour, action.to);
277
+ if (to === -1 || to === state.index || !ctx.isEligible(to)) return state;
278
+ return {
279
+ status: "running",
280
+ index: to,
281
+ history: [...state.history, state.index]
282
+ };
283
+ }
284
+ case "pause":
285
+ if (state.status !== "running") return state;
286
+ return {
287
+ ...state,
288
+ status: "paused",
289
+ reason: action.reason
290
+ };
291
+ case "resume": {
292
+ if (state.status !== "paused") return state;
293
+ const { reason: _reason, ...rest } = state;
294
+ return {
295
+ ...rest,
296
+ status: "running"
297
+ };
298
+ }
299
+ case "skip":
300
+ if (state.status !== "running" && state.status !== "paused") return state;
301
+ return {
302
+ status: "skipped",
303
+ index: state.index,
304
+ history: state.history
305
+ };
306
+ case "complete":
307
+ if (state.status !== "running" && state.status !== "paused") return state;
308
+ return {
309
+ status: "completed",
310
+ index: state.index,
311
+ history: state.history
312
+ };
313
+ case "abort":
314
+ if (state.status !== "running" && state.status !== "paused") return state;
315
+ return {
316
+ ...state,
317
+ status: "aborted",
318
+ reason: action.reason
319
+ };
320
+ }
321
+ }
322
+ function isActive(state) {
323
+ return state.status === "running" || state.status === "paused";
324
+ }
325
+ function isFinished(state) {
326
+ return state.status === "completed" || state.status === "skipped" || state.status === "aborted";
327
+ }
328
+ function canGoBack(state, ctx) {
329
+ return state.status === "running" && state.history.some((i) => ctx.isEligible(i));
330
+ }
331
+ /** True when another eligible step follows. False on the last step. */
332
+ function hasNext(state, ctx) {
333
+ return state.status === "running" && findEligible(ctx, state.index + 1) !== -1;
334
+ }
335
+ /** Position over all steps, ineligible ones included, so numbers stay stable. */
336
+ function progress(state, ctx) {
337
+ return {
338
+ current: state.index + 1,
339
+ total: ctx.tour.steps.length
340
+ };
341
+ }
342
+ //#endregion
343
+ //#region src/engine/controller.ts
344
+ const DEFAULT_WAIT_MS = 3e3;
345
+ var TourController = class {
346
+ tour;
347
+ state = IDLE_STATE;
348
+ renderer;
349
+ identity;
350
+ store;
351
+ sink;
352
+ hooks;
353
+ custom;
354
+ tourStateOf;
355
+ defaultWaitMs;
356
+ now;
357
+ listeners = /* @__PURE__ */ new Set();
358
+ /** Bumped whenever an async flow must be abandoned. */
359
+ generation = 0;
360
+ pendingAbort;
361
+ pendingTimer;
362
+ constructor(options) {
363
+ this.tour = options.tour;
364
+ this.renderer = options.renderer;
365
+ this.identity = options.identity ?? ANONYMOUS_IDENTITY;
366
+ this.store = new ProgressStore(options.storage ?? createMemoryStorage());
367
+ this.sink = options.sink ?? NOOP_SINK;
368
+ this.hooks = options.hooks ?? {};
369
+ this.custom = options.custom ?? {};
370
+ this.tourStateOf = options.tourState;
371
+ this.defaultWaitMs = options.defaultWaitMs ?? DEFAULT_WAIT_MS;
372
+ this.now = options.now ?? Date.now;
373
+ }
374
+ getState() {
375
+ return this.state;
376
+ }
377
+ subscribe(listener) {
378
+ this.listeners.add(listener);
379
+ return () => this.listeners.delete(listener);
380
+ }
381
+ /** Start from the first eligible step, or from `at` (step id or index). */
382
+ async start(at) {
383
+ if (this.state.status === "running" || this.state.status === "paused") return;
384
+ this.cancelPending();
385
+ this.dispatch(at === void 0 ? { type: "start" } : {
386
+ type: "start",
387
+ at
388
+ });
389
+ if (isFinished(this.state)) return this.finish();
390
+ this.emit("tour:started");
391
+ this.hooks.onStart?.(this.tour);
392
+ await this.persist("in-progress");
393
+ await this.showCurrent();
394
+ }
395
+ /** Start where the user left off, according to persisted progress. */
396
+ async resume() {
397
+ const record = await this.store.get(this.tour.id);
398
+ if (record?.state === "in-progress" && record.version === tourVersion(this.tour)) return this.start(record.stepId);
399
+ return this.start();
400
+ }
401
+ async next() {
402
+ if (this.state.status !== "running") return;
403
+ await this.leaveCurrent();
404
+ this.emit("step:completed", this.state.index);
405
+ this.dispatch({ type: "next" });
406
+ await this.afterTransition();
407
+ }
408
+ async back() {
409
+ if (!canGoBack(this.state, this.context())) return;
410
+ await this.leaveCurrent();
411
+ this.dispatch({ type: "back" });
412
+ await this.afterTransition();
413
+ }
414
+ async goTo(step) {
415
+ if (this.state.status !== "running") return;
416
+ const before = this.state;
417
+ const after = reduce(before, {
418
+ type: "go",
419
+ to: step
420
+ }, this.context());
421
+ if (after === before) return;
422
+ await this.leaveCurrent();
423
+ this.setState(after);
424
+ await this.afterTransition();
425
+ }
426
+ /** The user gave up on the tour (Skip button, close, Escape). */
427
+ async skip() {
428
+ if (!this.isActive()) return;
429
+ const ctx = this.stepContext();
430
+ await this.leaveCurrent();
431
+ this.dispatch({ type: "skip" });
432
+ await this.finish();
433
+ if (ctx) this.hooks.onSkip?.(ctx);
434
+ }
435
+ async abort(reason) {
436
+ if (!this.isActive()) return;
437
+ await this.leaveCurrent();
438
+ this.dispatch({
439
+ type: "abort",
440
+ reason
441
+ });
442
+ await this.finish();
443
+ }
444
+ /** Report a named application event. Advances a step waiting on it. */
445
+ notify(eventName) {
446
+ const advance = this.currentStep()?.advance;
447
+ if (typeof advance === "object" && advance.on === "event" && advance.name === eventName) this.next();
448
+ }
449
+ /** Tell the controller the route changed. Pauses or resumes route-bound steps. */
450
+ async routeChanged() {
451
+ const step = this.currentStep();
452
+ if (!step) return;
453
+ const onRoute = this.stepOnRoute(step);
454
+ if (this.state.status === "paused" && this.state.reason === "route" && onRoute) {
455
+ this.dispatch({ type: "resume" });
456
+ await this.showCurrent();
457
+ } else if (this.state.status === "running" && !onRoute) {
458
+ this.cancelPending();
459
+ this.dispatch({
460
+ type: "pause",
461
+ reason: "route"
462
+ });
463
+ await this.renderer.hide();
464
+ }
465
+ }
466
+ /** Stop everything and clear the screen without recording an outcome. */
467
+ async destroy() {
468
+ this.cancelPending();
469
+ this.listeners.clear();
470
+ await this.renderer.hide();
471
+ this.state = IDLE_STATE;
472
+ }
473
+ isActive() {
474
+ return this.state.status === "running" || this.state.status === "paused";
475
+ }
476
+ currentStep() {
477
+ return this.isActive() ? this.tour.steps[this.state.index] : void 0;
478
+ }
479
+ conditionEnv() {
480
+ const env = {
481
+ identity: this.identity,
482
+ elementExists: (t) => this.renderer.hasTarget(t),
483
+ custom: this.custom
484
+ };
485
+ const route = this.renderer.currentRoute?.();
486
+ if (route !== void 0) env.route = route;
487
+ if (this.tourStateOf) env.tourState = this.tourStateOf;
488
+ return env;
489
+ }
490
+ context() {
491
+ const env = this.conditionEnv();
492
+ return {
493
+ tour: this.tour,
494
+ isEligible: (i) => {
495
+ const step = this.tour.steps[i];
496
+ if (!step) return false;
497
+ return step.condition ? evaluateCondition(step.condition, env) : true;
498
+ }
499
+ };
500
+ }
501
+ stepOnRoute(step) {
502
+ const route = this.renderer.currentRoute?.();
503
+ if (!step.route || route === void 0) return true;
504
+ return matchRoute(step.route, route);
505
+ }
506
+ dispatch(action) {
507
+ this.setState(reduce(this.state, action, this.context()));
508
+ }
509
+ setState(next) {
510
+ if (next === this.state) return;
511
+ this.state = next;
512
+ for (const l of this.listeners) l(next);
513
+ }
514
+ stepContext() {
515
+ const step = this.currentStep();
516
+ if (!step) return void 0;
517
+ return {
518
+ tour: this.tour,
519
+ step,
520
+ index: this.state.index,
521
+ total: this.tour.steps.length
522
+ };
523
+ }
524
+ emit(type, stepIndex) {
525
+ const input = {
526
+ tour: this.tour,
527
+ identity: this.identity,
528
+ now: this.now
529
+ };
530
+ if (stepIndex !== void 0) input.stepIndex = stepIndex;
531
+ this.sink.emit(createEvent(type, input));
532
+ }
533
+ async persist(state) {
534
+ const record = {
535
+ tourId: this.tour.id,
536
+ version: tourVersion(this.tour),
537
+ state,
538
+ updatedAt: this.now()
539
+ };
540
+ const step = this.currentStep();
541
+ if (state === "in-progress" && step) record.stepId = step.id;
542
+ await this.store.set(record);
543
+ }
544
+ cancelPending() {
545
+ this.generation++;
546
+ this.pendingAbort?.abort();
547
+ this.pendingAbort = void 0;
548
+ if (this.pendingTimer !== void 0) clearTimeout(this.pendingTimer);
549
+ this.pendingTimer = void 0;
550
+ }
551
+ /** Run `beforeHide` for the step being left, if any. */
552
+ async leaveCurrent() {
553
+ const ctx = this.stepContext();
554
+ this.cancelPending();
555
+ if (ctx && this.state.status === "running") await this.hooks.steps?.[ctx.step.id]?.beforeHide?.(ctx);
556
+ }
557
+ async afterTransition() {
558
+ if (this.state.status === "running") return this.showCurrent();
559
+ if (isFinished(this.state)) return this.finish();
560
+ }
561
+ async finish() {
562
+ this.cancelPending();
563
+ await this.renderer.hide();
564
+ switch (this.state.status) {
565
+ case "completed":
566
+ this.emit("tour:completed");
567
+ await this.persist("completed");
568
+ this.hooks.onComplete?.(this.tour);
569
+ break;
570
+ case "skipped":
571
+ this.emit("tour:skipped");
572
+ await this.persist("skipped");
573
+ break;
574
+ case "aborted":
575
+ this.emit("tour:aborted");
576
+ await this.persist("skipped");
577
+ this.hooks.onAbort?.(this.tour, this.state.reason ?? "unknown");
578
+ }
579
+ }
580
+ async showCurrent() {
581
+ this.cancelPending();
582
+ const generation = this.generation;
583
+ const stale = () => generation !== this.generation;
584
+ const ctx = this.stepContext();
585
+ if (!ctx) return;
586
+ const { step } = ctx;
587
+ if (!this.stepOnRoute(step)) {
588
+ this.dispatch({
589
+ type: "pause",
590
+ reason: "route"
591
+ });
592
+ await this.renderer.hide();
593
+ return;
594
+ }
595
+ const proceed = await this.hooks.steps?.[step.id]?.beforeShow?.(ctx);
596
+ if (stale()) return;
597
+ if (proceed === false) {
598
+ this.emit("step:skipped", this.state.index);
599
+ this.dispatch({ type: "stepSkipped" });
600
+ return this.afterTransition();
601
+ }
602
+ if (step.target !== void 0 && !await this.ensureTarget(step, generation)) {
603
+ if (stale()) return;
604
+ this.emit("step:missing", this.state.index);
605
+ this.dispatch({ type: "stepMissing" });
606
+ return this.afterTransition();
607
+ }
608
+ if (stale()) return;
609
+ await this.renderer.show(this.renderContext(ctx));
610
+ if (stale()) return;
611
+ this.emit("step:shown", this.state.index);
612
+ this.hooks.onStepChange?.(ctx);
613
+ await this.persist("in-progress");
614
+ await this.hooks.steps?.[step.id]?.afterShow?.(ctx);
615
+ if (stale()) return;
616
+ this.armAdvance(step, generation);
617
+ }
618
+ /** Resolve `true` when the target is present, waiting if the step allows it. */
619
+ async ensureTarget(step, generation) {
620
+ const target = step.target;
621
+ if (target === void 0) return true;
622
+ if (this.renderer.hasTarget(target)) return true;
623
+ const waitMs = step.waitFor ?? (step.onMissing === "wait" ? this.defaultWaitMs : 0);
624
+ if (waitMs <= 0) return false;
625
+ const abort = new AbortController();
626
+ this.pendingAbort = abort;
627
+ const found = await this.renderer.waitForTarget(target, waitMs, abort.signal);
628
+ if (generation !== this.generation) return false;
629
+ this.pendingAbort = void 0;
630
+ return found;
631
+ }
632
+ /** Set up automatic advancement for `delay` and `element` steps. */
633
+ armAdvance(step, generation) {
634
+ const advance = step.advance;
635
+ if (typeof advance !== "object") return;
636
+ if (advance.on === "delay") this.pendingTimer = setTimeout(() => {
637
+ this.pendingTimer = void 0;
638
+ if (generation === this.generation) this.next();
639
+ }, advance.ms);
640
+ else if (advance.on === "element") {
641
+ const abort = new AbortController();
642
+ this.pendingAbort = abort;
643
+ this.renderer.waitForTarget(advance.target, Number.POSITIVE_INFINITY, abort.signal).then((found) => {
644
+ if (found && generation === this.generation) this.next();
645
+ });
646
+ }
647
+ }
648
+ renderContext(ctx) {
649
+ const engineCtx = this.context();
650
+ return {
651
+ tour: this.tour,
652
+ step: ctx.step,
653
+ index: ctx.index,
654
+ progress: progress(this.state, engineCtx),
655
+ isFirst: this.state.history.length === 0,
656
+ isLast: !hasNext(this.state, engineCtx),
657
+ canGoBack: canGoBack(this.state, engineCtx),
658
+ actions: {
659
+ next: () => void this.next(),
660
+ back: () => void this.back(),
661
+ skip: () => void this.skip(),
662
+ goTo: (s) => void this.goTo(s)
663
+ }
664
+ };
665
+ }
666
+ };
667
+ //#endregion
668
+ export { ANONYMOUS_IDENTITY, IDLE_STATE, NOOP_SINK, ProgressStore, SCHEMA_VERSION, STORAGE_PREFIX, TourController, canGoBack, combineSinks, createEvent, createMemoryStorage, defineTour, evaluateAll, evaluateCondition, evaluateTrait, findEligible, hasNext, isActive, isFinished, matchRoute, progress, reduce, resolveStepIndex, shouldShow, storageKey, tourVersion };
669
+
670
+ //# sourceMappingURL=index.js.map