@docentjs/core 0.2.0 → 0.3.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.d.cts CHANGED
@@ -610,6 +610,12 @@ interface DocentOptions {
610
610
  /** Builds a platform controller for a tour. The DOM package supplies this. */
611
611
  createController: (tour: Tour, options: SharedControllerOptions) => TourController;
612
612
  now?: () => number;
613
+ /**
614
+ * Start watching triggers right away. Default true. Framework bindings pass
615
+ * `false` and call `connect()` / `disconnect()` from their mount lifecycle,
616
+ * which keeps construction free of side effects (React StrictMode).
617
+ */
618
+ connect?: boolean;
613
619
  }
614
620
  interface DocentState {
615
621
  /** Id of the tour currently running or paused, if any. */
@@ -623,8 +629,6 @@ interface StartOptions {
623
629
  at?: number | string;
624
630
  }
625
631
  export declare class Docent {
626
- /** Resolves once tours and progress are loaded and triggers are armed. */
627
- readonly ready: Promise<void>;
628
632
  private readonly options;
629
633
  private readonly env;
630
634
  private readonly baseStorage;
@@ -642,8 +646,30 @@ export declare class Docent {
642
646
  private readonly cleanups;
643
647
  private readonly timers;
644
648
  private readonly listeners;
649
+ private readonly eventListeners;
645
650
  private destroyed;
651
+ private loading;
652
+ /** Wanted by the owner (between connect and disconnect). */
653
+ private connected;
654
+ /** Listening to routes, the source and triggers. */
655
+ private attached;
646
656
  constructor(options: DocentOptions);
657
+ /** Resolves once tours and progress are loaded. Loading starts on first use. */
658
+ get ready(): Promise<void>;
659
+ /** Start watching routes, the tour source and triggers. Idempotent. */
660
+ connect(): void;
661
+ /**
662
+ * Stop watching and remove any running tour without recording an outcome.
663
+ * `connect()` resumes. Unlike `destroy()`, the manager stays usable.
664
+ */
665
+ disconnect(): Promise<void>;
666
+ /** The tours currently managed. */
667
+ getTours(): Tour[];
668
+ /**
669
+ * Observe every lifecycle event from every tour, in addition to the `sink`
670
+ * option. Returns an unsubscribe function. Used by devtools.
671
+ */
672
+ onEvent(listener: (event: DocentEvent) => void): () => void;
647
673
  getState(): DocentState;
648
674
  subscribe(listener: DocentListener): () => void;
649
675
  /** The controller of the running tour, for fine-grained control. */
@@ -677,7 +703,8 @@ export declare class Docent {
677
703
  /** Stop the running tour (recorded as skipped). */
678
704
  stop(): Promise<void>;
679
705
  destroy(): Promise<void>;
680
- private init;
706
+ private load;
707
+ private attach;
681
708
  private setTours;
682
709
  private loadRecords;
683
710
  private disarmTriggers;
@@ -693,7 +720,8 @@ export declare class Docent {
693
720
  private fire;
694
721
  /** Route triggers are only valid while the user is still on a matching route. */
695
722
  private triggerStillHolds;
696
- private conditionEnv;
723
+ /** What conditions are evaluated against right now. Used by `isEligible` and devtools. */
724
+ getConditionEnv(): ConditionEnv;
697
725
  private sharedOptions;
698
726
  private run;
699
727
  /**
package/dist/index.d.ts CHANGED
@@ -610,6 +610,12 @@ interface DocentOptions {
610
610
  /** Builds a platform controller for a tour. The DOM package supplies this. */
611
611
  createController: (tour: Tour, options: SharedControllerOptions) => TourController;
612
612
  now?: () => number;
613
+ /**
614
+ * Start watching triggers right away. Default true. Framework bindings pass
615
+ * `false` and call `connect()` / `disconnect()` from their mount lifecycle,
616
+ * which keeps construction free of side effects (React StrictMode).
617
+ */
618
+ connect?: boolean;
613
619
  }
614
620
  interface DocentState {
615
621
  /** Id of the tour currently running or paused, if any. */
@@ -623,8 +629,6 @@ interface StartOptions {
623
629
  at?: number | string;
624
630
  }
625
631
  export declare class Docent {
626
- /** Resolves once tours and progress are loaded and triggers are armed. */
627
- readonly ready: Promise<void>;
628
632
  private readonly options;
629
633
  private readonly env;
630
634
  private readonly baseStorage;
@@ -642,8 +646,30 @@ export declare class Docent {
642
646
  private readonly cleanups;
643
647
  private readonly timers;
644
648
  private readonly listeners;
649
+ private readonly eventListeners;
645
650
  private destroyed;
651
+ private loading;
652
+ /** Wanted by the owner (between connect and disconnect). */
653
+ private connected;
654
+ /** Listening to routes, the source and triggers. */
655
+ private attached;
646
656
  constructor(options: DocentOptions);
657
+ /** Resolves once tours and progress are loaded. Loading starts on first use. */
658
+ get ready(): Promise<void>;
659
+ /** Start watching routes, the tour source and triggers. Idempotent. */
660
+ connect(): void;
661
+ /**
662
+ * Stop watching and remove any running tour without recording an outcome.
663
+ * `connect()` resumes. Unlike `destroy()`, the manager stays usable.
664
+ */
665
+ disconnect(): Promise<void>;
666
+ /** The tours currently managed. */
667
+ getTours(): Tour[];
668
+ /**
669
+ * Observe every lifecycle event from every tour, in addition to the `sink`
670
+ * option. Returns an unsubscribe function. Used by devtools.
671
+ */
672
+ onEvent(listener: (event: DocentEvent) => void): () => void;
647
673
  getState(): DocentState;
648
674
  subscribe(listener: DocentListener): () => void;
649
675
  /** The controller of the running tour, for fine-grained control. */
@@ -677,7 +703,8 @@ export declare class Docent {
677
703
  /** Stop the running tour (recorded as skipped). */
678
704
  stop(): Promise<void>;
679
705
  destroy(): Promise<void>;
680
- private init;
706
+ private load;
707
+ private attach;
681
708
  private setTours;
682
709
  private loadRecords;
683
710
  private disarmTriggers;
@@ -693,7 +720,8 @@ export declare class Docent {
693
720
  private fire;
694
721
  /** Route triggers are only valid while the user is still on a matching route. */
695
722
  private triggerStillHolds;
696
- private conditionEnv;
723
+ /** What conditions are evaluated against right now. Used by `isEligible` and devtools. */
724
+ getConditionEnv(): ConditionEnv;
697
725
  private sharedOptions;
698
726
  private run;
699
727
  /**
package/dist/index.js CHANGED
@@ -467,8 +467,9 @@ var TourController = class {
467
467
  async destroy() {
468
468
  this.cancelPending();
469
469
  this.listeners.clear();
470
- await this.renderer.hide();
470
+ const hidden = this.renderer.hide();
471
471
  this.state = IDLE_STATE;
472
+ await hidden;
472
473
  }
473
474
  isActive() {
474
475
  return this.state.status === "running" || this.state.status === "paused";
@@ -691,8 +692,6 @@ function isTourSource(value) {
691
692
  return !!value && !Array.isArray(value) && typeof value.load === "function";
692
693
  }
693
694
  var Docent = class {
694
- /** Resolves once tours and progress are loaded and triggers are armed. */
695
- ready;
696
695
  options;
697
696
  env;
698
697
  baseStorage;
@@ -710,14 +709,61 @@ var Docent = class {
710
709
  cleanups = [];
711
710
  timers = /* @__PURE__ */ new Set();
712
711
  listeners = /* @__PURE__ */ new Set();
712
+ eventListeners = /* @__PURE__ */ new Set();
713
713
  destroyed = false;
714
+ loading;
715
+ /** Wanted by the owner (between connect and disconnect). */
716
+ connected = false;
717
+ /** Listening to routes, the source and triggers. */
718
+ attached = false;
714
719
  constructor(options) {
715
720
  this.options = options;
716
721
  this.env = options.environment;
717
722
  this.identity = options.identity ?? ANONYMOUS_IDENTITY;
718
723
  this.baseStorage = options.storage ?? createMemoryStorage();
719
724
  this.store = new ProgressStore(scopeStorage(this.baseStorage, this.identity.id));
720
- this.ready = this.init();
725
+ if (options.connect !== false) this.connect();
726
+ }
727
+ /** Resolves once tours and progress are loaded. Loading starts on first use. */
728
+ get ready() {
729
+ this.loading ??= this.load();
730
+ return this.loading;
731
+ }
732
+ /** Start watching routes, the tour source and triggers. Idempotent. */
733
+ connect() {
734
+ if (this.destroyed || this.connected) return;
735
+ this.connected = true;
736
+ this.ready.then(() => {
737
+ if (this.connected && !this.attached && !this.destroyed) this.attach();
738
+ });
739
+ }
740
+ /**
741
+ * Stop watching and remove any running tour without recording an outcome.
742
+ * `connect()` resumes. Unlike `destroy()`, the manager stays usable.
743
+ */
744
+ async disconnect() {
745
+ this.connected = false;
746
+ if (this.attached) {
747
+ this.attached = false;
748
+ this.disarmTriggers();
749
+ for (const c of this.cleanups) c();
750
+ this.cleanups.length = 0;
751
+ }
752
+ this.queue = [];
753
+ await this.stopActive();
754
+ this.emitState();
755
+ }
756
+ /** The tours currently managed. */
757
+ getTours() {
758
+ return [...this.tours.values()];
759
+ }
760
+ /**
761
+ * Observe every lifecycle event from every tour, in addition to the `sink`
762
+ * option. Returns an unsubscribe function. Used by devtools.
763
+ */
764
+ onEvent(listener) {
765
+ this.eventListeners.add(listener);
766
+ return () => this.eventListeners.delete(listener);
721
767
  }
722
768
  getState() {
723
769
  return {
@@ -778,7 +824,7 @@ var Docent = class {
778
824
  isEligible(tourId) {
779
825
  const tour = this.tours.get(tourId);
780
826
  if (!tour) return false;
781
- return shouldShow(tour, this.records.get(tourId) ?? null) && evaluateAll(tour.conditions, this.conditionEnv());
827
+ return shouldShow(tour, this.records.get(tourId) ?? null) && evaluateAll(tour.conditions, this.getConditionEnv());
782
828
  }
783
829
  /** Progress state of a tour for the current user. */
784
830
  tourState(tourId) {
@@ -813,22 +859,20 @@ var Docent = class {
813
859
  await this.controller?.skip();
814
860
  }
815
861
  async destroy() {
862
+ await this.disconnect();
816
863
  this.destroyed = true;
817
- this.disarmTriggers();
818
- for (const c of this.cleanups) c();
819
- this.cleanups.length = 0;
820
- const controller = this.controller;
821
- this.controller = void 0;
822
- this.activeId = null;
823
- await controller?.destroy();
824
864
  this.listeners.clear();
865
+ this.eventListeners.clear();
825
866
  }
826
- async init() {
867
+ async load() {
827
868
  const source = this.options.tours;
828
869
  const initial = isTourSource(source) ? await source.load() : source ?? [];
829
870
  this.setTours(initial);
830
871
  await this.loadRecords();
831
- if (this.destroyed) return;
872
+ }
873
+ attach() {
874
+ this.attached = true;
875
+ const source = this.options.tours;
832
876
  if (isTourSource(source) && source.subscribe) this.cleanups.push(source.subscribe((tours) => {
833
877
  this.setTours(tours);
834
878
  this.loadRecords().then(() => this.armTriggers());
@@ -857,7 +901,7 @@ var Docent = class {
857
901
  * `except` skips one tour, used for the tour that just finished.
858
902
  */
859
903
  armTriggers(except) {
860
- if (this.destroyed) return;
904
+ if (this.destroyed || !this.attached) return;
861
905
  this.disarmTriggers();
862
906
  for (const tour of this.tours.values()) {
863
907
  const trigger = tour.trigger;
@@ -899,7 +943,7 @@ var Docent = class {
899
943
  }
900
944
  /** A trigger fired: start the tour if eligible, or queue it behind the running one. */
901
945
  fire(tourId) {
902
- if (this.destroyed || tourId === this.activeId) return;
946
+ if (this.destroyed || !this.attached || tourId === this.activeId) return;
903
947
  if (!this.isEligible(tourId) || !this.triggerStillHolds(tourId)) return;
904
948
  if (this.tours.get(tourId)?.trigger?.type === "auto") this.autoFired.add(tourId);
905
949
  if (this.activeId) {
@@ -916,7 +960,8 @@ var Docent = class {
916
960
  const route = this.env.currentRoute?.();
917
961
  return route !== void 0 && matchRoute(trigger.pattern, route);
918
962
  }
919
- conditionEnv() {
963
+ /** What conditions are evaluated against right now. Used by `isEligible` and devtools. */
964
+ getConditionEnv() {
920
965
  const env = {
921
966
  identity: this.identity,
922
967
  elementExists: (t) => this.env.hasTarget(t),
@@ -933,7 +978,10 @@ var Docent = class {
933
978
  storage: scopeStorage(this.baseStorage, this.identity.id),
934
979
  tourState: (id) => this.tourState(id)
935
980
  };
936
- if (this.options.sink) shared.sink = this.options.sink;
981
+ shared.sink = { emit: (event) => {
982
+ this.options.sink?.emit(event);
983
+ for (const l of this.eventListeners) l(event);
984
+ } };
937
985
  const hooks = this.options.hooks?.[tour.id];
938
986
  if (hooks) shared.hooks = hooks;
939
987
  if (this.options.custom) shared.custom = this.options.custom;
@@ -945,6 +993,12 @@ var Docent = class {
945
993
  const controller = this.options.createController(tour, this.sharedOptions(tour));
946
994
  this.controller = controller;
947
995
  this.activeId = tour.id;
996
+ this.records.set(tour.id, {
997
+ tourId: tour.id,
998
+ version: tourVersion(tour),
999
+ state: "in-progress",
1000
+ updatedAt: (this.options.now ?? Date.now)()
1001
+ });
948
1002
  this.emitState();
949
1003
  const off = controller.subscribe((state) => {
950
1004
  if (state.status === "completed" || state.status === "skipped" || state.status === "aborted") {