sia-reactor 0.0.21 → 0.0.22

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.
Files changed (38) hide show
  1. package/README.md +115 -89
  2. package/dist/{TimeTravelOverlay-CJv-S_Km.d.cts → TimeTravelOverlay-DiXUgbUU.d.cts} +9 -8
  3. package/dist/{TimeTravelOverlay-DxqJL0Zk.d.ts → TimeTravelOverlay-eWjAy0yr.d.ts} +9 -8
  4. package/dist/adapters/react.cjs +66 -84
  5. package/dist/adapters/react.d.cts +23 -22
  6. package/dist/adapters/react.d.ts +23 -22
  7. package/dist/adapters/react.js +3 -3
  8. package/dist/adapters/vanilla.cjs +66 -84
  9. package/dist/adapters/vanilla.d.cts +4 -4
  10. package/dist/adapters/vanilla.d.ts +4 -4
  11. package/dist/adapters/vanilla.js +3 -3
  12. package/dist/{chunk-2WBPGSRL.js → chunk-3SKLWTEA.js} +52 -70
  13. package/dist/{chunk-DP74DVRT.js → chunk-BTA6MIQ6.js} +40 -8
  14. package/dist/{chunk-TFLYCXK4.js → chunk-CS3FOV6J.js} +14 -13
  15. package/dist/{index-Oie9hhE8.d.cts → index-BgbbNXTW.d.cts} +306 -207
  16. package/dist/{index-Oie9hhE8.d.ts → index-BgbbNXTW.d.ts} +306 -207
  17. package/dist/index.cjs +54 -75
  18. package/dist/index.d.cts +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +2 -4
  21. package/dist/{plugins.cjs → modules.cjs} +437 -166
  22. package/dist/modules.d.cts +53 -0
  23. package/dist/modules.d.ts +53 -0
  24. package/dist/modules.js +627 -0
  25. package/dist/super.d.ts +610 -281
  26. package/dist/super.global.js +445 -174
  27. package/dist/timeTravel-CraHdbXZ.d.cts +352 -0
  28. package/dist/timeTravel-YUxRHRgh.d.ts +352 -0
  29. package/dist/utils.cjs +41 -7
  30. package/dist/utils.d.cts +1 -1
  31. package/dist/utils.d.ts +1 -1
  32. package/dist/utils.js +3 -1
  33. package/package.json +6 -6
  34. package/dist/plugins.d.cts +0 -112
  35. package/dist/plugins.d.ts +0 -112
  36. package/dist/plugins.js +0 -370
  37. package/dist/timeTravel-B1vedDQc.d.ts +0 -76
  38. package/dist/timeTravel-WpgWmKu-.d.cts +0 -76
