jotai-state-tree 1.7.4 → 1.8.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/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  A MobX-State-Tree (MST) compatible state management library powered by [Jotai](https://jotai.org/).
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/jotai-state-tree.svg)](https://www.npmjs.com/package/jotai-state-tree)
6
+ [![CI](https://github.com/bmartel/jotai-state-tree/actions/workflows/release.yml/badge.svg)](https://github.com/bmartel/jotai-state-tree/actions/workflows/release.yml)
7
+ [![coverage](.github/badges/coverage.svg)](https://github.com/bmartel/jotai-state-tree/actions/workflows/release.yml)
6
8
  [![license](https://img.shields.io/github/license/bmartel/jotai-state-tree.svg)](LICENSE)
7
9
 
8
10
  `jotai-state-tree` combines the transactional, tree-structured state model of MobX-State-Tree with the lightweight, zero-leak, high-performance atomic updates of Jotai. It is designed to be an API-compatible, drop-in replacement for MobX-State-Tree, featuring perfect TypeScript type safety out of the box.
@@ -32,6 +34,24 @@ npm install jotai-state-tree jotai
32
34
 
33
35
  ---
34
36
 
37
+ ## React Native Compatibility
38
+
39
+ `jotai-state-tree` is fully compatible with React Native projects.
40
+
41
+ ### Prerequisites & JS Engine
42
+ - **React Native Version**: `>= 0.70` is required.
43
+ - **JavaScript Engine**: The library relies on native ES2021 `WeakRef` and `FinalizationRegistry` features for memory management. If you use the Hermes engine (default since React Native 0.70), it must be version `0.12.0` or newer.
44
+
45
+ ### Using the Router in React Native
46
+ When running in React Native (or any non-browser environment), the built-in state router automatically disables DOM/browser integration and behaves as a fully-featured **in-memory router**. It maintains a navigation history stack internally, enabling you to use:
47
+ - `push(path)` / `replace(path)`
48
+ - `go(delta)` / `goBack()` / `goForward()`
49
+ - `RouteView` to reactively render screen components based on the active path
50
+
51
+ This allows you to manage native navigation state trees with full time-travel, middleware, and action recording support!
52
+
53
+ ---
54
+
35
55
  ## Quick Start
36
56
 
37
57
  ```typescript
@@ -3516,6 +3516,7 @@ function createActionRecorder(target) {
3516
3516
  // src/router.ts
3517
3517
  import { createContext, useContext } from "react";
3518
3518
  import { useAtomValue } from "jotai";
3519
+ var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.location !== "undefined" && typeof window.history !== "undefined";
3519
3520
  var RouteDefinition = model("RouteDefinition", {
3520
3521
  path: string,
3521
3522
  name: string,
@@ -3643,7 +3644,9 @@ var RouterModel = model("RouterModel", {
3643
3644
  })).volatile(() => ({
3644
3645
  beforeNavigate: null,
3645
3646
  afterNavigate: null,
3646
- _popStateListener: null
3647
+ _popStateListener: null,
3648
+ _historyStack: [],
3649
+ _historyIndex: -1
3647
3650
  })).actions((self) => {
3648
3651
  return {
3649
3652
  setGuards(before, after) {
@@ -3663,13 +3666,30 @@ var RouterModel = model("RouterModel", {
3663
3666
  self.params = matched ? matched.params : {};
3664
3667
  self.query = parsed.query;
3665
3668
  self.currentRouteName = matched ? matched.route.name : null;
3666
- if (typeof window !== "undefined" && window.history) {
3669
+ if (isBrowser) {
3667
3670
  const fullPath = pathname + search + hash;
3668
3671
  if (action === "PUSH") {
3669
3672
  window.history.pushState(state, "", fullPath);
3670
3673
  } else if (action === "REPLACE") {
3671
3674
  window.history.replaceState(state, "", fullPath);
3672
3675
  }
3676
+ } else {
3677
+ const fullPath = pathname + search + hash;
3678
+ if (action === "PUSH") {
3679
+ self._historyStack = self._historyStack.slice(0, self._historyIndex + 1);
3680
+ self._historyStack.push(fullPath);
3681
+ self._historyIndex = self._historyStack.length - 1;
3682
+ } else if (action === "REPLACE") {
3683
+ if (self._historyIndex === -1) {
3684
+ self._historyStack = [fullPath];
3685
+ self._historyIndex = 0;
3686
+ } else {
3687
+ self._historyStack[self._historyIndex] = fullPath;
3688
+ }
3689
+ } else if (action === "INITIAL") {
3690
+ self._historyStack = [fullPath];
3691
+ self._historyIndex = 0;
3692
+ }
3673
3693
  }
3674
3694
  },
3675
3695
  push: flow(function* (path, state) {
@@ -3743,23 +3763,34 @@ var RouterModel = model("RouterModel", {
3743
3763
  }
3744
3764
  }),
3745
3765
  go(delta) {
3746
- if (typeof window !== "undefined" && window.history) {
3766
+ if (isBrowser) {
3747
3767
  window.history.go(delta);
3768
+ } else {
3769
+ const nextIndex = self._historyIndex + delta;
3770
+ if (nextIndex >= 0 && nextIndex < self._historyStack.length) {
3771
+ self._historyIndex = nextIndex;
3772
+ const targetPath = self._historyStack[nextIndex];
3773
+ self.syncLocation(targetPath, "", "", "POP");
3774
+ }
3748
3775
  }
3749
3776
  },
3750
3777
  goBack() {
3751
- if (typeof window !== "undefined" && window.history) {
3778
+ if (isBrowser) {
3752
3779
  window.history.back();
3780
+ } else {
3781
+ self.go(-1);
3753
3782
  }
3754
3783
  },
3755
3784
  goForward() {
3756
- if (typeof window !== "undefined" && window.history) {
3785
+ if (isBrowser) {
3757
3786
  window.history.forward();
3787
+ } else {
3788
+ self.go(1);
3758
3789
  }
3759
3790
  }
3760
3791
  };
3761
3792
  }).afterCreate((self) => {
3762
- if (typeof window !== "undefined") {
3793
+ if (isBrowser) {
3763
3794
  const handlePopState = (event) => {
3764
3795
  const parsed = parseUrl(window.location.pathname + window.location.search + window.location.hash);
3765
3796
  const matched = matchRoutes(self.routes, parsed.pathname);
@@ -3797,7 +3828,7 @@ var RouterModel = model("RouterModel", {
3797
3828
  if (res === false) {
3798
3829
  revert();
3799
3830
  } else if (typeof res === "string") {
3800
- self.push(res);
3831
+ self.replace(res);
3801
3832
  } else {
3802
3833
  proceed();
3803
3834
  }
@@ -3808,7 +3839,7 @@ var RouterModel = model("RouterModel", {
3808
3839
  if (result === false) {
3809
3840
  revert();
3810
3841
  } else if (typeof result === "string") {
3811
- self.push(result);
3842
+ self.replace(result);
3812
3843
  } else {
3813
3844
  proceed();
3814
3845
  }
@@ -3821,7 +3852,7 @@ var RouterModel = model("RouterModel", {
3821
3852
  self.setPopStateListener(handlePopState);
3822
3853
  }
3823
3854
  }).beforeDestroy((self) => {
3824
- if (typeof window !== "undefined" && self._popStateListener) {
3855
+ if (isBrowser && self._popStateListener) {
3825
3856
  window.removeEventListener("popstate", self._popStateListener);
3826
3857
  self.setPopStateListener(null);
3827
3858
  }
@@ -3835,7 +3866,7 @@ function createRouter(config) {
3835
3866
  initialPathname = parsed.pathname;
3836
3867
  initialSearch = parsed.search;
3837
3868
  initialHash = parsed.hash;
3838
- } else if (typeof window !== "undefined") {
3869
+ } else if (isBrowser) {
3839
3870
  initialPathname = window.location.pathname;
3840
3871
  initialSearch = window.location.search;
3841
3872
  initialHash = window.location.hash;
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as ILiteralType, a as IEnumerationType, b as IFrozenType, c as IType, d as ISimpleType, e as IIdentifierType, f as IIdentifierNumberType, M as ModelProperties, g as IModelType, h as MixinConfig, i as IMixin, j as IAnyType, k as IArrayType, l as IMapType, m as IOptionalType, n as IMaybeType, o as IMaybeNullType, p as IUnionType, U as UnionOptions, q as ILateType, r as IAnyModelType, R as ReferenceOptions, s as IReferenceType, t as ISafeReferenceType, u as IRefinementType, v as IDisposer, S as SnapshotIn, w as Instance } from './router-DzUdXVV7.mjs';
2
- export { L as CustomTypeOptions, aG as IActionRecorder, aH as IActionRecording, C as IAnyComplexType, D as IAnyMixin, aE as IHistoryEntry, F as IJsonPatch, y as IMSTArray, z as IMSTMap, G as IReversibleJsonPatch, x as IStateTreeNode, aF as ITimeTravelManager, aC as IUndoManager, aD as IUndoManagerOptions, H as IValidationContext, K as IValidationError, J as IValidationResult, A as ModelInstance, E as ModelSelf, aJ as RouteDefinition, aI as RouterModel, B as SnapshotOut, T as applyPatch, O as applySnapshot, aw as cleanupStaleEntries, ax as clearAllRegistries, aa as clone, aq as cloneDeep, aB as createActionRecorder, aK as createRouter, aA as createTimeTravelManager, az as createUndoManager, a8 as destroy, a9 as detach, am as findAll, an as findFirst, as as freeze, a2 as getEnv, ag as getGlobalStore, a4 as getIdentifier, af as getMembers, ar as getOrCreatePath, Y as getParent, $ as getParentOfType, a0 as getPath, a1 as getPathParts, av as getRegistryStats, aj as getRelativePath, X as getRoot, N as getSnapshot, ap as getTreeStats, a3 as getType, _ as hasParent, al as haveSameRoot, a5 as isAlive, ak as isAncestor, at as isFrozen, a6 as isRoot, a7 as isStateTreeNode, ao as isValidReference, W as onAction, ay as onLifecycleChange, Q as onPatch, P as onSnapshot, V as recordPatches, ai as resetGlobalStore, ae as resolveIdentifier, ac as resolvePath, ah as setGlobalStore, Z as tryGetParent, ad as tryResolve, au as unfreeze, ab as walk } from './router-DzUdXVV7.mjs';
1
+ import { I as IType, a as ISimpleType, b as ILiteralType, c as IEnumerationType, d as IFrozenType, e as IIdentifierType, f as IIdentifierNumberType, M as ModelProperties, g as IModelType, h as MixinConfig, i as IMixin, j as IAnyType, k as IArrayType, l as IMapType, m as IOptionalType, n as IMaybeType, o as IMaybeNullType, p as IUnionType, U as UnionOptions, q as ILateType, r as IAnyModelType, R as ReferenceOptions, s as IReferenceType, t as ISafeReferenceType, u as IRefinementType, v as IDisposer, S as SnapshotIn, w as Instance } from './router-9U4hkUrl.mjs';
2
+ export { C as CustomTypeOptions, x as IActionRecorder, y as IActionRecording, z as IAnyComplexType, A as IAnyMixin, B as IHistoryEntry, D as IJsonPatch, E as IMSTArray, F as IMSTMap, G as IReversibleJsonPatch, H as IStateTreeNode, J as ITimeTravelManager, K as IUndoManager, L as IUndoManagerOptions, N as IValidationContext, O as IValidationError, P as IValidationResult, Q as ModelInstance, T as ModelSelf, V as RouteDefinition, W as RouterModel, X as SnapshotOut, Y as applyPatch, Z as applySnapshot, _ as cleanupStaleEntries, $ as clearAllRegistries, a0 as clone, a1 as cloneDeep, a2 as createActionRecorder, a3 as createRouter, a4 as createTimeTravelManager, a5 as createUndoManager, a6 as destroy, a7 as detach, a8 as findAll, a9 as findFirst, aa as freeze, ab as getEnv, ac as getGlobalStore, ad as getIdentifier, ae as getMembers, af as getOrCreatePath, ag as getParent, ah as getParentOfType, ai as getPath, aj as getPathParts, ak as getRegistryStats, al as getRelativePath, am as getRoot, an as getSnapshot, ao as getTreeStats, ap as getType, aq as hasParent, ar as haveSameRoot, as as isAlive, at as isAncestor, au as isFrozen, av as isRoot, aw as isStateTreeNode, ax as isValidReference, ay as onAction, az as onLifecycleChange, aA as onPatch, aB as onSnapshot, aC as recordPatches, aD as resetGlobalStore, aE as resolveIdentifier, aF as resolvePath, aG as setGlobalStore, aH as tryGetParent, aI as tryResolve, aJ as unfreeze, aK as walk } from './router-9U4hkUrl.mjs';
3
3
  import 'jotai/vanilla/internals';
4
4
  import 'jotai';
5
5
  import 'react';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as ILiteralType, a as IEnumerationType, b as IFrozenType, c as IType, d as ISimpleType, e as IIdentifierType, f as IIdentifierNumberType, M as ModelProperties, g as IModelType, h as MixinConfig, i as IMixin, j as IAnyType, k as IArrayType, l as IMapType, m as IOptionalType, n as IMaybeType, o as IMaybeNullType, p as IUnionType, U as UnionOptions, q as ILateType, r as IAnyModelType, R as ReferenceOptions, s as IReferenceType, t as ISafeReferenceType, u as IRefinementType, v as IDisposer, S as SnapshotIn, w as Instance } from './router-DzUdXVV7.js';
2
- export { L as CustomTypeOptions, aG as IActionRecorder, aH as IActionRecording, C as IAnyComplexType, D as IAnyMixin, aE as IHistoryEntry, F as IJsonPatch, y as IMSTArray, z as IMSTMap, G as IReversibleJsonPatch, x as IStateTreeNode, aF as ITimeTravelManager, aC as IUndoManager, aD as IUndoManagerOptions, H as IValidationContext, K as IValidationError, J as IValidationResult, A as ModelInstance, E as ModelSelf, aJ as RouteDefinition, aI as RouterModel, B as SnapshotOut, T as applyPatch, O as applySnapshot, aw as cleanupStaleEntries, ax as clearAllRegistries, aa as clone, aq as cloneDeep, aB as createActionRecorder, aK as createRouter, aA as createTimeTravelManager, az as createUndoManager, a8 as destroy, a9 as detach, am as findAll, an as findFirst, as as freeze, a2 as getEnv, ag as getGlobalStore, a4 as getIdentifier, af as getMembers, ar as getOrCreatePath, Y as getParent, $ as getParentOfType, a0 as getPath, a1 as getPathParts, av as getRegistryStats, aj as getRelativePath, X as getRoot, N as getSnapshot, ap as getTreeStats, a3 as getType, _ as hasParent, al as haveSameRoot, a5 as isAlive, ak as isAncestor, at as isFrozen, a6 as isRoot, a7 as isStateTreeNode, ao as isValidReference, W as onAction, ay as onLifecycleChange, Q as onPatch, P as onSnapshot, V as recordPatches, ai as resetGlobalStore, ae as resolveIdentifier, ac as resolvePath, ah as setGlobalStore, Z as tryGetParent, ad as tryResolve, au as unfreeze, ab as walk } from './router-DzUdXVV7.js';
1
+ import { I as IType, a as ISimpleType, b as ILiteralType, c as IEnumerationType, d as IFrozenType, e as IIdentifierType, f as IIdentifierNumberType, M as ModelProperties, g as IModelType, h as MixinConfig, i as IMixin, j as IAnyType, k as IArrayType, l as IMapType, m as IOptionalType, n as IMaybeType, o as IMaybeNullType, p as IUnionType, U as UnionOptions, q as ILateType, r as IAnyModelType, R as ReferenceOptions, s as IReferenceType, t as ISafeReferenceType, u as IRefinementType, v as IDisposer, S as SnapshotIn, w as Instance } from './router-9U4hkUrl.js';
2
+ export { C as CustomTypeOptions, x as IActionRecorder, y as IActionRecording, z as IAnyComplexType, A as IAnyMixin, B as IHistoryEntry, D as IJsonPatch, E as IMSTArray, F as IMSTMap, G as IReversibleJsonPatch, H as IStateTreeNode, J as ITimeTravelManager, K as IUndoManager, L as IUndoManagerOptions, N as IValidationContext, O as IValidationError, P as IValidationResult, Q as ModelInstance, T as ModelSelf, V as RouteDefinition, W as RouterModel, X as SnapshotOut, Y as applyPatch, Z as applySnapshot, _ as cleanupStaleEntries, $ as clearAllRegistries, a0 as clone, a1 as cloneDeep, a2 as createActionRecorder, a3 as createRouter, a4 as createTimeTravelManager, a5 as createUndoManager, a6 as destroy, a7 as detach, a8 as findAll, a9 as findFirst, aa as freeze, ab as getEnv, ac as getGlobalStore, ad as getIdentifier, ae as getMembers, af as getOrCreatePath, ag as getParent, ah as getParentOfType, ai as getPath, aj as getPathParts, ak as getRegistryStats, al as getRelativePath, am as getRoot, an as getSnapshot, ao as getTreeStats, ap as getType, aq as hasParent, ar as haveSameRoot, as as isAlive, at as isAncestor, au as isFrozen, av as isRoot, aw as isStateTreeNode, ax as isValidReference, ay as onAction, az as onLifecycleChange, aA as onPatch, aB as onSnapshot, aC as recordPatches, aD as resetGlobalStore, aE as resolveIdentifier, aF as resolvePath, aG as setGlobalStore, aH as tryGetParent, aI as tryResolve, aJ as unfreeze, aK as walk } from './router-9U4hkUrl.js';
3
3
  import 'jotai/vanilla/internals';
4
4
  import 'jotai';
5
5
  import 'react';
package/dist/index.js CHANGED
@@ -4338,6 +4338,7 @@ function createActionRecorder(target) {
4338
4338
  // src/router.ts
4339
4339
  var import_react = require("react");
4340
4340
  var import_jotai4 = require("jotai");
4341
+ var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.location !== "undefined" && typeof window.history !== "undefined";
4341
4342
  var RouteDefinition = model("RouteDefinition", {
4342
4343
  path: string,
4343
4344
  name: string,
@@ -4465,7 +4466,9 @@ var RouterModel = model("RouterModel", {
4465
4466
  })).volatile(() => ({
4466
4467
  beforeNavigate: null,
4467
4468
  afterNavigate: null,
4468
- _popStateListener: null
4469
+ _popStateListener: null,
4470
+ _historyStack: [],
4471
+ _historyIndex: -1
4469
4472
  })).actions((self) => {
4470
4473
  return {
4471
4474
  setGuards(before, after) {
@@ -4485,13 +4488,30 @@ var RouterModel = model("RouterModel", {
4485
4488
  self.params = matched ? matched.params : {};
4486
4489
  self.query = parsed.query;
4487
4490
  self.currentRouteName = matched ? matched.route.name : null;
4488
- if (typeof window !== "undefined" && window.history) {
4491
+ if (isBrowser) {
4489
4492
  const fullPath = pathname + search + hash;
4490
4493
  if (action === "PUSH") {
4491
4494
  window.history.pushState(state, "", fullPath);
4492
4495
  } else if (action === "REPLACE") {
4493
4496
  window.history.replaceState(state, "", fullPath);
4494
4497
  }
4498
+ } else {
4499
+ const fullPath = pathname + search + hash;
4500
+ if (action === "PUSH") {
4501
+ self._historyStack = self._historyStack.slice(0, self._historyIndex + 1);
4502
+ self._historyStack.push(fullPath);
4503
+ self._historyIndex = self._historyStack.length - 1;
4504
+ } else if (action === "REPLACE") {
4505
+ if (self._historyIndex === -1) {
4506
+ self._historyStack = [fullPath];
4507
+ self._historyIndex = 0;
4508
+ } else {
4509
+ self._historyStack[self._historyIndex] = fullPath;
4510
+ }
4511
+ } else if (action === "INITIAL") {
4512
+ self._historyStack = [fullPath];
4513
+ self._historyIndex = 0;
4514
+ }
4495
4515
  }
4496
4516
  },
4497
4517
  push: flow(function* (path, state) {
@@ -4565,23 +4585,34 @@ var RouterModel = model("RouterModel", {
4565
4585
  }
4566
4586
  }),
4567
4587
  go(delta) {
4568
- if (typeof window !== "undefined" && window.history) {
4588
+ if (isBrowser) {
4569
4589
  window.history.go(delta);
4590
+ } else {
4591
+ const nextIndex = self._historyIndex + delta;
4592
+ if (nextIndex >= 0 && nextIndex < self._historyStack.length) {
4593
+ self._historyIndex = nextIndex;
4594
+ const targetPath = self._historyStack[nextIndex];
4595
+ self.syncLocation(targetPath, "", "", "POP");
4596
+ }
4570
4597
  }
4571
4598
  },
4572
4599
  goBack() {
4573
- if (typeof window !== "undefined" && window.history) {
4600
+ if (isBrowser) {
4574
4601
  window.history.back();
4602
+ } else {
4603
+ self.go(-1);
4575
4604
  }
4576
4605
  },
4577
4606
  goForward() {
4578
- if (typeof window !== "undefined" && window.history) {
4607
+ if (isBrowser) {
4579
4608
  window.history.forward();
4609
+ } else {
4610
+ self.go(1);
4580
4611
  }
4581
4612
  }
4582
4613
  };
4583
4614
  }).afterCreate((self) => {
4584
- if (typeof window !== "undefined") {
4615
+ if (isBrowser) {
4585
4616
  const handlePopState = (event) => {
4586
4617
  const parsed = parseUrl(window.location.pathname + window.location.search + window.location.hash);
4587
4618
  const matched = matchRoutes(self.routes, parsed.pathname);
@@ -4619,7 +4650,7 @@ var RouterModel = model("RouterModel", {
4619
4650
  if (res === false) {
4620
4651
  revert();
4621
4652
  } else if (typeof res === "string") {
4622
- self.push(res);
4653
+ self.replace(res);
4623
4654
  } else {
4624
4655
  proceed();
4625
4656
  }
@@ -4630,7 +4661,7 @@ var RouterModel = model("RouterModel", {
4630
4661
  if (result === false) {
4631
4662
  revert();
4632
4663
  } else if (typeof result === "string") {
4633
- self.push(result);
4664
+ self.replace(result);
4634
4665
  } else {
4635
4666
  proceed();
4636
4667
  }
@@ -4643,7 +4674,7 @@ var RouterModel = model("RouterModel", {
4643
4674
  self.setPopStateListener(handlePopState);
4644
4675
  }
4645
4676
  }).beforeDestroy((self) => {
4646
- if (typeof window !== "undefined" && self._popStateListener) {
4677
+ if (isBrowser && self._popStateListener) {
4647
4678
  window.removeEventListener("popstate", self._popStateListener);
4648
4679
  self.setPopStateListener(null);
4649
4680
  }
@@ -4657,7 +4688,7 @@ function createRouter(config) {
4657
4688
  initialPathname = parsed.pathname;
4658
4689
  initialSearch = parsed.search;
4659
4690
  initialHash = parsed.hash;
4660
- } else if (typeof window !== "undefined") {
4691
+ } else if (isBrowser) {
4661
4692
  initialPathname = window.location.pathname;
4662
4693
  initialSearch = window.location.search;
4663
4694
  initialHash = window.location.hash;
package/dist/index.mjs CHANGED
@@ -96,7 +96,7 @@ import {
96
96
  union,
97
97
  unprotect,
98
98
  walk
99
- } from "./chunk-WEWWBVTQ.mjs";
99
+ } from "./chunk-MGW4FLIA.mjs";
100
100
 
101
101
  // src/map.ts
102
102
  var MSTMap = class extends Map {
package/dist/react.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import React, { ComponentType, FC, ReactNode } from 'react';
2
- import { ag as getGlobalStore, aD as IUndoManagerOptions, aC as IUndoManager, aF as ITimeTravelManager } from './router-DzUdXVV7.mjs';
3
- export { aM as RouterContext, aL as hasStateTreeNode, aN as useRouter } from './router-DzUdXVV7.mjs';
1
+ import React, { FC, ReactNode, ComponentType } from 'react';
2
+ import { ac as getGlobalStore, J as ITimeTravelManager, L as IUndoManagerOptions, K as IUndoManager } from './router-9U4hkUrl.mjs';
3
+ export { aL as RouterContext, aM as hasStateTreeNode, aN as useRouter } from './router-9U4hkUrl.mjs';
4
4
  import 'jotai/vanilla/internals';
5
5
  import 'jotai';
6
6
 
package/dist/react.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import React, { ComponentType, FC, ReactNode } from 'react';
2
- import { ag as getGlobalStore, aD as IUndoManagerOptions, aC as IUndoManager, aF as ITimeTravelManager } from './router-DzUdXVV7.js';
3
- export { aM as RouterContext, aL as hasStateTreeNode, aN as useRouter } from './router-DzUdXVV7.js';
1
+ import React, { FC, ReactNode, ComponentType } from 'react';
2
+ import { ac as getGlobalStore, J as ITimeTravelManager, L as IUndoManagerOptions, K as IUndoManager } from './router-9U4hkUrl.js';
3
+ export { aL as RouterContext, aM as hasStateTreeNode, aN as useRouter } from './router-9U4hkUrl.js';
4
4
  import 'jotai/vanilla/internals';
5
5
  import 'jotai';
6
6
 
package/dist/react.js CHANGED
@@ -2467,6 +2467,7 @@ function maybeNull(type) {
2467
2467
  }
2468
2468
 
2469
2469
  // src/router.ts
2470
+ var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.location !== "undefined" && typeof window.history !== "undefined";
2470
2471
  var RouteDefinition = model("RouteDefinition", {
2471
2472
  path: string,
2472
2473
  name: string,
@@ -2594,7 +2595,9 @@ var RouterModel = model("RouterModel", {
2594
2595
  })).volatile(() => ({
2595
2596
  beforeNavigate: null,
2596
2597
  afterNavigate: null,
2597
- _popStateListener: null
2598
+ _popStateListener: null,
2599
+ _historyStack: [],
2600
+ _historyIndex: -1
2598
2601
  })).actions((self) => {
2599
2602
  return {
2600
2603
  setGuards(before, after) {
@@ -2614,13 +2617,30 @@ var RouterModel = model("RouterModel", {
2614
2617
  self.params = matched ? matched.params : {};
2615
2618
  self.query = parsed.query;
2616
2619
  self.currentRouteName = matched ? matched.route.name : null;
2617
- if (typeof window !== "undefined" && window.history) {
2620
+ if (isBrowser) {
2618
2621
  const fullPath = pathname + search + hash;
2619
2622
  if (action === "PUSH") {
2620
2623
  window.history.pushState(state, "", fullPath);
2621
2624
  } else if (action === "REPLACE") {
2622
2625
  window.history.replaceState(state, "", fullPath);
2623
2626
  }
2627
+ } else {
2628
+ const fullPath = pathname + search + hash;
2629
+ if (action === "PUSH") {
2630
+ self._historyStack = self._historyStack.slice(0, self._historyIndex + 1);
2631
+ self._historyStack.push(fullPath);
2632
+ self._historyIndex = self._historyStack.length - 1;
2633
+ } else if (action === "REPLACE") {
2634
+ if (self._historyIndex === -1) {
2635
+ self._historyStack = [fullPath];
2636
+ self._historyIndex = 0;
2637
+ } else {
2638
+ self._historyStack[self._historyIndex] = fullPath;
2639
+ }
2640
+ } else if (action === "INITIAL") {
2641
+ self._historyStack = [fullPath];
2642
+ self._historyIndex = 0;
2643
+ }
2624
2644
  }
2625
2645
  },
2626
2646
  push: flow(function* (path, state) {
@@ -2694,23 +2714,34 @@ var RouterModel = model("RouterModel", {
2694
2714
  }
2695
2715
  }),
2696
2716
  go(delta) {
2697
- if (typeof window !== "undefined" && window.history) {
2717
+ if (isBrowser) {
2698
2718
  window.history.go(delta);
2719
+ } else {
2720
+ const nextIndex = self._historyIndex + delta;
2721
+ if (nextIndex >= 0 && nextIndex < self._historyStack.length) {
2722
+ self._historyIndex = nextIndex;
2723
+ const targetPath = self._historyStack[nextIndex];
2724
+ self.syncLocation(targetPath, "", "", "POP");
2725
+ }
2699
2726
  }
2700
2727
  },
2701
2728
  goBack() {
2702
- if (typeof window !== "undefined" && window.history) {
2729
+ if (isBrowser) {
2703
2730
  window.history.back();
2731
+ } else {
2732
+ self.go(-1);
2704
2733
  }
2705
2734
  },
2706
2735
  goForward() {
2707
- if (typeof window !== "undefined" && window.history) {
2736
+ if (isBrowser) {
2708
2737
  window.history.forward();
2738
+ } else {
2739
+ self.go(1);
2709
2740
  }
2710
2741
  }
2711
2742
  };
2712
2743
  }).afterCreate((self) => {
2713
- if (typeof window !== "undefined") {
2744
+ if (isBrowser) {
2714
2745
  const handlePopState = (event) => {
2715
2746
  const parsed = parseUrl(window.location.pathname + window.location.search + window.location.hash);
2716
2747
  const matched = matchRoutes(self.routes, parsed.pathname);
@@ -2748,7 +2779,7 @@ var RouterModel = model("RouterModel", {
2748
2779
  if (res === false) {
2749
2780
  revert();
2750
2781
  } else if (typeof res === "string") {
2751
- self.push(res);
2782
+ self.replace(res);
2752
2783
  } else {
2753
2784
  proceed();
2754
2785
  }
@@ -2759,7 +2790,7 @@ var RouterModel = model("RouterModel", {
2759
2790
  if (result === false) {
2760
2791
  revert();
2761
2792
  } else if (typeof result === "string") {
2762
- self.push(result);
2793
+ self.replace(result);
2763
2794
  } else {
2764
2795
  proceed();
2765
2796
  }
@@ -2772,7 +2803,7 @@ var RouterModel = model("RouterModel", {
2772
2803
  self.setPopStateListener(handlePopState);
2773
2804
  }
2774
2805
  }).beforeDestroy((self) => {
2775
- if (typeof window !== "undefined" && self._popStateListener) {
2806
+ if (isBrowser && self._popStateListener) {
2776
2807
  window.removeEventListener("popstate", self._popStateListener);
2777
2808
  self.setPopStateListener(null);
2778
2809
  }
package/dist/react.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  setActiveTrackingFn,
13
13
  setIsApplyingSnapshotOrPatch,
14
14
  useRouter
15
- } from "./chunk-WEWWBVTQ.mjs";
15
+ } from "./chunk-MGW4FLIA.mjs";
16
16
 
17
17
  // src/react.ts
18
18
  import React, {
@@ -674,6 +674,8 @@ declare const RouterModel: IModelType<{
674
674
  beforeNavigate: ((from: any, to: any) => boolean | string | Promise<boolean | string> | undefined) | null;
675
675
  afterNavigate: ((to: any) => void) | null;
676
676
  _popStateListener: ((event: PopStateEvent) => void) | null;
677
+ _historyStack: string[];
678
+ _historyIndex: number;
677
679
  }>;
678
680
  declare function createRouter(config: {
679
681
  routes: Array<{
@@ -721,8 +723,10 @@ declare function createRouter(config: {
721
723
  beforeNavigate: ((from: any, to: any) => boolean | string | Promise<boolean | string> | undefined) | null;
722
724
  afterNavigate: ((to: any) => void) | null;
723
725
  _popStateListener: ((event: PopStateEvent) => void) | null;
726
+ _historyStack: string[];
727
+ _historyIndex: number;
724
728
  };
725
729
  declare const RouterContext: React.Context<any>;
726
730
  declare function useRouter(): any;
727
731
 
728
- export { getParentOfType as $, type ModelInstance as A, type SnapshotOut as B, type IAnyComplexType as C, type IAnyMixin as D, type ModelSelf as E, type IJsonPatch as F, type IReversibleJsonPatch as G, type IValidationContext as H, type ILiteralType as I, type IValidationResult as J, type IValidationError as K, type CustomTypeOptions as L, type ModelProperties as M, getSnapshot as N, applySnapshot as O, onSnapshot as P, onPatch as Q, type ReferenceOptions as R, type SnapshotIn as S, applyPatch as T, type UnionOptions as U, recordPatches as V, onAction as W, getRoot as X, getParent as Y, tryGetParent as Z, hasParent as _, type IEnumerationType as a, getPath as a0, getPathParts as a1, getEnv as a2, getType as a3, getIdentifier as a4, isAlive as a5, isRoot as a6, isStateTreeNode as a7, destroy as a8, detach as a9, createTimeTravelManager as aA, createActionRecorder as aB, type IUndoManager as aC, type IUndoManagerOptions as aD, type IHistoryEntry as aE, type ITimeTravelManager as aF, type IActionRecorder as aG, type IActionRecording as aH, RouterModel as aI, RouteDefinition as aJ, createRouter as aK, hasStateTreeNode as aL, RouterContext as aM, useRouter as aN, clone as aa, walk as ab, resolvePath as ac, tryResolve as ad, resolveIdentifier as ae, getMembers as af, getGlobalStore as ag, setGlobalStore as ah, resetGlobalStore as ai, getRelativePath as aj, isAncestor as ak, haveSameRoot as al, findAll as am, findFirst as an, isValidReference as ao, getTreeStats as ap, cloneDeep as aq, getOrCreatePath as ar, freeze as as, isFrozen as at, unfreeze as au, getRegistryStats as av, cleanupStaleEntries as aw, clearAllRegistries as ax, onLifecycleChange as ay, createUndoManager as az, type IFrozenType as b, type IType as c, type ISimpleType as d, type IIdentifierType as e, type IIdentifierNumberType as f, type IModelType as g, type MixinConfig as h, type IMixin as i, type IAnyType as j, type IArrayType as k, type IMapType as l, type IOptionalType as m, type IMaybeType as n, type IMaybeNullType as o, type IUnionType as p, type ILateType as q, type IAnyModelType as r, type IReferenceType as s, type ISafeReferenceType as t, type IRefinementType as u, type IDisposer as v, type Instance as w, type IStateTreeNode as x, type IMSTArray as y, type IMSTMap as z };
732
+ export { clearAllRegistries as $, type IAnyMixin as A, type IHistoryEntry as B, type CustomTypeOptions as C, type IJsonPatch as D, type IMSTArray as E, type IMSTMap as F, type IReversibleJsonPatch as G, type IStateTreeNode as H, type IType as I, type ITimeTravelManager as J, type IUndoManager as K, type IUndoManagerOptions as L, type ModelProperties as M, type IValidationContext as N, type IValidationError as O, type IValidationResult as P, type ModelInstance as Q, type ReferenceOptions as R, type SnapshotIn as S, type ModelSelf as T, type UnionOptions as U, RouteDefinition as V, RouterModel as W, type SnapshotOut as X, applyPatch as Y, applySnapshot as Z, cleanupStaleEntries as _, type ISimpleType as a, clone as a0, cloneDeep as a1, createActionRecorder as a2, createRouter as a3, createTimeTravelManager as a4, createUndoManager as a5, destroy as a6, detach as a7, findAll as a8, findFirst as a9, onPatch as aA, onSnapshot as aB, recordPatches as aC, resetGlobalStore as aD, resolveIdentifier as aE, resolvePath as aF, setGlobalStore as aG, tryGetParent as aH, tryResolve as aI, unfreeze as aJ, walk as aK, RouterContext as aL, hasStateTreeNode as aM, useRouter as aN, freeze as aa, getEnv as ab, getGlobalStore as ac, getIdentifier as ad, getMembers as ae, getOrCreatePath as af, getParent as ag, getParentOfType as ah, getPath as ai, getPathParts as aj, getRegistryStats as ak, getRelativePath as al, getRoot as am, getSnapshot as an, getTreeStats as ao, getType as ap, hasParent as aq, haveSameRoot as ar, isAlive as as, isAncestor as at, isFrozen as au, isRoot as av, isStateTreeNode as aw, isValidReference as ax, onAction as ay, onLifecycleChange as az, type ILiteralType as b, type IEnumerationType as c, type IFrozenType as d, type IIdentifierType as e, type IIdentifierNumberType as f, type IModelType as g, type MixinConfig as h, type IMixin as i, type IAnyType as j, type IArrayType as k, type IMapType as l, type IOptionalType as m, type IMaybeType as n, type IMaybeNullType as o, type IUnionType as p, type ILateType as q, type IAnyModelType as r, type IReferenceType as s, type ISafeReferenceType as t, type IRefinementType as u, type IDisposer as v, type Instance as w, type IActionRecorder as x, type IActionRecording as y, type IAnyComplexType as z };
@@ -674,6 +674,8 @@ declare const RouterModel: IModelType<{
674
674
  beforeNavigate: ((from: any, to: any) => boolean | string | Promise<boolean | string> | undefined) | null;
675
675
  afterNavigate: ((to: any) => void) | null;
676
676
  _popStateListener: ((event: PopStateEvent) => void) | null;
677
+ _historyStack: string[];
678
+ _historyIndex: number;
677
679
  }>;
678
680
  declare function createRouter(config: {
679
681
  routes: Array<{
@@ -721,8 +723,10 @@ declare function createRouter(config: {
721
723
  beforeNavigate: ((from: any, to: any) => boolean | string | Promise<boolean | string> | undefined) | null;
722
724
  afterNavigate: ((to: any) => void) | null;
723
725
  _popStateListener: ((event: PopStateEvent) => void) | null;
726
+ _historyStack: string[];
727
+ _historyIndex: number;
724
728
  };
725
729
  declare const RouterContext: React.Context<any>;
726
730
  declare function useRouter(): any;
727
731
 
728
- export { getParentOfType as $, type ModelInstance as A, type SnapshotOut as B, type IAnyComplexType as C, type IAnyMixin as D, type ModelSelf as E, type IJsonPatch as F, type IReversibleJsonPatch as G, type IValidationContext as H, type ILiteralType as I, type IValidationResult as J, type IValidationError as K, type CustomTypeOptions as L, type ModelProperties as M, getSnapshot as N, applySnapshot as O, onSnapshot as P, onPatch as Q, type ReferenceOptions as R, type SnapshotIn as S, applyPatch as T, type UnionOptions as U, recordPatches as V, onAction as W, getRoot as X, getParent as Y, tryGetParent as Z, hasParent as _, type IEnumerationType as a, getPath as a0, getPathParts as a1, getEnv as a2, getType as a3, getIdentifier as a4, isAlive as a5, isRoot as a6, isStateTreeNode as a7, destroy as a8, detach as a9, createTimeTravelManager as aA, createActionRecorder as aB, type IUndoManager as aC, type IUndoManagerOptions as aD, type IHistoryEntry as aE, type ITimeTravelManager as aF, type IActionRecorder as aG, type IActionRecording as aH, RouterModel as aI, RouteDefinition as aJ, createRouter as aK, hasStateTreeNode as aL, RouterContext as aM, useRouter as aN, clone as aa, walk as ab, resolvePath as ac, tryResolve as ad, resolveIdentifier as ae, getMembers as af, getGlobalStore as ag, setGlobalStore as ah, resetGlobalStore as ai, getRelativePath as aj, isAncestor as ak, haveSameRoot as al, findAll as am, findFirst as an, isValidReference as ao, getTreeStats as ap, cloneDeep as aq, getOrCreatePath as ar, freeze as as, isFrozen as at, unfreeze as au, getRegistryStats as av, cleanupStaleEntries as aw, clearAllRegistries as ax, onLifecycleChange as ay, createUndoManager as az, type IFrozenType as b, type IType as c, type ISimpleType as d, type IIdentifierType as e, type IIdentifierNumberType as f, type IModelType as g, type MixinConfig as h, type IMixin as i, type IAnyType as j, type IArrayType as k, type IMapType as l, type IOptionalType as m, type IMaybeType as n, type IMaybeNullType as o, type IUnionType as p, type ILateType as q, type IAnyModelType as r, type IReferenceType as s, type ISafeReferenceType as t, type IRefinementType as u, type IDisposer as v, type Instance as w, type IStateTreeNode as x, type IMSTArray as y, type IMSTMap as z };
732
+ export { clearAllRegistries as $, type IAnyMixin as A, type IHistoryEntry as B, type CustomTypeOptions as C, type IJsonPatch as D, type IMSTArray as E, type IMSTMap as F, type IReversibleJsonPatch as G, type IStateTreeNode as H, type IType as I, type ITimeTravelManager as J, type IUndoManager as K, type IUndoManagerOptions as L, type ModelProperties as M, type IValidationContext as N, type IValidationError as O, type IValidationResult as P, type ModelInstance as Q, type ReferenceOptions as R, type SnapshotIn as S, type ModelSelf as T, type UnionOptions as U, RouteDefinition as V, RouterModel as W, type SnapshotOut as X, applyPatch as Y, applySnapshot as Z, cleanupStaleEntries as _, type ISimpleType as a, clone as a0, cloneDeep as a1, createActionRecorder as a2, createRouter as a3, createTimeTravelManager as a4, createUndoManager as a5, destroy as a6, detach as a7, findAll as a8, findFirst as a9, onPatch as aA, onSnapshot as aB, recordPatches as aC, resetGlobalStore as aD, resolveIdentifier as aE, resolvePath as aF, setGlobalStore as aG, tryGetParent as aH, tryResolve as aI, unfreeze as aJ, walk as aK, RouterContext as aL, hasStateTreeNode as aM, useRouter as aN, freeze as aa, getEnv as ab, getGlobalStore as ac, getIdentifier as ad, getMembers as ae, getOrCreatePath as af, getParent as ag, getParentOfType as ah, getPath as ai, getPathParts as aj, getRegistryStats as ak, getRelativePath as al, getRoot as am, getSnapshot as an, getTreeStats as ao, getType as ap, hasParent as aq, haveSameRoot as ar, isAlive as as, isAncestor as at, isFrozen as au, isRoot as av, isStateTreeNode as aw, isValidReference as ax, onAction as ay, onLifecycleChange as az, type ILiteralType as b, type IEnumerationType as c, type IFrozenType as d, type IIdentifierType as e, type IIdentifierNumberType as f, type IModelType as g, type MixinConfig as h, type IMixin as i, type IAnyType as j, type IArrayType as k, type IMapType as l, type IOptionalType as m, type IMaybeType as n, type IMaybeNullType as o, type IUnionType as p, type ILateType as q, type IAnyModelType as r, type IReferenceType as s, type ISafeReferenceType as t, type IRefinementType as u, type IDisposer as v, type Instance as w, type IActionRecorder as x, type IActionRecording as y, type IAnyComplexType as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jotai-state-tree",
3
- "version": "1.7.4",
3
+ "version": "1.8.0",
4
4
  "description": "MobX-State-Tree API compatible library powered by Jotai",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -77,7 +77,7 @@
77
77
  "semantic-release": "^25.0.2",
78
78
  "tsup": "^8.0.0",
79
79
  "typedoc": "^0.28.15",
80
- "typescript": "^5.3.0",
80
+ "typescript": "^6.0.3",
81
81
  "vitest": "^4.0.16"
82
82
  },
83
83
  "repository": {
@@ -57,8 +57,8 @@ describe("Performance", () => {
57
57
  const elapsed = performance.now() - start;
58
58
 
59
59
  expect(instances.length).toBe(10000);
60
- // Should complete in reasonable time (less than 5 seconds on most machines)
61
- expect(elapsed).toBeLessThan(5000);
60
+ // Should complete in reasonable time (less than 5 seconds on most machines, relaxed for CI runners)
61
+ expect(elapsed).toBeLessThan(15000);
62
62
 
63
63
  // Cleanup
64
64
  instances.forEach((i) => destroy(i));
@@ -84,7 +84,7 @@ describe("Performance", () => {
84
84
  const tree = Branch.create(createTree(10)); // 2^10 = 1024 leaf nodes
85
85
  const elapsed = performance.now() - start;
86
86
 
87
- expect(elapsed).toBeLessThan(5000);
87
+ expect(elapsed).toBeLessThan(15000);
88
88
 
89
89
  destroy(tree);
90
90
  });
@@ -109,7 +109,7 @@ describe("Performance", () => {
109
109
  const elapsed = performance.now() - start;
110
110
 
111
111
  expect(list.items.length).toBe(10000);
112
- expect(elapsed).toBeLessThan(5000);
112
+ expect(elapsed).toBeLessThan(15000);
113
113
 
114
114
  destroy(list);
115
115
  });
@@ -138,7 +138,7 @@ describe("Performance", () => {
138
138
  const elapsed = performance.now() - start;
139
139
 
140
140
  expect(counter.value).toBe(10000);
141
- expect(elapsed).toBeLessThan(2000);
141
+ expect(elapsed).toBeLessThan(10000);
142
142
 
143
143
  destroy(counter);
144
144
  });
@@ -181,7 +181,7 @@ describe("Performance", () => {
181
181
  const elapsed = performance.now() - start;
182
182
 
183
183
  expect(list.items.length).toBe(500);
184
- expect(elapsed).toBeLessThan(5000);
184
+ expect(elapsed).toBeLessThan(15000);
185
185
 
186
186
  destroy(list);
187
187
  });
@@ -218,7 +218,7 @@ describe("Performance", () => {
218
218
 
219
219
  const elapsed = performance.now() - start;
220
220
 
221
- expect(elapsed).toBeLessThan(5000);
221
+ expect(elapsed).toBeLessThan(15000);
222
222
 
223
223
  destroy(instance);
224
224
  });
@@ -252,7 +252,7 @@ describe("Performance", () => {
252
252
 
253
253
  const elapsed = performance.now() - start;
254
254
 
255
- expect(elapsed).toBeLessThan(2000);
255
+ expect(elapsed).toBeLessThan(10000);
256
256
 
257
257
  destroy(store);
258
258
  });
@@ -294,7 +294,7 @@ describe("Performance", () => {
294
294
  const elapsed = performance.now() - start;
295
295
 
296
296
  expect(callCount).toBe(10000); // 100 listeners * 100 updates
297
- expect(elapsed).toBeLessThan(2000);
297
+ expect(elapsed).toBeLessThan(10000);
298
298
 
299
299
  // Cleanup
300
300
  disposers.forEach((d) => d());
@@ -334,7 +334,7 @@ describe("Performance", () => {
334
334
  const elapsed = performance.now() - start;
335
335
 
336
336
  expect(patchCount).toBe(10000);
337
- expect(elapsed).toBeLessThan(2000);
337
+ expect(elapsed).toBeLessThan(10000);
338
338
 
339
339
  disposers.forEach((d) => d());
340
340
  destroy(instance);
@@ -369,7 +369,7 @@ describe("Performance", () => {
369
369
  const elapsed = performance.now() - start;
370
370
 
371
371
  expect(clones.length).toBe(10);
372
- expect(elapsed).toBeLessThan(3000);
372
+ expect(elapsed).toBeLessThan(15000);
373
373
 
374
374
  // Cleanup
375
375
  destroy(original);
@@ -633,8 +633,8 @@ describe("Stress Tests", () => {
633
633
  const elapsed = performance.now() - start;
634
634
 
635
635
  expect(lastResult).toBe("Alice");
636
- // Reference resolution should be extremely fast (less than 1 second for 50k calls)
637
- expect(elapsed).toBeLessThan(1000);
636
+ // Reference resolution should be extremely fast (less than 1 second for 50k calls, relaxed for CI)
637
+ expect(elapsed).toBeLessThan(5000);
638
638
 
639
639
  destroy(user);
640
640
  destroy(post);
@@ -678,8 +678,8 @@ describe("Stress Tests", () => {
678
678
  const elapsed = performance.now() - start;
679
679
 
680
680
  expect(matchCount).toBe(10000);
681
- // Resolving 10,000 distinct references should be fast (less than 1.5 seconds)
682
- expect(elapsed).toBeLessThan(1500);
681
+ // Resolving 10,000 distinct references should be fast (less than 1.5 seconds, relaxed for CI)
682
+ expect(elapsed).toBeLessThan(8000);
683
683
 
684
684
  // Cleanup
685
685
  posts.forEach((p) => destroy(p));
@@ -421,6 +421,8 @@ describe('State Router', () => {
421
421
  popStateCallback(new PopStateEvent('popstate', { state: null }));
422
422
  });
423
423
  expect(router.pathname).toBe('/users/redirected');
424
+ expect(window.history.replaceState).toHaveBeenCalledWith(undefined, '', '/users/redirected');
425
+ expect(window.history.pushState).not.toHaveBeenCalled();
424
426
 
425
427
  // 2. PopState promise rejection (should revert URL)
426
428
  vi.stubGlobal('location', { pathname: '/files/error', search: '', hash: '' });
@@ -533,6 +535,8 @@ describe('State Router', () => {
533
535
  popStateCallback(new PopStateEvent('popstate', { state: null }));
534
536
  });
535
537
  expect(r4.pathname).toBe('/users/redirected');
538
+ expect(window.history.replaceState).toHaveBeenCalledWith(undefined, '', '/users/redirected');
539
+ expect(window.history.pushState).not.toHaveBeenCalled();
536
540
 
537
541
  // 5. popstate with async beforeNavigate resolving to false
538
542
  const r5 = createRouter({
@@ -277,15 +277,98 @@ describe('Utility Types Extra', () => {
277
277
  expect(OnlyPostProcessor.validate({ name: 'bob' }, []).valid).toBe(true);
278
278
  });
279
279
 
280
- it('router in node environment (window undefined branches)', () => {
280
+ it('router in node environment (in-memory stack routing)', () => {
281
+ const routes = [
282
+ { path: '/', name: 'home' },
283
+ { path: '/about', name: 'about' },
284
+ { path: '/users/:id', name: 'user-profile' },
285
+ { path: '/contact', name: 'contact' },
286
+ { path: '/help', name: 'help' },
287
+ ];
281
288
  const r = createRouter({
282
- routes: [{ path: '/', name: 'home' }],
289
+ routes,
290
+ initialUrl: '/',
283
291
  });
292
+
293
+ expect(r.pathname).toBe('/');
294
+ expect((r as any)._historyStack).toEqual(['/']);
295
+ expect((r as any)._historyIndex).toBe(0);
296
+
297
+ // Push new path
298
+ r.syncLocation('/about', '', '', 'PUSH');
299
+ expect(r.pathname).toBe('/about');
300
+ expect((r as any)._historyStack).toEqual(['/', '/about']);
301
+ expect((r as any)._historyIndex).toBe(1);
302
+
303
+ // Push another path
304
+ r.syncLocation('/users/123', '', '', 'PUSH');
305
+ expect(r.pathname).toBe('/users/123');
306
+ expect(r.params).toEqual({ id: '123' });
307
+ expect((r as any)._historyStack).toEqual(['/', '/about', '/users/123']);
308
+ expect((r as any)._historyIndex).toBe(2);
309
+
310
+ // Go back
311
+ r.goBack();
312
+ expect(r.pathname).toBe('/about');
313
+ expect((r as any)._historyIndex).toBe(1);
314
+
315
+ // Go forward
316
+ r.goForward();
317
+ expect(r.pathname).toBe('/users/123');
318
+ expect((r as any)._historyIndex).toBe(2);
319
+
320
+ // Go back 2 steps
321
+ r.go(-2);
284
322
  expect(r.pathname).toBe('/');
285
- expect(() => r.syncLocation('/about', '', '', 'PUSH')).not.toThrow();
286
- expect(() => r.go(1)).not.toThrow();
287
- expect(() => r.goBack()).not.toThrow();
288
- expect(() => r.goForward()).not.toThrow();
323
+ expect((r as any)._historyIndex).toBe(0);
324
+
325
+ // Push from middle (should truncate forward stack)
326
+ r.syncLocation('/contact', '', '', 'PUSH');
327
+ expect(r.pathname).toBe('/contact');
328
+ expect((r as any)._historyStack).toEqual(['/', '/contact']);
329
+ expect((r as any)._historyIndex).toBe(1);
330
+
331
+ // Replace current path
332
+ r.syncLocation('/help', '', '', 'REPLACE');
333
+ expect(r.pathname).toBe('/help');
334
+ expect((r as any)._historyStack).toEqual(['/', '/help']);
335
+ expect((r as any)._historyIndex).toBe(1);
336
+ });
337
+
338
+ it('router in environment where window is defined but document is undefined (e.g., React Native remote debugger)', () => {
339
+ const originalWindow = (global as any).window;
340
+ try {
341
+ // Mock window without document (React Native remote debugger environment)
342
+ (global as any).window = {};
343
+
344
+ const routes = [
345
+ { path: '/', name: 'home' },
346
+ { path: '/about', name: 'about' },
347
+ ];
348
+ const r = createRouter({
349
+ routes,
350
+ initialUrl: '/',
351
+ });
352
+
353
+ expect(r.pathname).toBe('/');
354
+ expect((r as any)._historyStack).toEqual(['/']);
355
+
356
+ // Should not throw or crash and behave as in-memory router
357
+ expect(() => r.syncLocation('/about', '', '', 'PUSH')).not.toThrow();
358
+ expect(r.pathname).toBe('/about');
359
+ expect((r as any)._historyStack).toEqual(['/', '/about']);
360
+ expect((r as any)._historyIndex).toBe(1);
361
+
362
+ expect(() => r.goBack()).not.toThrow();
363
+ expect(r.pathname).toBe('/');
364
+ expect((r as any)._historyIndex).toBe(0);
365
+ } finally {
366
+ if (originalWindow === undefined) {
367
+ delete (global as any).window;
368
+ } else {
369
+ (global as any).window = originalWindow;
370
+ }
371
+ }
289
372
  });
290
373
  });
291
374
 
package/src/router.ts CHANGED
@@ -7,6 +7,12 @@ import { optional, maybeNull } from "./utilities";
7
7
  import { flow } from "./lifecycle";
8
8
  import { getStateTreeNode, hasStateTreeNode, getGlobalStore } from "./tree";
9
9
 
10
+ const isBrowser =
11
+ typeof window !== "undefined" &&
12
+ typeof window.document !== "undefined" &&
13
+ typeof window.location !== "undefined" &&
14
+ typeof window.history !== "undefined";
15
+
10
16
  // ============================================================================
11
17
  // Route Definition Model
12
18
  // ============================================================================
@@ -169,6 +175,8 @@ export const RouterModel = model("RouterModel", {
169
175
  beforeNavigate: null as ((from: any, to: any) => boolean | string | Promise<boolean | string> | undefined) | null,
170
176
  afterNavigate: null as ((to: any) => void) | null,
171
177
  _popStateListener: null as ((event: PopStateEvent) => void) | null,
178
+ _historyStack: [] as string[],
179
+ _historyIndex: -1,
172
180
  }))
173
181
  .actions((self) => {
174
182
  return {
@@ -194,13 +202,30 @@ export const RouterModel = model("RouterModel", {
194
202
  self.query = parsed.query;
195
203
  self.currentRouteName = matched ? matched.route.name : null;
196
204
 
197
- if (typeof window !== "undefined" && window.history) {
205
+ if (isBrowser) {
198
206
  const fullPath = pathname + search + hash;
199
207
  if (action === "PUSH") {
200
208
  window.history.pushState(state, "", fullPath);
201
209
  } else if (action === "REPLACE") {
202
210
  window.history.replaceState(state, "", fullPath);
203
211
  }
212
+ } else {
213
+ const fullPath = pathname + search + hash;
214
+ if (action === "PUSH") {
215
+ self._historyStack = self._historyStack.slice(0, self._historyIndex + 1);
216
+ self._historyStack.push(fullPath);
217
+ self._historyIndex = self._historyStack.length - 1;
218
+ } else if (action === "REPLACE") {
219
+ if (self._historyIndex === -1) {
220
+ self._historyStack = [fullPath];
221
+ self._historyIndex = 0;
222
+ } else {
223
+ self._historyStack[self._historyIndex] = fullPath;
224
+ }
225
+ } else if (action === "INITIAL") {
226
+ self._historyStack = [fullPath];
227
+ self._historyIndex = 0;
228
+ }
204
229
  }
205
230
  },
206
231
 
@@ -287,26 +312,37 @@ export const RouterModel = model("RouterModel", {
287
312
  }),
288
313
 
289
314
  go(delta: number) {
290
- if (typeof window !== "undefined" && window.history) {
315
+ if (isBrowser) {
291
316
  window.history.go(delta);
317
+ } else {
318
+ const nextIndex = self._historyIndex + delta;
319
+ if (nextIndex >= 0 && nextIndex < self._historyStack.length) {
320
+ self._historyIndex = nextIndex;
321
+ const targetPath = self._historyStack[nextIndex];
322
+ (self as any).syncLocation(targetPath, "", "", "POP");
323
+ }
292
324
  }
293
325
  },
294
326
 
295
327
  goBack() {
296
- if (typeof window !== "undefined" && window.history) {
328
+ if (isBrowser) {
297
329
  window.history.back();
330
+ } else {
331
+ (self as any).go(-1);
298
332
  }
299
333
  },
300
334
 
301
335
  goForward() {
302
- if (typeof window !== "undefined" && window.history) {
336
+ if (isBrowser) {
303
337
  window.history.forward();
338
+ } else {
339
+ (self as any).go(1);
304
340
  }
305
341
  }
306
342
  };
307
343
  })
308
344
  .afterCreate((self) => {
309
- if (typeof window !== "undefined") {
345
+ if (isBrowser) {
310
346
  const handlePopState = (event: PopStateEvent) => {
311
347
  const parsed = parseUrl(window.location.pathname + window.location.search + window.location.hash);
312
348
  const matched = matchRoutes(self.routes, parsed.pathname);
@@ -349,7 +385,7 @@ export const RouterModel = model("RouterModel", {
349
385
  if (res === false) {
350
386
  revert();
351
387
  } else if (typeof res === "string") {
352
- self.push(res);
388
+ self.replace(res);
353
389
  } else {
354
390
  proceed();
355
391
  }
@@ -360,7 +396,7 @@ export const RouterModel = model("RouterModel", {
360
396
  if (result === false) {
361
397
  revert();
362
398
  } else if (typeof result === "string") {
363
- self.push(result);
399
+ self.replace(result);
364
400
  } else {
365
401
  proceed();
366
402
  }
@@ -375,7 +411,7 @@ export const RouterModel = model("RouterModel", {
375
411
  }
376
412
  })
377
413
  .beforeDestroy((self) => {
378
- if (typeof window !== "undefined" && self._popStateListener) {
414
+ if (isBrowser && self._popStateListener) {
379
415
  window.removeEventListener("popstate", self._popStateListener);
380
416
  self.setPopStateListener(null);
381
417
  }
@@ -400,7 +436,7 @@ export function createRouter(config: {
400
436
  initialPathname = parsed.pathname;
401
437
  initialSearch = parsed.search;
402
438
  initialHash = parsed.hash;
403
- } else if (typeof window !== "undefined") {
439
+ } else if (isBrowser) {
404
440
  initialPathname = window.location.pathname;
405
441
  initialSearch = window.location.search;
406
442
  initialHash = window.location.hash;