@@ -23,7 +23,6 @@ var sia = (() => {
23
23
  __export(super_exports, {
24
24
  CTX: () => CTX,
25
25
  EVT_OPTS: () => EVT_OPTS,
26
- EVT_WARN: () => EVT_WARN,
27
26
  INDIFFABLE: () => INDIFFABLE,
28
27
  INERTIA: () => INERTIA,
29
28
  NIL: () => NIL,
@@ -48,7 +47,7 @@ var sia = (() => {
48
47
  isVolatile: () => isVolatile,
49
48
  live: () => live,
50
49
  methods: () => methods,
51
- plugins: () => plugins_exports,
50
+ modules: () => modules_exports,
52
51
  reactive: () => reactive,
53
52
  stable: () => stable,
54
53
  state: () => state,
@@ -60,7 +59,9 @@ var sia = (() => {
60
59
  var CTX = {
61
60
  /** Flag indicating whether the application is running in development mode. */
62
61
  isDevEnv: "undefined" !== typeof process ? process.env.NODE_ENV !== "production" : true,
63
- /** active `Autotracker` instance, override for automatic dependency collection on `Reactor` traps. */
62
+ /** Flag indicating whether a cascade is currently ongoing so reactors can allow all writes. */
63
+ isCascading: false,
64
+ /** Active `Autotracker` instance, override for automatic dependency collection on `Reactor` traps. */
64
65
  autotracker: null
65
66
  };
66
67
  var RAW = /* @__PURE__ */ Symbol.for("S.I.A_RAW");
@@ -72,7 +73,6 @@ var sia = (() => {
72
73
  var SSVERSION = /* @__PURE__ */ Symbol.for("S.I.A_SNAPSHOT_VERSION");
73
74
  var RTR_BATCH = "undefined" !== typeof window ? ("undefined" !== typeof queueMicrotask ? queueMicrotask : setTimeout).bind(window) : "undefined" !== typeof process && process.nextTick ? process.nextTick : setTimeout;
74
75
  var RTR_LOG = console.log.bind(console, "[S.I.A Reactor]");
75
- var EVT_WARN = console.warn.bind(console, "[S.I.A Event]");
76
76
  var EVT_OPTS = { LISTENER: ["capture", "depth", "once", "signal", "immediate"], MEDIATOR: ["lazy", "signal", "immediate"] };
77
77
  var NIL = Object.freeze({});
78
78
  var NOOP = () => {
@@ -171,16 +171,43 @@ var sia = (() => {
171
171
  function parseAnyObj(obj, separator = ".", keyFunc = (p) => p, seen = /* @__PURE__ */ new WeakSet()) {
172
172
  if (!isObj(obj) || seen.has(obj)) return obj;
173
173
  seen.add(obj);
174
- const result = {};
175
- Object.keys(obj).forEach((k) => k === "*" || k.includes(separator) ? setAny(result, k, parseAnyObj(obj[k], separator, keyFunc, seen), separator, keyFunc) : result[k] = isObj(obj[k]) ? parseAnyObj(obj[k], separator, keyFunc, seen) : obj[k]);
174
+ const result = {}, keys2 = Object.keys(obj);
175
+ for (let i = 0, len = keys2.length; i < len; i++) {
176
+ const k = keys2[i];
177
+ k === "*" || k.includes(separator) ? setAny(result, k, parseAnyObj(obj[k], separator, keyFunc, seen), separator, keyFunc) : result[k] = isObj(obj[k]) ? parseAnyObj(obj[k], separator, keyFunc, seen) : obj[k];
178
+ }
176
179
  return result;
177
180
  }
178
181
  function parseEvtOpts(options, opts, boolOpt = opts[0], result = {}) {
179
182
  return Object.assign(result, "boolean" === typeof options ? { [boolOpt]: options } : options), result;
180
183
  }
184
+ function fanout(a, b, c, d) {
185
+ const isEvtPld = !!a?.target, [state2, path, news, olds, opts, type] = isEvtPld ? [a.root, a.currentTarget.path, a.currentTarget.value, a.currentTarget.oldValue, b || NIL, a.type] : [a, b, c, (d || NIL).merge ? getAny(a, b) : NIL, d || NIL], target = path === "*" ? state2 : getAny(state2, path);
186
+ if (isEvtPld && type !== "set" && type !== "delete" || !target || !canHandle(news, opts)) return;
187
+ const prev = CTX.isCascading;
188
+ CTX.isCascading = isEvtPld;
189
+ try {
190
+ const walk = (target2, obj, depth = 1, keys2 = Object.keys(obj)) => {
191
+ for (let i = 0, len = keys2.length; i < len; i++) {
192
+ const val = obj[keys2[i]];
193
+ try {
194
+ depth > 1 && canHandle(val, opts) ? walk(target2[keys2[i]] ||= {}, val, depth - 1) : target2[keys2[i]] = val;
195
+ } catch {
196
+ }
197
+ }
198
+ };
199
+ walk(target, opts.merge && canHandle(olds, opts) ? mergeObjs(olds, news) : news, opts.depth === true ? Infinity : +opts.depth);
200
+ } finally {
201
+ CTX.isCascading = prev;
202
+ }
203
+ }
181
204
  function mergeObjs(o1 = {}, o2 = {}) {
182
- const merged = { ...o1 || {}, ...o2 || {} };
183
- return Object.keys(merged).forEach((k) => isObj(o1?.[k]) && isObj(o2?.[k]) && (merged[k] = mergeObjs(o1[k], o2[k]))), merged;
205
+ const merged = { ...o1 ||= {}, ...o2 ||= {} }, keys2 = Object.keys(merged);
206
+ for (let i = 0, len = keys2.length; i < len; i++) {
207
+ const k = keys2[i];
208
+ if (isObj(o1[k]) && isObj(o2[k])) merged[k] = mergeObjs(o1[k], o2[k]);
209
+ }
210
+ return merged;
184
211
  }
185
212
  function getTrailRecords(obj, path, reverse = false) {
186
213
  const parts = path.split("."), chain = [["*", obj, obj]];
@@ -195,7 +222,11 @@ var sia = (() => {
195
222
  const clone = config.preserveContext ? Object.create(Object.getPrototypeOf(obj)) : Array.isArray(obj) ? [] : {};
196
223
  seen.set(obj, clone);
197
224
  const keys2 = config.preserveContext ? Reflect.ownKeys(obj) : Object.keys(obj);
198
- for (let i = 0, len = keys2.length; i < len; i++) clone[keys2[i]] = deepClone(obj[keys2[i]], config, seen);
225
+ for (let i = 0, len = keys2.length; i < len; i++)
226
+ try {
227
+ clone[keys2[i]] = deepClone(obj[keys2[i]], config, seen);
228
+ } catch {
229
+ }
199
230
  return clone;
200
231
  }
201
232
  function nuke(target) {
@@ -235,35 +266,34 @@ var sia = (() => {
235
266
  staticType;
236
267
  /** Original event target context. */
237
268
  target;
238
- /** Root reactive object for this event wave. */
269
+ /** Root reactive object for this event instance wave. */
239
270
  root;
240
- /** Original target path for this event wave. */
271
+ /** Original target path for this event instance wave. */
241
272
  path;
242
273
  /** Current value at the event target path. */
243
274
  value;
244
275
  /** Previous value at the event target path. */
245
276
  oldValue;
246
- /** Whether resolve/reject intent semantics are allowed for this event. */
277
+ /** Whether resolve/reject intent semantics are allowed for this event instance. */
247
278
  rejectable;
248
- /** Whether this event wave can bubble back up to ancestors or just capture down. */
279
+ /** Whether this event instance wave can bubble back up to ancestors or just capture down. */
249
280
  bubbles;
250
281
  /**
251
- * `DOMHighResTimeStamp` for this event payload for native event parity and accuracy.
282
+ * `DOMHighResTimeStamp` for this event instance payload for native event parity and accuracy.
252
283
  * Enable `eventTimeStamps` option, then use this over custom timestamps in listeners for accuracy.
253
284
  * */
254
285
  timestamp;
255
- _warn = NOOP;
286
+ /** The `Reactor` instance that dispatched this event instance. */
287
+ reactor;
256
288
  _resolved = "";
257
289
  _rejected = "";
258
290
  _propagationStopped = false;
259
291
  _immediatePropagationStopped = false;
260
292
  /**
261
293
  * @param payload Source payload for this event instance.
262
- * @param bubbles Whether bubbling is enabled for this wave.
263
- * @param canWarn Whether warning output is enabled.
264
- * @param canStamp Whether timestamping is enabled.
294
+ * @param reactor The `Reactor` instance creating this event instance.
265
295
  */
266
- constructor(payload, bubbles = false, canStamp = false, canWarn = true) {
296
+ constructor(payload, reactor) {
267
297
  this.staticType = this.type = payload.type;
268
298
  this.target = payload.target;
269
299
  this.currentTarget = payload.currentTarget;
@@ -272,9 +302,9 @@ var sia = (() => {
272
302
  this.value = payload.target.value;
273
303
  this.oldValue = payload.target.oldValue;
274
304
  this.rejectable = payload.rejectable;
275
- this.bubbles = bubbles;
276
- if (canStamp) this.timestamp = performance.now();
277
- if (canWarn) this._warn = EVT_WARN;
305
+ this.bubbles = !!reactor.config.eventBubbling;
306
+ if (reactor.config.eventTimeStamps) this.timestamp = performance.now();
307
+ this.reactor = reactor;
278
308
  }
279
309
  /** Whether propagation has been stopped. */
280
310
  get propagationStopped() {
@@ -304,9 +334,9 @@ var sia = (() => {
304
334
  * @example e.resolve("API Load successful"); // message
305
335
  */
306
336
  resolve(message) {
307
- if (!this.rejectable) return this._warn(`[ReactorEvent] Ignored resolve() call on a non-rejectable ${this.staticType} at "${this.path}"`);
308
- if (this.eventPhase !== _ReactorEvent.CAPTURING_PHASE) this._warn(`[ReactorEvent] Resolving an intent on ${this.staticType} at "${this.path}" outside of the capture phase is unadvised.`);
309
- if (this.rejectable) this._resolved = message || `Could ${this.staticType} intended value at "${this.path}"`;
337
+ if (!this.rejectable) return this.reactor.log(`[ReactorEvent] Ignored \`resolve()\` call on a non-rejectable ${this.staticType} at "${this.path}"`);
338
+ if (this.eventPhase !== _ReactorEvent.CAPTURING_PHASE) this.reactor.log(`[ReactorEvent] Resolving an intent on ${this.staticType} at "${this.path}" outside of the capture phase is unadvised.`);
339
+ if (this.rejectable) this.reactor.log(`[ReactorEvent] ${this._resolved = message || `Could ${this.staticType} intended value at "${this.path}"`}`);
310
340
  }
311
341
  /** Rejection reason for rejectable events. */
312
342
  get rejected() {
@@ -319,9 +349,9 @@ var sia = (() => {
319
349
  * @example e.resolve("User is not logged in"); // reason
320
350
  */
321
351
  reject(reason) {
322
- if (!this.rejectable) return this._warn(`[ReactorEvent] Ignored reject() call on a non-rejectable ${this.staticType} at "${this.path}"`);
323
- if (this.eventPhase !== _ReactorEvent.CAPTURING_PHASE) this._warn(`[ReactorEvent] Rejecting an intent on ${this.staticType} at "${this.path}" outside of the capture phase is unadvised.`);
324
- if (this.rejectable) this._rejected = reason || `Couldn't ${this.staticType} intended value at "${this.path}"`;
352
+ if (!this.rejectable) return this.reactor.log(`[ReactorEvent] Ignored \`reject()\` call on a non-rejectable ${this.staticType} at "${this.path}"`);
353
+ if (this.eventPhase !== _ReactorEvent.CAPTURING_PHASE) this.reactor.log(`[ReactorEvent] Rejecting an intent on ${this.staticType} at "${this.path}" outside of the capture phase is unadvised.`);
354
+ if (this.rejectable) this.reactor.log(`[ReactorEvent] ${this._rejected = reason || `Couldn't ${this.staticType} intended value at "${this.path}"`}`);
325
355
  }
326
356
  /**
327
357
  * Returns event path values from target to root.
@@ -330,24 +360,22 @@ var sia = (() => {
330
360
  composedPath() {
331
361
  return getTrailRecords(this.root, this.path, true).map((r) => r[2]);
332
362
  }
333
- get canWarn() {
334
- return this._warn !== NOOP;
335
- }
336
363
  };
337
364
 
338
365
  // src/ts/core/reactor.ts
339
366
  var Reactor = class {
367
+ /** Logger function for this reactor instance, override if desired, `this.canLog = false` resets. */
340
368
  log = NOOP;
369
+ /** The core state object for this reactor instance. */
341
370
  core;
342
371
  // `?:`s | pay the ~800 byte price upfront for what u might never use
343
- plugins;
372
+ /** The modules being used by this reactor. */
373
+ modules;
374
+ /** Configuration options for this reactor instance. */
344
375
  config;
376
+ /** Whether this reactor instance is currently batching updates, a window view into the engine timing */
345
377
  isBatching = false;
346
378
  // Async Batching
347
- isCascading = false;
348
- // Setter Cascading
349
- isLogging = false;
350
- // keeping track so API getter doesn't slow down internal iterations in any way
351
379
  queue;
352
380
  // Tasks to run after flush
353
381
  batch;
@@ -414,7 +442,7 @@ var sia = (() => {
414
442
  safeValue = value?.[RAW] || value;
415
443
  unchanged = this.config.equalityFunction(safeValue, safeOldValue);
416
444
  }
417
- if (!indiffable && unchanged && !this.isCascading) return this.log(`\u{1F504} [Reactor \`set\` Trap] Unchanged for "${keyStr}" on "${paths}"`), true;
445
+ if (!indiffable && unchanged && !CTX.isCascading) return this.log(`\u{1F504} [Reactor \`set\` Trap] Unchanged for "${keyStr}" on "${paths}"`), true;
418
446
  if (this.config.set) terminated = (value = this.config.set(object, key2, value, oldValue, receiver, paths)) === TERMINATOR;
419
447
  if (this.setters) {
420
448
  const wildcords = this.setters.get("*");
@@ -568,7 +596,7 @@ var sia = (() => {
568
596
  if (this.queue?.size) for (const task of this.queue) task(), this.queue.delete(task);
569
597
  }
570
598
  wave(path, payload) {
571
- const e = new ReactorEvent(payload, this.config.eventBubbling, this.config.eventTimeStamps, this.isLogging), chain = getTrailRecords(this.core, path);
599
+ const e = new ReactorEvent(payload, this), chain = getTrailRecords(this.core, path);
572
600
  e.eventPhase = ReactorEvent.CAPTURING_PHASE;
573
601
  for (let i = 0; i <= chain.length - 2; i++) {
574
602
  if (e.propagationStopped) break;
@@ -642,8 +670,8 @@ var sia = (() => {
642
670
  return depth;
643
671
  }
644
672
  getContext(path) {
645
- const lastDot = path.lastIndexOf("."), value = getAny(this.core, path), object = lastDot === -1 ? this.core : getAny(this.core, path.slice(0, lastDot));
646
- return { path, value, key: path.slice(lastDot + 1) || "", hadKey: true, object };
673
+ const last = path.lastIndexOf("."), value = getAny(this.core, path), object = last === -1 ? this.core : getAny(this.core, path.slice(0, last));
674
+ return { path, value, key: path.slice(last + 1) || "", hadKey: true, object };
647
675
  }
648
676
  bindSignal(cord, sig) {
649
677
  if (sig) sig.aborted ? cord.clup() : sig.addEventListener("abort", cord.clup, { once: true });
@@ -659,7 +687,11 @@ var sia = (() => {
659
687
  const clone = !raw ? this.config.preserveContext ? Object.create(Object.getPrototypeOf(obj)) : Array.isArray(obj) ? [] : {} : obj;
660
688
  seen.set(obj, clone);
661
689
  const keys2 = this.config.preserveContext ? Reflect.ownKeys(obj) : Object.keys(obj);
662
- for (let i = 0, len = keys2.length; i < len; i++) clone[keys2[i]] = this.cloned(obj[keys2[i]], raw, seen);
690
+ for (let i = 0, len = keys2.length; i < len; i++)
691
+ try {
692
+ clone[keys2[i]] = this.cloned(obj[keys2[i]], raw, seen);
693
+ } catch {
694
+ }
663
695
  if (!raw && this.config.smartCloning) this.snapCache.set(obj, clone), obj[SSVERSION] = version;
664
696
  return clone;
665
697
  }
@@ -747,7 +779,7 @@ var sia = (() => {
747
779
  * rtr.delete("cache.temp", () => TERMINATOR);
748
780
  */
749
781
  delete(path, callback, options) {
750
- return this.syncAdd("delete", path, callback, options, (imm) => (imm !== "auto" || inAny(this.core, path)) && deleteAny(this.core, path, void 0));
782
+ return this.syncAdd("delete", path, callback, options, (imm) => (imm !== "auto" || inAny(this.core, path)) && deleteAny(this.core, path));
751
783
  }
752
784
  /** Registers a delete mediator for a path that only triggers once. */
753
785
  donce(path, callback, options) {
@@ -764,7 +796,7 @@ var sia = (() => {
764
796
  }
765
797
  /**
766
798
  * Registers a watcher for a path.
767
- * Watch callbacks run synchronously with the operation.
799
+ * Watch callbacks run synchronously with the operation, use leaf paths for reliability as it sees exact sets; no bubbling here.
768
800
  * @param path Path or wildcard path.
769
801
  * @param callback Watch callback.
770
802
  * @param options Sync options.
@@ -813,7 +845,7 @@ var sia = (() => {
813
845
  cord = { cb: callback, capture, depth, once, clup: () => this.off(path, callback, options), lDepth: depth !== void 0 ? this.getDepth(path) : depth };
814
846
  if (immediate && (immediate !== "auto" || inAny(this.core, path))) {
815
847
  const target = this.getContext(path);
816
- callback(new ReactorEvent({ type: "init", target, currentTarget: target, root: this.core, rejectable: false }, this.config.eventBubbling, this.isLogging));
848
+ callback(new ReactorEvent({ type: "init", target, currentTarget: target, root: this.core, rejectable: false }, this));
817
849
  }
818
850
  (cords ?? (this.listeners.set(path, cords = []), cords)).push(cord);
819
851
  return this.bindSignal(cord, signal);
@@ -844,56 +876,39 @@ var sia = (() => {
844
876
  return this.cloned(arguments.length < 2 ? this.core : branch, raw);
845
877
  }
846
878
  /**
847
- * Cascades object updates into direct child paths.
848
- * @param payload Event or payload source.
849
- * @param objectSafe Merge old/new object values before cascading, don't set for arrays; merger doesn't play nice
850
- * @example
851
- * rtr.on("user", (event) => rtr.cascade(event));
852
- * @example
853
- * rtr.watch("user", (_value, payload) => rtr.cascade(payload));
854
- */
855
- cascade({ type, currentTarget: { path, value: news, oldValue: olds } }, objectSafe = true) {
856
- if (type !== "set" && type !== "delete" || !canHandle(news, this.config) || (objectSafe ? !canHandle(olds, this.config) : false)) return;
857
- const obj = objectSafe ? mergeObjs(olds, news) : news, keys2 = Object.keys(obj);
858
- this.isCascading = true;
859
- for (let i = 0, len = keys2.length; i < len; i++) setAny(this.core, path === "*" ? keys2[i] : path + "." + keys2[i], obj[keys2[i]]);
860
- this.isCascading = false;
861
- }
862
- /**
863
- * Installs a plugin instance.
864
- * @param plugin Plugin instance.
865
- * @returns Current reactor for fluent chaining.
879
+ * Installs a module instance.
880
+ * @param target Module instance.
881
+ * @param id Optional identification tag for this instance in the module.
882
+ * @returns Current `Reactor` instance for fluent chaining.
866
883
  */
867
- plugIn(plugin) {
868
- const name = plugin.constructor.plugName;
869
- this.plugins?.get(name)?.destroy();
870
- return (this.plugins ??= /* @__PURE__ */ new Map()).set(name, (plugin.setup(this), plugin)), this;
884
+ use(target, id) {
885
+ return (this.modules ??= /* @__PURE__ */ new Set()).add(target.setup(this, id)), this;
871
886
  }
872
- /** Resets the reactor to its initial state. */
887
+ /** Resets this reactor instance to its initial state. */
873
888
  reset() {
874
889
  this.getters?.clear(), this.setters?.clear(), this.deleters?.clear(), this.watchers?.clear(), this.listeners?.clear();
875
890
  this.batch?.clear(), this.queue?.clear(), this.isBatching = false;
876
891
  }
877
892
  destroy() {
878
- if (this.plugins) for (const plug of this.plugins.values()) plug.destroy();
893
+ if (this.modules) for (const mdle of this.modules) mdle.destroy();
879
894
  this.reset(), nuke(this);
880
895
  }
881
896
  get canLog() {
882
- return this.isLogging = this.log !== NOOP;
897
+ return this.log !== NOOP;
883
898
  }
884
899
  set canLog(value) {
885
- this.log = (this.isLogging = value) ? RTR_LOG : NOOP;
900
+ this.log = value ? RTR_LOG : NOOP;
886
901
  }
887
- get canTraceLineage() {
888
- return this.config.referenceTracking && !!this.config.lineageTracing;
902
+ get canLineageTrace() {
903
+ return this.config.lineageTracing && this.config.referenceTracking;
889
904
  }
890
905
  get canSmartClone() {
891
- return this.config.referenceTracking && !!this.config.smartCloning;
906
+ return this.config.smartCloning && this.config.referenceTracking;
892
907
  }
893
908
  };
894
909
 
895
910
  // src/ts/core/mixins.ts
896
- var methods = ["tick", "stall", "nostall", "get", "gonce", "noget", "set", "sonce", "noset", "delete", "donce", "nodelete", "watch", "wonce", "nowatch", "on", "once", "off", "snapshot", "cascade", "plugIn", "reset", "destroy"];
911
+ var methods = ["tick", "stall", "nostall", "get", "gonce", "noget", "set", "sonce", "noset", "delete", "donce", "nodelete", "watch", "wonce", "nowatch", "on", "once", "off", "snapshot", "use", "reset", "destroy"];
897
912
  function reactive(target, build, preferences = NIL) {
898
913
  if ("__Reactor__" in target) return target;
899
914
  const descriptors = {}, rtr = getReactor(target, true, build), locks = { enumerable: false, configurable: true, writable: false }, hasAffix = !!(preferences.prefix || preferences.suffix);
@@ -901,7 +916,7 @@ var sia = (() => {
901
916
  let key = methods[i];
902
917
  if (hasAffix) (preferences.whitelist?.includes(key) ?? true) && (key = `${preferences.prefix || ""}${key}${preferences.suffix || ""}`);
903
918
  else if (preferences.whitelist?.includes(key)) continue;
904
- descriptors[key] = { value: rtr[key].bind(rtr), ...locks };
919
+ descriptors[key] = { value: rtr[methods[i]].bind(rtr), ...locks };
905
920
  }
906
921
  descriptors["__Reactor__"] = { value: rtr, ...locks };
907
922
  return Object.defineProperties(rtr.core, descriptors), rtr.core;
@@ -957,6 +972,7 @@ var sia = (() => {
957
972
  createEl: () => createEl,
958
973
  deepClone: () => deepClone,
959
974
  deleteAny: () => deleteAny,
975
+ fanout: () => fanout,
960
976
  formatKeyForDisplay: () => formatKeyForDisplay,
961
977
  formatKeyShortcutsForDisplay: () => formatKeyShortcutsForDisplay,
962
978
  getAny: () => getAny,
@@ -1123,63 +1139,95 @@ var sia = (() => {
1123
1139
  }
1124
1140
  }
1125
1141
 
1126
- // src/ts/plugins.ts
1127
- var plugins_exports = {};
1128
- __export(plugins_exports, {
1129
- BaseReactorPlugin: () => BaseReactorPlugin,
1142
+ // src/ts/modules.ts
1143
+ var modules_exports = {};
1144
+ __export(modules_exports, {
1145
+ AsyncStorageAdapter: () => AsyncStorageAdapter,
1146
+ BaseReactorModule: () => BaseReactorModule,
1130
1147
  BaseStorageAdapter: () => BaseStorageAdapter,
1148
+ COOKIE_ADAPTER_BUILD: () => COOKIE_ADAPTER_BUILD,
1149
+ CookieAdapter: () => CookieAdapter,
1131
1150
  INDEXED_DB_ADAPTER_BUILD: () => INDEXED_DB_ADAPTER_BUILD,
1132
1151
  IndexedDBAdapter: () => IndexedDBAdapter,
1133
1152
  LocalStorageAdapter: () => LocalStorageAdapter,
1134
- MemoryStorageAdapter: () => MemoryStorageAdapter,
1135
- PERSIST_PLUGIN_BUILD: () => PERSIST_PLUGIN_BUILD,
1136
- PersistPlugin: () => PersistPlugin,
1153
+ MemoryAdapter: () => MemoryAdapter,
1154
+ PERSIST_MODULE_BUILD: () => PERSIST_MODULE_BUILD,
1155
+ PersistModule: () => PersistModule,
1156
+ SessionStorageAdapter: () => SessionStorageAdapter,
1137
1157
  StorageAdapter: () => StorageAdapter,
1138
- TIME_TRAVEL_PLUGIN_BUILD: () => TIME_TRAVEL_PLUGIN_BUILD,
1139
- TimeTravelPlugin: () => TimeTravelPlugin
1158
+ TIME_TRAVEL_MODULE_BUILD: () => TIME_TRAVEL_MODULE_BUILD,
1159
+ TimeTravelModule: () => TimeTravelModule
1140
1160
  });
1141
1161
 
1142
- // src/ts/plugins/base.ts
1143
- var BaseReactorPlugin = class {
1144
- static plugName;
1162
+ // src/ts/modules/base.ts
1163
+ var wpArr = ["*"];
1164
+ var BaseReactorModule = class {
1165
+ static moduleName;
1145
1166
  get name() {
1146
- return this.constructor.plugName;
1167
+ return this.constructor.moduleName;
1147
1168
  }
1148
1169
  ac = new AbortController();
1149
1170
  signal = this.ac.signal;
1150
- rtr;
1171
+ rtrs = /* @__PURE__ */ new Map();
1172
+ rids = /* @__PURE__ */ new WeakMap();
1173
+ // for quick 0(1) lookups over iteration
1174
+ wired = false;
1151
1175
  config;
1152
1176
  state;
1153
1177
  constructor(config, rtr, state2) {
1154
1178
  guardAllMethods(this, this.guard);
1155
- this.rtr = rtr;
1156
1179
  this.config = isObj(config) ? reactive(config) : config;
1157
1180
  this.state = isObj(state2) ? reactive(state2) : state2;
1181
+ rtr && this.attach(rtr);
1182
+ }
1183
+ /**
1184
+ * Connect to a `Reactor` instance, allows managing multiple reactors if needed.
1185
+ * @param target `Reactor` instance or `reactive()` object to connect to.
1186
+ * @param id Optional custom id for the reactor, prefer over default implicit index id when managing multiple reactors, supports paths to merge into a single tree.
1187
+ * @returns Current `ReactorModule` instance for fluent chaining.
1188
+ * @example
1189
+ * const mod = new MyModule().attach(state1).attach(state2); // implicit index-based ids by default, add a .setup() or `Reactor.use()` when ready for init.
1190
+ * @example
1191
+ * const persist = new PersistModule(config).attach(sessState, "session").attach(adminState, "session.admin"); // don't use "*", causes de-serialization issues.
1192
+ */
1193
+ attach(target, id = this.rtrs.size) {
1194
+ const rtr = getReactor(target);
1195
+ if (!rtr || this.rtrs.has(id)) return this;
1196
+ return this.rids.set((this.rtrs.set(id, rtr), rtr), id), this.onAttach(rtr, id), this;
1197
+ }
1198
+ onAttach(_rtr, _rid) {
1158
1199
  }
1159
- /** Entry point called to initialize plugin wiring. */
1160
- setup(rtr) {
1161
- this.rtr ??= rtr;
1162
- this.wire();
1200
+ /**
1201
+ * Entry point called to initialize module wiring, calls `.attach(target, id)` first, `Reactor.use()` calls this internally.
1202
+ * Should run as last in `.attach()` chain or after all desired reactors if using multiple; so wiring is done safely after.
1203
+ * @param target `Reactor` instance or `reactive()` object to connect to.
1204
+ * @param id Optional id for the reactor, prefer over default implicit index id when managing multiple reactors.
1205
+ * @returns Current `ReactorModule` instance for fluent chaining.
1206
+ * @example
1207
+ * const mod = new MyModule().attach(state1).setup(state2); // if using multiple, this should run last; with same params as `.attach()` for a shorter chain
1208
+ */
1209
+ setup(target, id) {
1210
+ return this.attach(target, id), !this.wired && (this.wire(), this.wired = true), this;
1163
1211
  }
1164
1212
  destroy() {
1165
1213
  this.ac.abort();
1166
1214
  this.onDestroy?.();
1167
1215
  }
1168
1216
  /**
1169
- * Wraps a function with plugin-scoped error logging.
1217
+ * Wraps a function with module-scoped error logging.
1170
1218
  * Use this when creating functions dynamically (for example, before attaching an anonymous listener on the fly).
1171
1219
  * @example
1172
1220
  * window.addEventListener("resize", this.guard(() => this.syncLayout(true)), { signal: this.signal });
1173
1221
  */
1174
1222
  guard = (fn) => {
1175
- return guardMethod(fn, (e) => this.rtr.log(`[Reactor "${this.name}" Plug] Error: ${e}`));
1223
+ return guardMethod(fn, (e) => this.rtrs.values().next().value?.log(`[Reactor "${this.name}" Module] Error: ${e}`));
1176
1224
  };
1177
1225
  // `()=>{}`: needs to be bounded even before initialization
1178
1226
  };
1179
1227
 
1180
1228
  // src/ts/utils/store.ts
1181
1229
  var BaseStorageAdapter = class {
1182
- static name;
1230
+ name = "StorageAdapter";
1183
1231
  config;
1184
1232
  warn = (act = "", mssg = "Support issue or Private Mode", key = "", store = "") => this.config.debug && console.warn(`[${this.constructor.name} \`${act}\`] Failed${key ? `for ${key}` : ""} ${store ? ` on "${store}"` : ""} ${this.config.dbName ? ` at ${this.config.dbName}` : ""} (${mssg})`);
1185
1233
  constructor(config) {
@@ -1187,26 +1235,44 @@ var sia = (() => {
1187
1235
  }
1188
1236
  };
1189
1237
  var StorageAdapter = class extends BaseStorageAdapter {
1238
+ name = "SyncStorageAdapter";
1190
1239
  };
1191
1240
  var AsyncStorageAdapter = class extends BaseStorageAdapter {
1241
+ name = "AsyncStorageAdapter";
1192
1242
  };
1193
1243
  var LocalStorageAdapter = class extends StorageAdapter {
1194
- static name = "LocalStorage";
1195
- get(key) {
1244
+ name = "LocalStorage";
1245
+ /**
1246
+ * Reads and parses a value from localStorage.
1247
+ * @param key Storage key.
1248
+ * @returns Parsed value, or `undefined` when missing/unreadable.
1249
+ */
1250
+ get(key, reviver = this.config.reviver) {
1196
1251
  try {
1197
1252
  const v = localStorage.getItem(key);
1198
- return v ? JSON.parse(v) : void 0;
1253
+ return v ? JSON.parse(v, reviver) : void 0;
1199
1254
  } catch {
1200
1255
  return void 0;
1201
1256
  }
1202
1257
  }
1203
- set(key, value) {
1258
+ /**
1259
+ * Serializes and writes a value to localStorage.
1260
+ * @param key Storage key.
1261
+ * @param value Value to serialize.
1262
+ * @returns `true` when write succeeds, else `false`.
1263
+ */
1264
+ set(key, value, replacer = this.config.replacer) {
1204
1265
  try {
1205
- return localStorage.setItem(key, JSON.stringify(value)), true;
1266
+ return localStorage.setItem(key, JSON.stringify(value, replacer)), true;
1206
1267
  } catch (e) {
1207
1268
  return this.warn("setItem", void 0, key), false;
1208
1269
  }
1209
1270
  }
1271
+ /**
1272
+ * Removes a single key from localStorage.
1273
+ * @param key Storage key.
1274
+ * @returns `true` when removal succeeds, else `false`.
1275
+ */
1210
1276
  remove(key) {
1211
1277
  try {
1212
1278
  return localStorage.removeItem(key), true;
@@ -1214,6 +1280,10 @@ var sia = (() => {
1214
1280
  return this.warn("removeItem", void 0, key), false;
1215
1281
  }
1216
1282
  }
1283
+ /**
1284
+ * Clears all localStorage entries for the current origin.
1285
+ * @returns `true` when clear succeeds, else `false`.
1286
+ */
1217
1287
  clear() {
1218
1288
  try {
1219
1289
  return localStorage.clear(), true;
@@ -1222,25 +1292,94 @@ var sia = (() => {
1222
1292
  }
1223
1293
  }
1224
1294
  };
1225
- var MemoryStorageAdapter = class extends StorageAdapter {
1295
+ var SessionStorageAdapter = class extends StorageAdapter {
1296
+ name = "SessionStorage";
1297
+ /**
1298
+ * Reads and parses a value from sessionStorage.
1299
+ * @param key Storage key.
1300
+ * @returns Parsed value, or `undefined` when missing/unreadable.
1301
+ */
1302
+ get(key, reviver = this.config.reviver) {
1303
+ try {
1304
+ const v = sessionStorage.getItem(key);
1305
+ return v ? JSON.parse(v, reviver) : void 0;
1306
+ } catch {
1307
+ return void 0;
1308
+ }
1309
+ }
1310
+ /**
1311
+ * Serializes and writes a value to sessionStorage.
1312
+ * @param key Storage key.
1313
+ * @param value Value to serialize.
1314
+ * @returns `true` when write succeeds, else `false`.
1315
+ */
1316
+ set(key, value, replacer = this.config.replacer) {
1317
+ try {
1318
+ return sessionStorage.setItem(key, JSON.stringify(value, replacer)), true;
1319
+ } catch (e) {
1320
+ return this.warn("setItem", void 0, key), false;
1321
+ }
1322
+ }
1323
+ /**
1324
+ * Removes a single key from sessionStorage.
1325
+ * @param key Storage key.
1326
+ * @returns `true` when removal succeeds, else `false`.
1327
+ */
1328
+ remove(key) {
1329
+ try {
1330
+ return sessionStorage.removeItem(key), true;
1331
+ } catch (e) {
1332
+ return this.warn("removeItem", void 0, key), false;
1333
+ }
1334
+ }
1335
+ /**
1336
+ * Clears all sessionStorage entries for the current tab session.
1337
+ * @returns `true` when clear succeeds, else `false`.
1338
+ */
1339
+ clear() {
1340
+ try {
1341
+ return sessionStorage.clear(), true;
1342
+ } catch (e) {
1343
+ return this.warn("clear", void 0), false;
1344
+ }
1345
+ }
1346
+ };
1347
+ var MemoryAdapter = class extends StorageAdapter {
1348
+ name = "Memory";
1226
1349
  constructor(build) {
1227
1350
  super({ store: /* @__PURE__ */ new Map(), ...build });
1228
1351
  }
1229
- get(key) {
1352
+ /**
1353
+ * Reads and parses a value from memory storage.
1354
+ * @param key Storage key.
1355
+ * @returns Parsed value, or `undefined` when missing/unreadable.
1356
+ */
1357
+ get(key, reviver = this.config.reviver) {
1230
1358
  try {
1231
1359
  const v = this.config.store.get(key);
1232
- return v ? JSON.parse(v) : void 0;
1360
+ return v ? JSON.parse(v, reviver) : void 0;
1233
1361
  } catch {
1234
1362
  return void 0;
1235
1363
  }
1236
1364
  }
1237
- set(key, value) {
1365
+ /**
1366
+ * Serializes and writes a value to memory storage.
1367
+ * @param key Storage key.
1368
+ * @param value Value to serialize.
1369
+ * @returns `true` when write succeeds, else `false`.
1370
+ */
1371
+ set(key, value, replacer = this.config.replacer) {
1238
1372
  try {
1239
- return this.config.store.set(key, JSON.stringify(value)), true;
1373
+ return this.config.store.set(key, JSON.stringify(value, replacer)), true;
1240
1374
  } catch (e) {
1241
1375
  return this.warn("set", void 0, key), false;
1242
1376
  }
1243
1377
  }
1378
+ /**
1379
+ * Removes a single key from memory storage.
1380
+ * @param key Storage key.
1381
+ * @returns `true` when removal succeeds, else `false`.
1382
+ */
1244
1383
  remove(key) {
1245
1384
  try {
1246
1385
  return this.config.store.delete(key), true;
@@ -1248,6 +1387,10 @@ var sia = (() => {
1248
1387
  return this.warn("remove", void 0, key), false;
1249
1388
  }
1250
1389
  }
1390
+ /**
1391
+ * Clears all entries from memory storage.
1392
+ * @returns `true` when clear succeeds, else `false`.
1393
+ */
1251
1394
  clear() {
1252
1395
  try {
1253
1396
  return this.config.store.clear(), true;
@@ -1256,23 +1399,100 @@ var sia = (() => {
1256
1399
  }
1257
1400
  }
1258
1401
  };
1402
+ var CookieAdapter = class extends StorageAdapter {
1403
+ name = "Cookie";
1404
+ deets = (opts = NIL, _d = opts.domain ?? this.config.domain, _m = opts.maxAge ?? this.config.maxAge, _e = opts.expires ?? this.config.expires) => `Path=${opts.path ?? this.config.path}; SameSite=${opts.sameSite ?? this.config.sameSite}${_d ? `; Domain=${_d}` : ""}${opts.secure ?? this.config.secure ? "; Secure" : ""}${_m !== void 0 ? `; Max-Age=${_m}` : ""}${_e !== void 0 ? `; Expires=${_e instanceof Date ? _e.toUTCString() : _e}` : ""}`;
1405
+ constructor(build) {
1406
+ super({ secure: "undefined" !== typeof window && location.protocol === "https:", ...COOKIE_ADAPTER_BUILD, ...build });
1407
+ }
1408
+ /**
1409
+ * Reads and parses a cookie visible to the current page scope.
1410
+ * @param key Cookie key.
1411
+ * @returns Parsed value, or `undefined` when missing/unreadable.
1412
+ */
1413
+ get(key, reviver = this.config.reviver) {
1414
+ try {
1415
+ const k = encodeURIComponent(key) + "=";
1416
+ for (const pair of document.cookie ? document.cookie.split("; ") : []) {
1417
+ if (!pair.startsWith(k)) continue;
1418
+ return JSON.parse(decodeURIComponent(pair.slice(k.length)), reviver);
1419
+ }
1420
+ return void 0;
1421
+ } catch {
1422
+ return void 0;
1423
+ }
1424
+ }
1425
+ /**
1426
+ * Writes a cookie with optional per-call scope/lifetime overrides.
1427
+ * @param key Cookie key.
1428
+ * @param value Value to serialize.
1429
+ * @param opts Optional per-call cookie options.
1430
+ * @returns `true` when write succeeds, else `false`.
1431
+ */
1432
+ set(key, value, opts = NIL, replacer = this.config.replacer) {
1433
+ try {
1434
+ return document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(value, replacer))}; ${this.deets(opts)}`, true;
1435
+ } catch {
1436
+ return this.warn("set", void 0, key), false;
1437
+ }
1438
+ }
1439
+ /**
1440
+ * Removes a cookie key using matching scope attributes.
1441
+ * @param key Cookie key.
1442
+ * @param opts Optional per-call scope overrides.
1443
+ * @returns `true` when removal succeeds, else `false`.
1444
+ */
1445
+ remove(key, opts = NIL) {
1446
+ try {
1447
+ return document.cookie = `${encodeURIComponent(key)}=; ${this.deets({ ...opts, maxAge: 0, expires: /* @__PURE__ */ new Date(0) })}`, true;
1448
+ } catch {
1449
+ return this.warn("remove", void 0, key), false;
1450
+ }
1451
+ }
1452
+ /**
1453
+ * Attempts to remove all visible cookie keys for the given scope.
1454
+ * @param opts Optional per-call scope overrides.
1455
+ * @returns `true` when clear succeeds, else `false`.
1456
+ */
1457
+ clear(opts = NIL) {
1458
+ try {
1459
+ for (const pair of document.cookie ? document.cookie.split("; ") : []) {
1460
+ const idx = pair.indexOf("=");
1461
+ document.cookie = `${idx === -1 ? pair : pair.slice(0, idx)}=; ${this.deets({ ...opts, maxAge: 0, expires: /* @__PURE__ */ new Date(0) })}`;
1462
+ }
1463
+ return true;
1464
+ } catch {
1465
+ return this.warn("clear"), false;
1466
+ }
1467
+ }
1468
+ };
1259
1469
  var IndexedDBAdapter = class extends AsyncStorageAdapter {
1470
+ name = "IndexedDB";
1260
1471
  db;
1261
- warn = (act = "", mssg = "Support issue or Private Mode", store = "", key = "") => console.warn(`[IndexedDB \`${act}\`] Failed${store ? ` for ${key} on "${store}"` : ""} at ${this.config.dbName} (${mssg})`);
1262
1472
  constructor(build) {
1263
1473
  super({ ...INDEXED_DB_ADAPTER_BUILD, ...build });
1264
1474
  }
1475
+ /**
1476
+ * Returns a connected IndexedDB instance, opening it when needed.
1477
+ * @returns Connected database handle.
1478
+ */
1265
1479
  async idb() {
1266
1480
  const idb = this.config.onidb();
1267
1481
  if (idb || this.db) return Promise.resolve(idb || this.db);
1268
1482
  return new Promise((res, rej) => {
1269
1483
  const req = indexedDB.open(this.config.dbName, this.config.version);
1270
1484
  req.onupgradeneeded = (e) => (this.config.onupgradeneeded(req.result, e), this.config.stores.forEach((s) => !req.result.objectStoreNames.contains(s) && req.result.createObjectStore(s)));
1271
- req.onsuccess = (e) => (this.config.onsuccess(req.result, e), req.result.onversionchange = (e2) => (this.warn("update", "Updated in another tab"), this.config.onversionchange(req.result, e2), req.result.close()), res(this.db = req.result));
1485
+ req.onsuccess = (e) => (this.config.onsuccess(req.result, e), req.result.onversionchange = (e2) => (this.config.onversionchange(req.result, e2), this.warn("update", "Updated in another tab"), req.result.close()), res(this.db = req.result));
1272
1486
  req.onerror = (e) => (this.config.onerror(req.error, e), this.warn("open", "Something went wrong"), rej(req.error));
1273
1487
  req.onblocked = (e) => (this.config.onblocked(e), this.warn("open", "Close other tabs for updates"));
1274
1488
  });
1275
1489
  }
1490
+ /**
1491
+ * Reads a value by key from an object store.
1492
+ * @param key Record key.
1493
+ * @param store Optional object-store override.
1494
+ * @returns Stored value, or `undefined` when missing/unreadable.
1495
+ */
1276
1496
  async get(key, store = this.config.stores[0]) {
1277
1497
  try {
1278
1498
  const req = (await this.idb()).transaction(store).objectStore(store).get(key);
@@ -1281,6 +1501,13 @@ var sia = (() => {
1281
1501
  return this.warn("get", void 0, store), void 0;
1282
1502
  }
1283
1503
  }
1504
+ /**
1505
+ * Writes a value by key into an object store.
1506
+ * @param key Record key.
1507
+ * @param value Value to store.
1508
+ * @param store Optional object-store override.
1509
+ * @returns `true` when write succeeds, else `false`.
1510
+ */
1284
1511
  async set(key, value, store = this.config.stores[0]) {
1285
1512
  try {
1286
1513
  const req = (await this.idb()).transaction(store, "readwrite").objectStore(store).put(value, key);
@@ -1289,6 +1516,12 @@ var sia = (() => {
1289
1516
  return this.warn("put", void 0, store), false;
1290
1517
  }
1291
1518
  }
1519
+ /**
1520
+ * Deletes a value by key from an object store.
1521
+ * @param key Record key.
1522
+ * @param store Optional object-store override.
1523
+ * @returns `true` when delete succeeds, else `false`.
1524
+ */
1292
1525
  async remove(key, store = this.config.stores[0]) {
1293
1526
  try {
1294
1527
  const req = (await this.idb()).transaction(store, "readwrite").objectStore(store).delete(key);
@@ -1297,6 +1530,11 @@ var sia = (() => {
1297
1530
  return this.warn("delete", void 0, store), false;
1298
1531
  }
1299
1532
  }
1533
+ /**
1534
+ * Clears one or more object stores.
1535
+ * @param stores Store name or list of store names to clear.
1536
+ * @returns `true` when all clears succeed, else `false`.
1537
+ */
1300
1538
  async clear(stores = this.config.stores) {
1301
1539
  let success = true;
1302
1540
  for (const store of Array.isArray(stores) ? stores : [stores])
@@ -1309,90 +1547,121 @@ var sia = (() => {
1309
1547
  return success;
1310
1548
  }
1311
1549
  };
1550
+ var COOKIE_ADAPTER_BUILD = { path: "/", sameSite: "Lax", domain: void 0, debug: false };
1312
1551
  var INDEXED_DB_ADAPTER_BUILD = { dbName: "REACTOR_IDB", stores: ["VAULT"], version: 1, onidb: NOOP, onupgradeneeded: NOOP, onversionchange: NOOP, onsuccess: NOOP, onerror: NOOP, onblocked: NOOP };
1313
1552
 
1314
- // src/ts/plugins/persist.ts
1315
- var PersistPlugin = class extends BaseReactorPlugin {
1316
- static plugName = "persist";
1553
+ // src/ts/modules/persist.ts
1554
+ var PersistModule = class extends BaseReactorModule {
1555
+ static moduleName = "persist";
1317
1556
  adapter;
1557
+ hydrateSeq = 0;
1318
1558
  saveTimeoutId = 0;
1319
1559
  get payload() {
1320
- const snap = this.config.useSnapshot ? this.rtr.snapshot() : this.rtr.core;
1321
- return this.config.paths ? this.config.paths.reduce((acc, p) => (setAny(acc, p, getAny(snap, p)), acc), {}) : snap;
1560
+ let res = this.rtrs.size > 1 ? {} : void 0;
1561
+ for (const [rid, rtr] of this.rtrs) {
1562
+ const snap = this.config.useSnapshot ? (this.config.useSnapshot === true && (rtr.config.referenceTracking = rtr.config.smartCloning = true), rtr.snapshot()) : rtr.core, val = this.config.paths ? this.config.paths.reduce((acc, p) => (setAny(acc, p, getAny(snap, p)), acc), {}) : snap;
1563
+ this.rtrs.size > 1 ? setAny(res, rid, val) : res = val;
1564
+ }
1565
+ return res;
1322
1566
  }
1323
1567
  constructor(config, rtr) {
1324
- super({ ...PERSIST_PLUGIN_BUILD, ...config }, rtr);
1568
+ super({ ...PERSIST_MODULE_BUILD, ...config }, rtr, { hydrated: false });
1325
1569
  }
1326
1570
  wire() {
1327
1571
  "undefined" !== typeof window && window.addEventListener("pagehide", this.onDestroy, { signal: this.signal });
1328
1572
  "undefined" !== typeof document && document.addEventListener("visibilitychange", () => document.visibilityState === "hidden" && this.onDestroy(), { signal: this.signal });
1329
- this.config.on("adapter", this.handleAdapterChange, { signal: this.signal, immediate: true });
1330
- this.config.on("disabled", this.handleDisabledChange, { signal: this.signal, immediate: true });
1331
- this.config.on("paths", this.handlePathsState, { signal: this.signal, immediate: true });
1573
+ this.config.on("adapter", this.handleAdapter, { signal: this.signal, immediate: true });
1574
+ this.config.on("disabled", this.handleDisabled, { signal: this.signal, immediate: true });
1575
+ this.config.on("paths", this.handlePaths, { signal: this.signal, immediate: true });
1576
+ }
1577
+ onAttach(rtr) {
1578
+ for (const p of this.config.paths ?? wpArr) !this.config.disabled ? rtr.on(p, this.handleSave, { signal: this.signal, immediate: true }) : rtr.off(p, this.handleSave);
1332
1579
  }
1333
- async handleAdapterChange({ value = LocalStorageAdapter }) {
1580
+ async handleAdapter({ value = LocalStorageAdapter }) {
1581
+ const seq = ++this.hydrateSeq;
1334
1582
  if (this.adapter && value === this.adapter.constructor) return;
1583
+ this.state.hydrated = false;
1335
1584
  this.adapter?.remove(this.config.key);
1336
- this.adapter = "function" === typeof value ? new value({ debug: this.rtr.canLog }) : value;
1337
- let saved = this.adapter.get(this.config.key);
1338
- const isAsync = saved instanceof Promise;
1339
- saved = !isAsync ? saved : await saved;
1340
- const set = (p, news, olds) => setAny(this.rtr.core, p, isPOJO(news, this.rtr.config) && isPOJO(olds, this.rtr.config) ? mergeObjs(news, olds) : olds);
1341
- if (saved) for (const p of this.config.paths ?? ["*"]) set(p, getAny(this.rtr.core, p), getAny(saved, p));
1342
- saved && this.rtr.tick(this.config.paths ?? "*");
1343
- }
1344
- handleDisabledChange({ value }) {
1345
- for (const p of this.config.paths ?? ["*"]) this.rtr.off(p, this.throttleSave), !value && this.rtr.on(p, this.throttleSave, { signal: this.signal, immediate: true });
1585
+ this.adapter = "function" === typeof value ? new value({ debug: !!this.rtrs.values().next().value?.canLog }) : value;
1586
+ try {
1587
+ let saved = this.adapter.get(this.config.key);
1588
+ const isAsync = saved instanceof Promise, depth = this.config.fanout ?? isAsync;
1589
+ saved = !isAsync ? saved : await saved;
1590
+ if (seq !== this.hydrateSeq || !saved) return;
1591
+ for (const [rid, rtr] of this.rtrs) {
1592
+ const entry = this.rtrs.size > 1 ? getAny(saved, rid) : saved;
1593
+ if (!entry) continue;
1594
+ const set = (p, news, olds) => (depth ? fanout : setAny)(rtr.core, p, isPOJO(news, rtr.config) && isPOJO(olds, rtr.config) ? mergeObjs(news, olds) : olds, depth ? { depth } : void 0);
1595
+ for (const p of this.config.paths ?? wpArr) set(p, getAny(rtr.core, p), getAny(entry, p));
1596
+ }
1597
+ for (const rtr of this.rtrs.values()) rtr.tick(!depth ? this.config.paths ?? "*" : "*");
1598
+ } catch {
1599
+ } finally {
1600
+ if (seq === this.hydrateSeq) this.state.hydrated = true;
1601
+ }
1602
+ }
1603
+ handleDisabled({ value }) {
1604
+ for (const rtr of this.rtrs.values()) this.onAttach(rtr);
1346
1605
  value && this.adapter?.remove(this.config.key);
1347
1606
  }
1348
- handlePathsState({ value: paths = ["*"], oldValue: prevs = ["*"] }) {
1349
- for (const p of prevs) this.rtr.off(p, this.throttleSave);
1350
- for (const p of paths) this.rtr.off(p, this.throttleSave), !this.config.disabled && this.rtr.on(p, this.throttleSave, { signal: this.signal, immediate: true });
1607
+ handlePaths({ value: paths = wpArr, oldValue: prevs = wpArr }) {
1608
+ for (const rtr of this.rtrs.values()) {
1609
+ for (const p of prevs) rtr.off(p, this.handleSave);
1610
+ for (const p of paths) rtr.off(p, this.handleSave), !this.config.disabled && rtr.on(p, this.handleSave, { signal: this.signal, immediate: true });
1611
+ }
1351
1612
  }
1352
- throttleSave() {
1613
+ handleSave(e) {
1614
+ if (!this.state.hydrated) return e.stopImmediatePropagation();
1353
1615
  if (!this.saveTimeoutId) this.saveTimeoutId = setTimeout2(() => (this.adapter.set(this.config.key, this.payload), this.saveTimeoutId = 0), this.config.throttle, this.signal);
1354
1616
  }
1355
- /** Clears persisted payload for this plugin instance and drops any pending save. */
1617
+ /** Clears persisted payload for this module instance and drops any pending save. */
1356
1618
  clear() {
1357
1619
  clearTimeout(this.saveTimeoutId);
1358
- this.saveTimeoutId = -1, this.rtr.stall(() => this.saveTimeoutId = 0);
1620
+ this.saveTimeoutId = -1;
1621
+ for (const rtr of this.rtrs.values()) rtr.stall(() => this.saveTimeoutId = 0);
1359
1622
  this.adapter?.remove(this.config.key);
1360
1623
  }
1361
1624
  onDestroy() {
1362
- !this.config.disabled && this.adapter?.set(this.config.key, this.payload);
1625
+ this.state.hydrated && !this.config.disabled && this.adapter?.set(this.config.key, this.payload);
1363
1626
  }
1364
1627
  };
1365
- var PERSIST_PLUGIN_BUILD = { disabled: false, key: "REACTOR_STORE", throttle: 2500, useSnapshot: false };
1628
+ var PERSIST_MODULE_BUILD = { disabled: false, key: "REACTOR_STORE", throttle: 2500, useSnapshot: false };
1366
1629
 
1367
- // src/ts/plugins/timeTravel.ts
1368
- var TimeTravelPlugin = class extends BaseReactorPlugin {
1369
- static plugName = "timeTravel";
1630
+ // src/ts/modules/timeTravel.ts
1631
+ var TimeTravelModule = class extends BaseReactorModule {
1632
+ static moduleName = "timeTravel";
1370
1633
  lastTimestamp = 0;
1371
1634
  playbackTimeoutId = -1;
1372
1635
  constructor(config, rtr) {
1373
- super({ ...TIME_TRAVEL_PLUGIN_BUILD, ...config }, rtr, { initialState: null, history: [], currentFrame: 0, paused: true });
1636
+ super({ ...TIME_TRAVEL_MODULE_BUILD, ...config }, rtr, { initialState: {}, history: [], currentFrame: 0, paused: true });
1374
1637
  }
1375
1638
  // ===========================================================================
1376
1639
  // THE FOUNDATION & WIRETAP (Passive Recording)
1377
1640
  // ===========================================================================
1378
1641
  wire() {
1379
- this.rtr.config.referenceTracking = this.rtr.config.smartCloning = this.rtr.config.eventTimeStamps = true;
1380
- if (!this.state.history.length || this.state.initialState == null) this.state.initialState = this.rtr.snapshot();
1381
1642
  this.lastTimestamp = performance.now();
1382
1643
  this.state.set("currentFrame", (v = 0) => clamp(0, v, this.state.history.length), { signal: this.signal, immediate: true });
1383
- this.config.on("paths", this.handlePathsState, { signal: this.signal, immediate: true });
1644
+ this.config.on("paths", this.handlePaths, { signal: this.signal, immediate: true });
1384
1645
  !this.state.paused && this.play();
1385
1646
  }
1386
- handlePathsState({ value: paths = ["*"], oldValue: prevs = ["*"] }) {
1387
- for (const p of prevs) this.rtr.off(p, this.record);
1388
- for (const p of paths) this.rtr.off(p, this.record), this.rtr.on(p, this.record, { signal: this.signal });
1647
+ onAttach(rtr, rid) {
1648
+ rtr.config.referenceTracking = rtr.config.smartCloning = rtr.config.eventTimeStamps = true;
1649
+ if (!this.state.history.length || !this.state.initialState[rid]) this.state.initialState[rid] = rtr.snapshot();
1650
+ for (const p of this.config.paths ?? wpArr) rtr.on(p, this.record, { signal: this.signal });
1651
+ }
1652
+ handlePaths({ value: paths = wpArr, oldValue: prevs = wpArr }) {
1653
+ for (const rtr of this.rtrs.values()) {
1654
+ for (const p of prevs) rtr.off(p, this.record);
1655
+ for (const p of paths) rtr.off(p, this.record), rtr.on(p, this.record, { signal: this.signal });
1656
+ }
1389
1657
  }
1390
1658
  /** Chronicling the lifecycle of the system, Captures the essence of every mutation wave that bubbles up. */
1391
- record(e) {
1659
+ record(e, rid = this.rids.get(e.reactor)) {
1392
1660
  if (!this.state.paused) return;
1393
1661
  if (this.state.currentFrame < this.state.history.length) this.state.history = this.state.history.slice(0, this.state.currentFrame);
1394
1662
  if (this.state.history.length >= this.config.maxHistoryLength) this.state.history = this.state.history.slice(1);
1395
- this.state.history.push({ path: e.target.path, value: this.rtr.snapshot(false, e.target.value), oldValue: this.rtr.snapshot(false, e.target.oldValue), type: e.staticType, rejected: e.rejected, timedelta: e.timestamp - this.lastTimestamp, hadKey: e.target.hadKey });
1663
+ this.state.history.push({ path: e.target.path, value: e.reactor.snapshot(false, e.target.value), oldValue: e.reactor.snapshot(false, e.target.oldValue), type: e.staticType, hadKey: e.target.hadKey, deltat: e.timestamp - this.lastTimestamp, rid });
1664
+ if (e.rejected) this.state.history[this.state.history.length - 1].rejected = e.rejected;
1396
1665
  this.state.currentFrame = this.state.history.length;
1397
1666
  this.lastTimestamp = e.timestamp;
1398
1667
  }
@@ -1401,7 +1670,7 @@ var sia = (() => {
1401
1670
  this.pause();
1402
1671
  this.playbackTimeoutId = -1;
1403
1672
  this.state.history.length = this.state.currentFrame = 0;
1404
- this.state.initialState = this.rtr.snapshot();
1673
+ this.state.initialState = Object.fromEntries(this.rtrs.entries().map(([rid, rtr]) => [rid, rtr.snapshot()]));
1405
1674
  this.lastTimestamp = performance.now();
1406
1675
  }
1407
1676
  // ===========================================================================
@@ -1414,12 +1683,13 @@ var sia = (() => {
1414
1683
  while (this.state.currentFrame !== target) {
1415
1684
  const e = this.state.history[forward ? this.state.currentFrame : this.state.currentFrame - 1];
1416
1685
  if (!e) break;
1417
- if (forward) e.type === "delete" ? deleteAny(this.rtr.core, e.path) : setAny(this.rtr.core, e.path, deepClone(e.value, this.rtr.config));
1418
- else !e.hadKey ? deleteAny(this.rtr.core, e.path) : setAny(this.rtr.core, e.path, deepClone(e.oldValue, this.rtr.config));
1686
+ const rtr = this.rtrs.get(e.rid) || this.rtrs.values().next().value;
1687
+ if (forward) e.type === "delete" ? deleteAny(rtr.core, e.path) : setAny(rtr.core, e.path, deepClone(e.value, rtr.config));
1688
+ else !e.hadKey ? deleteAny(rtr.core, e.path) : setAny(rtr.core, e.path, deepClone(e.oldValue, rtr.config));
1419
1689
  forward ? this.state.currentFrame++ : this.state.currentFrame--;
1420
- if (e.rejected) this.rtr.log(`[Reactor ${this.name} Plug] ${forward ? "Replaying" : "Reversing"} REJECTED intent at "${e.path}"`);
1690
+ if (e.rejected) rtr.log(`[Reactor ${this.name} Module] ${forward ? "Replaying" : "Reversing"} REJECTED intent at "${e.path}"`);
1421
1691
  }
1422
- this.rtr.tick();
1692
+ for (const rtr of this.rtrs.values()) rtr.tick();
1423
1693
  if (!keepShield) this.state.paused = true;
1424
1694
  }
1425
1695
  /** Step through time, Moves the playhead and teleports the state. */
@@ -1438,9 +1708,9 @@ var sia = (() => {
1438
1708
  async automove(forward = true) {
1439
1709
  this.state.paused = false;
1440
1710
  while ((forward ? this.state.currentFrame < this.state.history.length : this.state.currentFrame > 0) && !this.state.paused) {
1441
- const currIndex = forward ? this.state.currentFrame : this.state.currentFrame - 1, e = this.state.history[forward ? currIndex + 1 : currIndex - 1];
1711
+ const idx = forward ? this.state.currentFrame : this.state.currentFrame - 1, e = this.state.history[forward ? idx + 1 : idx - 1];
1442
1712
  this.jumpTo(this.state.currentFrame + (forward ? 1 : -1), true);
1443
- if (e?.timedelta > 0) await new Promise((res) => this.playbackTimeoutId = setTimeout2(() => res(0), Math.min(e.timedelta, this.config.maxPlaybackDelay), this.signal));
1713
+ if (e?.deltat > 0) await new Promise((res) => this.playbackTimeoutId = setTimeout2(() => res(0), Math.min(e.deltat, this.config.maxPlaybackDelay), this.signal));
1444
1714
  }
1445
1715
  this.state.paused = true;
1446
1716
  }
@@ -1458,24 +1728,24 @@ var sia = (() => {
1458
1728
  try {
1459
1729
  return JSON.stringify(this.state, replacer, space);
1460
1730
  } catch (e) {
1461
- return this.rtr.log(`[Reactor ${this.name} Plug] Failed to export session`), "";
1731
+ return this.rtrs.values().next().value?.log(`[Reactor ${this.name} Module] Failed to export session`), "";
1462
1732
  }
1463
1733
  }
1464
1734
  /** Imports a session from a JSON string, allowing you to replay or analyze past states. */
1465
- import(json) {
1735
+ import(json, reviver) {
1466
1736
  try {
1467
- setAny(this.state, "*", JSON.parse(json));
1737
+ setAny(this.state, "*", JSON.parse(json, reviver));
1468
1738
  this.lastTimestamp = performance.now();
1469
1739
  const resume = !this.state.paused, target = this.state.currentFrame;
1470
1740
  this.state.paused = false;
1471
- setAny(this.rtr.core, "*", deepClone(this.state.initialState, this.rtr.config)), this.rtr.tick();
1741
+ for (const [rid, rtr] of this.rtrs) setAny(rtr.core, "*", deepClone(this.state.initialState[rid], rtr.config)), rtr.tick();
1472
1742
  this.state.currentFrame = 0, this.jumpTo(target), resume && this.play();
1473
1743
  } catch (e) {
1474
- this.rtr.log(`[Reactor ${this.name} Plug] Failed to load session`);
1744
+ this.rtrs.values().next().value?.log(`[Reactor ${this.name} Module] Failed to load session`);
1475
1745
  }
1476
1746
  }
1477
1747
  };
1478
- var TIME_TRAVEL_PLUGIN_BUILD = { maxPlaybackDelay: 2e3 };
1748
+ var TIME_TRAVEL_MODULE_BUILD = { maxPlaybackDelay: 2e3 };
1479
1749
 
1480
1750
  // src/ts/adapters/vanilla.ts
1481
1751
  var vanilla_exports = {};
@@ -1590,9 +1860,9 @@ var sia = (() => {
1590
1860
  * const stop = atrkr.callback(() => console.log("changed")); // re-run after when ".user.name" changes
1591
1861
  * @example Packaged Customization
1592
1862
  * const atrkr = new Autotracker(); // no reactor passed
1593
- * withTracker(atrkr, () => state.user.name); // import `withTracker` first
1863
+ * withTracker(atrkr, () => state.user.name); // import `withTracker` too
1594
1864
  * const stop = atrkr.callback(() => console.log("sync"), { sync: true }); // re-run immediately when ".user.name" changes, works on any path used from any reactor state
1595
- * @example Extensive customization
1865
+ * @example Extensive Customization
1596
1866
  * atrkr.unblock();
1597
1867
  * const prev = CTX.autotracker;
1598
1868
  * CTX.autotracker = atrkr; // import CTX first
@@ -1640,8 +1910,8 @@ var sia = (() => {
1640
1910
 
1641
1911
  // src/ts/adapters/vanilla/TimeTravelOverlay.ts
1642
1912
  var keys = {
1643
- overrides: ["Ctrl+z", "Cmd+z", "Ctrl+y", "Cmd+y", "Ctrl+Shift+z", "Cmd+Shift+z", "Ctrl+g", "Cmd+g", ",", ".", "ArrowLeft", "ArrowRight", "Space", "Alt+Space", "Escape", "Delete", "e", "i", "c"],
1644
- shortcuts: { undo: ["Ctrl+z", "Cmd+z"], redo: ["Ctrl+y", "Cmd+y", "Ctrl+Shift+z", "Cmd+Shift+z"], genesis: ["Ctrl+g", "Cmd+g"], prevFrame: ",", nextFrame: ".", skipBwd: "ArrowLeft", skipFwd: "ArrowRight", playPause: "Space", rewind: "Alt+Space", closeOverlay: "Escape", clrHistory: "Delete", export: "e", import: "i", clear: "c" }
1913
+ overrides: ["Ctrl+z", "Cmd+z", "Ctrl+y", "Cmd+y", "Ctrl+Shift+z", "Cmd+Shift+z", "Home", "End", ",", ".", "ArrowLeft", "ArrowRight", "Space", "Alt+Space", "Escape", "Delete", "e", "i", "c"],
1914
+ shortcuts: { undo: ["Ctrl+z", "Cmd+z"], redo: ["Ctrl+y", "Cmd+y", "Ctrl+Shift+z", "Cmd+Shift+z"], genesis: "Home", ending: "End", prevFrame: ",", nextFrame: ".", skipBwd: "ArrowLeft", skipFwd: "ArrowRight", playPause: "Space", rewind: "Alt+Space", closeOverlay: "Escape", clrHistory: "Delete", export: "e", import: "i", clear: "c" }
1645
1915
  };
1646
1916
  var TimeTravelOverlay = class _TimeTravelOverlay {
1647
1917
  static count = 0;
@@ -1652,22 +1922,22 @@ var sia = (() => {
1652
1922
  els;
1653
1923
  clups = [];
1654
1924
  keyup;
1655
- /** Creates a docked TimeTravel overlay bound to a plugin instance.
1656
- * @param time TimeTravel plugin instance that owns timeline operations.
1925
+ /** Creates a docked TimeTravel overlay bound to a module instance.
1926
+ * @param time TimeTravel module instance that owns timeline operations.
1657
1927
  * @param build Optional initial overlay config overrides.
1658
1928
  */
1659
1929
  constructor(time, build = {}) {
1660
1930
  this.time = time;
1661
1931
  this.config = reactive({ title: `Time Travel Overlay ${this.index = ++_TimeTravelOverlay.count}`, ...build });
1662
1932
  this.state.open = !!this.config.startOpen;
1663
- const s = this.time.state, host = createEl("div", { className: "tt-overlay-host" }), toggle = createEl("button", { className: "tt-overlay-toggle", type: "button", onclick: () => this.state.open = !this.state.open }), panel = createEl("aside", { className: "tt-overlay", ariaLabel: "time travel overlay" }), title = createEl("div", { className: "title" }), frame = createEl("span", { className: "muted" }), clrHistory = createEl("button", { textContent: `Clear History${formatKeyForDisplay(keys.shortcuts.clrHistory)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.clrHistory, false), onclick: () => (this.time.clear(), this.state.import = "") }), undo = createEl("button", { textContent: `Undo${formatKeyForDisplay(keys.shortcuts.undo[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.undo, false), onclick: this.time.undo }), redo = createEl("button", { textContent: `Redo${formatKeyForDisplay(keys.shortcuts.redo[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.redo, false), onclick: this.time.redo }), genesis = createEl("button", { textContent: `Genesis${formatKeyForDisplay(keys.shortcuts.genesis[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.genesis, false), onclick: () => this.time.jumpTo(0) }), playPause = createEl("button", { onclick: () => this.time[s.paused ? "play" : "pause"](), ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.playPause, false) }), rewind = createEl("button", { textContent: `Rewind${formatKeyForDisplay(keys.shortcuts.rewind)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.rewind, false), onclick: this.time.rewind }), range = createEl("input", { type: "range", min: "0", max: "0", value: "0", title: "time travel frame", ariaLabel: "time travel frame", oninput: () => this.time.jumpTo(Number(range.value)) }), exp = createEl("button", { textContent: `Export${formatKeyForDisplay(keys.shortcuts.export)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.export, false), onclick: () => this.state.import = this.time.export(null, 2) }), imp = createEl("button", { textContent: `Import${formatKeyForDisplay(keys.shortcuts.import)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.import, false), onclick: () => this.state.import.trim().length && this.time.import(this.state.import) }), clr = createEl("button", { textContent: `Clear${formatKeyForDisplay(keys.shortcuts.clear)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.clear, false), onclick: () => this.state.import = "" }), payload = createEl("textarea", { className: "tt-io", readOnly: true, placeholder: "current payload json", title: "current payload" }), io = createEl("textarea", { className: "tt-io", placeholder: "timeline payload json", oninput: () => this.state.import = io.value }), foot = createEl("p", { className: "tt-footnote", textContent: "Want this in your app? " }), link = createEl("a", { target: "_blank", rel: "noreferrer noopener", textContent: "sia-reactor", href: "https://www.npmjs.com/package/sia-reactor" }), box = createEl("div", { className: "tt-status-box" }), status = createEl("div", { className: "tt-status-row" }), row1 = createEl("div", { className: "tt-row" }), row2 = createEl("div", { className: "tt-row" }), row3 = createEl("div", { className: "tt-row" });
1933
+ const s = this.time.state, host = createEl("div", { className: "tt-overlay-host" }), toggle = createEl("button", { className: "tt-overlay-toggle", type: "button", onclick: () => this.state.open = !this.state.open }), panel = createEl("aside", { className: "tt-overlay", ariaLabel: "time travel overlay" }), title = createEl("div", { className: "title" }), frame = createEl("span", { className: "muted" }), clrHistory = createEl("button", { textContent: `Clear History${formatKeyForDisplay(keys.shortcuts.clrHistory)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.clrHistory, false), onclick: () => (this.time.clear(), this.state.import = "") }), undo = createEl("button", { textContent: `Undo${formatKeyForDisplay(keys.shortcuts.undo[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.undo, false), onclick: this.time.undo }), redo = createEl("button", { textContent: `Redo${formatKeyForDisplay(keys.shortcuts.redo[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.redo, false), onclick: this.time.redo }), genesis = createEl("button", { textContent: `Genesis${formatKeyForDisplay(keys.shortcuts.genesis)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.genesis, false), onclick: () => this.time.jumpTo(0) }), playPause = createEl("button", { onclick: () => this.time[s.paused ? "play" : "pause"](), ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.playPause, false) }), rewind = createEl("button", { textContent: `Rewind${formatKeyForDisplay(keys.shortcuts.rewind)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.rewind, false), onclick: this.time.rewind }), range = createEl("input", { type: "range", min: "0", max: "0", value: "0", title: "time travel frame", ariaLabel: "time travel frame", oninput: () => this.time.jumpTo(Number(range.value)) }), exp = createEl("button", { textContent: `Export${formatKeyForDisplay(keys.shortcuts.export)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.export, false), onclick: () => this.state.import = this.time.export(null, 2) }), imp = createEl("button", { textContent: `Import${formatKeyForDisplay(keys.shortcuts.import)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.import, false), onclick: () => this.state.import.trim().length && this.time.import(this.state.import) }), clr = createEl("button", { textContent: `Clear${formatKeyForDisplay(keys.shortcuts.clear)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.clear, false), onclick: () => this.state.import = "" }), payload = createEl("textarea", { className: "tt-io", readOnly: true, placeholder: "current payload json", title: "current payload" }), io = createEl("textarea", { className: "tt-io", placeholder: "timeline payload json", oninput: () => this.state.import = io.value }), foot = createEl("p", { className: "tt-footnote", textContent: "Want this in your app? " }), link = createEl("a", { target: "_blank", rel: "noreferrer noopener", textContent: "sia-reactor", href: "https://www.npmjs.com/package/sia-reactor" }), box = createEl("div", { className: "tt-status-box" }), status = createEl("div", { className: "tt-status-row" }), row1 = createEl("div", { className: "tt-row" }), row2 = createEl("div", { className: "tt-row" }), row3 = createEl("div", { className: "tt-row" });
1664
1934
  status.append((box.append(frame), box), clrHistory);
1665
1935
  panel.append(title, status, (row1.append(undo, redo, genesis), row1), (row2.append(playPause, rewind), row2), payload, range, (row3.append(exp, imp, clr), row3), io, (foot.appendChild(link), foot));
1666
1936
  host.append(toggle, panel);
1667
1937
  this.els = { host, toggle, panel, title, frame, clrHistory, undo, redo, genesis, playPause, rewind, range, exp, imp, clr, payload, io };
1668
1938
  this.keyup = (e) => {
1669
- const a = this.state.open && keyEventAllowed(e, keys);
1670
- a === "undo" ? this.time.undo() : a === "redo" ? this.time.redo() : a === "genesis" ? this.time.jumpTo(0) : a === "prevFrame" ? this.time.step(1, false) : a === "nextFrame" ? this.time.step(1, true) : a === "skipBwd" ? this.time.step(5, false) : a === "skipFwd" ? this.time.step(5, true) : a === "rewind" ? this.time.rewind() : a === "playPause" ? this.time[s.paused ? "play" : "pause"]() : a === "clrHistory" ? this.time.clear() : a === "closeOverlay" ? this.state.open = false : a === "export" ? this.state.import = this.time.export() : a === "import" ? this.state.import.trim().length && this.time.import(this.state.import) : a === "clear" && (this.state.import = "");
1939
+ const a = this.state.open && (this.config.devOnly ? CTX.isDevEnv : true) && keyEventAllowed(e, keys);
1940
+ a === "undo" ? this.time.undo() : a === "redo" ? this.time.redo() : a === "genesis" ? this.time.jumpTo(0) : a === "ending" ? this.time.jumpTo(s.history.length) : a === "prevFrame" ? this.time.step(1, false) : a === "nextFrame" ? this.time.step(1, true) : a === "skipBwd" ? this.time.step(5, false) : a === "skipFwd" ? this.time.step(5, true) : a === "rewind" ? this.time.rewind() : a === "playPause" ? this.time[s.paused ? "play" : "pause"]() : a === "clrHistory" ? this.time.clear() : a === "closeOverlay" ? this.state.open = false : a === "export" ? this.state.import = this.time.export() : a === "import" ? this.state.import.trim().length && this.time.import(this.state.import) : a === "clear" && (this.state.import = "");
1671
1941
  };
1672
1942
  window.addEventListener("keydown", this.keyup);
1673
1943
  const sync = [
@@ -1682,7 +1952,8 @@ var sia = (() => {
1682
1952
  effect(() => {
1683
1953
  frame.textContent = `Frame: ${s.currentFrame} / ${s.history.length}`;
1684
1954
  range.disabled = clrHistory.disabled = !s.history.length;
1685
- genesis.disabled = rewind.disabled = undo.disabled = !s.currentFrame;
1955
+ genesis.disabled = undo.disabled = !s.currentFrame;
1956
+ rewind.disabled = !s.paused || !s.currentFrame;
1686
1957
  playPause.disabled = redo.disabled = s.currentFrame >= s.history.length;
1687
1958
  range.max = String(s.history.length);
1688
1959
  range.value = String(Math.min(s.currentFrame, s.history.length));
@@ -1696,7 +1967,7 @@ var sia = (() => {
1696
1967
  this.clups.push(...sync);
1697
1968
  }
1698
1969
  destroy() {
1699
- this.clups.forEach((fn) => fn());
1970
+ for (const clup of this.clups) clup();
1700
1971
  this.keyup && window.removeEventListener("keydown", this.keyup);
1701
1972
  this.els.host.remove();
1702
1973
  nuke(this), --_TimeTravelOverlay.count;