jotai-state-tree 1.2.0 → 1.2.1

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.
@@ -77,6 +77,9 @@ var StateTreeNode = class {
77
77
  this.patchListeners = /* @__PURE__ */ new Set();
78
78
  /** Volatile state (non-serialized) */
79
79
  this.volatileState = {};
80
+ /** Cached snapshot for structural sharing optimization */
81
+ this.cachedSnapshot = void 0;
82
+ this.isSnapshotDirty = true;
80
83
  this.$id = generateNodeId();
81
84
  this.$type = type;
82
85
  this.$env = env ?? parent?.$env;
@@ -86,6 +89,15 @@ var StateTreeNode = class {
86
89
  nodeRegistry.set(this.$id, { node: new WeakRef(this), instance: null });
87
90
  nodeFinalizationRegistry.register(this, this.$id, this);
88
91
  }
92
+ invalidateSnapshot() {
93
+ if (!this.isSnapshotDirty) {
94
+ this.isSnapshotDirty = true;
95
+ this.cachedSnapshot = void 0;
96
+ if (this.$parent) {
97
+ this.$parent.invalidateSnapshot();
98
+ }
99
+ }
100
+ }
89
101
  /** Set the instance reference */
90
102
  setInstance(instance) {
91
103
  const entry = nodeRegistry.get(this.$id);
@@ -110,6 +122,9 @@ var StateTreeNode = class {
110
122
  );
111
123
  }
112
124
  const oldValue = this.getValue();
125
+ if (oldValue === value) {
126
+ return;
127
+ }
113
128
  globalStore.set(this.valueAtom, value);
114
129
  this.notifyPatch(
115
130
  { op: "replace", path: this.$path, value },
@@ -127,6 +142,7 @@ var StateTreeNode = class {
127
142
  child.$env = child.$env ?? this.$env;
128
143
  this.children.set(key, child);
129
144
  lifecycleHookHandlers.runAfterAttach?.(child);
145
+ this.invalidateSnapshot();
130
146
  }
131
147
  /** Recursively update the path of a node and all its children */
132
148
  updatePathRecursively(node, newPath) {
@@ -142,6 +158,7 @@ var StateTreeNode = class {
142
158
  if (child) {
143
159
  child.destroy();
144
160
  this.children.delete(key);
161
+ this.invalidateSnapshot();
145
162
  }
146
163
  }
147
164
  /** Get a child node */
@@ -204,6 +221,7 @@ var StateTreeNode = class {
204
221
  }
205
222
  /** Notify snapshot listeners */
206
223
  notifySnapshotChange() {
224
+ this.invalidateSnapshot();
207
225
  let current = this;
208
226
  while (current) {
209
227
  const snapshot = getSnapshotFromNode(current);
@@ -261,6 +279,7 @@ var StateTreeNode = class {
261
279
  detach() {
262
280
  if (this.$parent) {
263
281
  lifecycleHookHandlers.runBeforeDetach?.(this);
282
+ const parent = this.$parent;
264
283
  for (const [key, child] of this.$parent.children) {
265
284
  if (child === this) {
266
285
  this.$parent.children.delete(key);
@@ -269,6 +288,7 @@ var StateTreeNode = class {
269
288
  }
270
289
  this.$parent = null;
271
290
  this.$path = "";
291
+ parent.invalidateSnapshot();
272
292
  }
273
293
  }
274
294
  };
@@ -283,38 +303,44 @@ function hasStateTreeNode(instance) {
283
303
  return instance !== null && typeof instance === "object" && $treenode in instance;
284
304
  }
285
305
  function getSnapshotFromNode(node) {
306
+ if (!node.isSnapshotDirty && node.cachedSnapshot !== void 0) {
307
+ return node.cachedSnapshot;
308
+ }
286
309
  const type = node.$type;
287
310
  const value = node.getValue();
311
+ let snapshot;
288
312
  if (type._kind === "model") {
289
- const snapshot = {};
313
+ const modelSnapshot = {};
290
314
  const children = node.getChildren();
291
315
  for (const [key, childNode] of children) {
292
- snapshot[key] = getSnapshotFromNode(childNode);
316
+ modelSnapshot[key] = getSnapshotFromNode(childNode);
293
317
  }
294
318
  if (node.postProcessor) {
295
- return node.postProcessor(snapshot);
319
+ snapshot = node.postProcessor(modelSnapshot);
320
+ } else {
321
+ snapshot = modelSnapshot;
296
322
  }
297
- return snapshot;
298
- }
299
- if (type._kind === "array") {
323
+ } else if (type._kind === "array") {
300
324
  const arr = value;
301
- return arr.map((_, index) => {
325
+ snapshot = arr.map((_, index) => {
302
326
  const childNode = node.getChild(String(index));
303
327
  return childNode ? getSnapshotFromNode(childNode) : arr[index];
304
328
  });
305
- }
306
- if (type._kind === "map") {
307
- const snapshot = {};
329
+ } else if (type._kind === "map") {
330
+ const mapSnapshot = {};
308
331
  const children = node.getChildren();
309
332
  for (const [key, childNode] of children) {
310
- snapshot[key] = getSnapshotFromNode(childNode);
333
+ mapSnapshot[key] = getSnapshotFromNode(childNode);
311
334
  }
312
- return snapshot;
313
- }
314
- if (type._kind === "reference") {
315
- return node.identifierValue ?? value;
335
+ snapshot = mapSnapshot;
336
+ } else if (type._kind === "reference") {
337
+ snapshot = node.identifierValue ?? value;
338
+ } else {
339
+ snapshot = value;
316
340
  }
317
- return value;
341
+ node.cachedSnapshot = snapshot;
342
+ node.isSnapshotDirty = false;
343
+ return snapshot;
318
344
  }
319
345
  function applySnapshotToNode(node, snapshot) {
320
346
  if (!node.$isAlive) {
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as ISimpleType, a as IType, b as IIdentifierType, c as IIdentifierNumberType, d as ILiteralType, e as IEnumerationType, f as IFrozenType, 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, x as IReversibleJsonPatch } from './tree-B2tSEN1S.mjs';
2
- export { L as CustomTypeOptions, D as IAnyComplexType, E as IAnyMixin, G as IJsonPatch, z as IMSTArray, A as IMSTMap, y as IStateTreeNode, H as IValidationContext, K as IValidationError, J as IValidationResult, B as ModelInstance, F as ModelSelf, C as SnapshotOut, T as applyPatch, O as applySnapshot, aw as cleanupStaleEntries, ax as clearAllRegistries, aa as clone, aq as cloneDeep, 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 './tree-B2tSEN1S.mjs';
1
+ import { I as ISimpleType, a as IType, b as IIdentifierType, c as IIdentifierNumberType, d as ILiteralType, e as IEnumerationType, f as IFrozenType, 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, x as IReversibleJsonPatch } from './tree-BV2K9utF.mjs';
2
+ export { L as CustomTypeOptions, D as IAnyComplexType, E as IAnyMixin, G as IJsonPatch, z as IMSTArray, A as IMSTMap, y as IStateTreeNode, H as IValidationContext, K as IValidationError, J as IValidationResult, B as ModelInstance, F as ModelSelf, C as SnapshotOut, T as applyPatch, O as applySnapshot, aw as cleanupStaleEntries, ax as clearAllRegistries, aa as clone, aq as cloneDeep, 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 './tree-BV2K9utF.mjs';
3
3
  import 'jotai/vanilla/internals';
4
4
  import 'jotai';
5
5
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as ISimpleType, a as IType, b as IIdentifierType, c as IIdentifierNumberType, d as ILiteralType, e as IEnumerationType, f as IFrozenType, 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, x as IReversibleJsonPatch } from './tree-B2tSEN1S.js';
2
- export { L as CustomTypeOptions, D as IAnyComplexType, E as IAnyMixin, G as IJsonPatch, z as IMSTArray, A as IMSTMap, y as IStateTreeNode, H as IValidationContext, K as IValidationError, J as IValidationResult, B as ModelInstance, F as ModelSelf, C as SnapshotOut, T as applyPatch, O as applySnapshot, aw as cleanupStaleEntries, ax as clearAllRegistries, aa as clone, aq as cloneDeep, 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 './tree-B2tSEN1S.js';
1
+ import { I as ISimpleType, a as IType, b as IIdentifierType, c as IIdentifierNumberType, d as ILiteralType, e as IEnumerationType, f as IFrozenType, 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, x as IReversibleJsonPatch } from './tree-BV2K9utF.js';
2
+ export { L as CustomTypeOptions, D as IAnyComplexType, E as IAnyMixin, G as IJsonPatch, z as IMSTArray, A as IMSTMap, y as IStateTreeNode, H as IValidationContext, K as IValidationError, J as IValidationResult, B as ModelInstance, F as ModelSelf, C as SnapshotOut, T as applyPatch, O as applySnapshot, aw as cleanupStaleEntries, ax as clearAllRegistries, aa as clone, aq as cloneDeep, 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 './tree-BV2K9utF.js';
3
3
  import 'jotai/vanilla/internals';
4
4
  import 'jotai';
5
5
 
package/dist/index.js CHANGED
@@ -508,6 +508,9 @@ var StateTreeNode = class {
508
508
  this.patchListeners = /* @__PURE__ */ new Set();
509
509
  /** Volatile state (non-serialized) */
510
510
  this.volatileState = {};
511
+ /** Cached snapshot for structural sharing optimization */
512
+ this.cachedSnapshot = void 0;
513
+ this.isSnapshotDirty = true;
511
514
  this.$id = generateNodeId();
512
515
  this.$type = type;
513
516
  this.$env = env ?? parent?.$env;
@@ -517,6 +520,15 @@ var StateTreeNode = class {
517
520
  nodeRegistry.set(this.$id, { node: new WeakRef(this), instance: null });
518
521
  nodeFinalizationRegistry.register(this, this.$id, this);
519
522
  }
523
+ invalidateSnapshot() {
524
+ if (!this.isSnapshotDirty) {
525
+ this.isSnapshotDirty = true;
526
+ this.cachedSnapshot = void 0;
527
+ if (this.$parent) {
528
+ this.$parent.invalidateSnapshot();
529
+ }
530
+ }
531
+ }
520
532
  /** Set the instance reference */
521
533
  setInstance(instance) {
522
534
  const entry = nodeRegistry.get(this.$id);
@@ -541,6 +553,9 @@ var StateTreeNode = class {
541
553
  );
542
554
  }
543
555
  const oldValue = this.getValue();
556
+ if (oldValue === value) {
557
+ return;
558
+ }
544
559
  globalStore.set(this.valueAtom, value);
545
560
  this.notifyPatch(
546
561
  { op: "replace", path: this.$path, value },
@@ -558,6 +573,7 @@ var StateTreeNode = class {
558
573
  child.$env = child.$env ?? this.$env;
559
574
  this.children.set(key, child);
560
575
  lifecycleHookHandlers.runAfterAttach?.(child);
576
+ this.invalidateSnapshot();
561
577
  }
562
578
  /** Recursively update the path of a node and all its children */
563
579
  updatePathRecursively(node, newPath) {
@@ -573,6 +589,7 @@ var StateTreeNode = class {
573
589
  if (child) {
574
590
  child.destroy();
575
591
  this.children.delete(key);
592
+ this.invalidateSnapshot();
576
593
  }
577
594
  }
578
595
  /** Get a child node */
@@ -635,6 +652,7 @@ var StateTreeNode = class {
635
652
  }
636
653
  /** Notify snapshot listeners */
637
654
  notifySnapshotChange() {
655
+ this.invalidateSnapshot();
638
656
  let current = this;
639
657
  while (current) {
640
658
  const snapshot = getSnapshotFromNode(current);
@@ -692,6 +710,7 @@ var StateTreeNode = class {
692
710
  detach() {
693
711
  if (this.$parent) {
694
712
  lifecycleHookHandlers.runBeforeDetach?.(this);
713
+ const parent = this.$parent;
695
714
  for (const [key, child] of this.$parent.children) {
696
715
  if (child === this) {
697
716
  this.$parent.children.delete(key);
@@ -700,6 +719,7 @@ var StateTreeNode = class {
700
719
  }
701
720
  this.$parent = null;
702
721
  this.$path = "";
722
+ parent.invalidateSnapshot();
703
723
  }
704
724
  }
705
725
  };
@@ -714,38 +734,44 @@ function hasStateTreeNode(instance) {
714
734
  return instance !== null && typeof instance === "object" && $treenode in instance;
715
735
  }
716
736
  function getSnapshotFromNode(node) {
737
+ if (!node.isSnapshotDirty && node.cachedSnapshot !== void 0) {
738
+ return node.cachedSnapshot;
739
+ }
717
740
  const type = node.$type;
718
741
  const value = node.getValue();
742
+ let snapshot;
719
743
  if (type._kind === "model") {
720
- const snapshot = {};
744
+ const modelSnapshot = {};
721
745
  const children = node.getChildren();
722
746
  for (const [key, childNode] of children) {
723
- snapshot[key] = getSnapshotFromNode(childNode);
747
+ modelSnapshot[key] = getSnapshotFromNode(childNode);
724
748
  }
725
749
  if (node.postProcessor) {
726
- return node.postProcessor(snapshot);
750
+ snapshot = node.postProcessor(modelSnapshot);
751
+ } else {
752
+ snapshot = modelSnapshot;
727
753
  }
728
- return snapshot;
729
- }
730
- if (type._kind === "array") {
754
+ } else if (type._kind === "array") {
731
755
  const arr = value;
732
- return arr.map((_, index) => {
756
+ snapshot = arr.map((_, index) => {
733
757
  const childNode = node.getChild(String(index));
734
758
  return childNode ? getSnapshotFromNode(childNode) : arr[index];
735
759
  });
736
- }
737
- if (type._kind === "map") {
738
- const snapshot = {};
760
+ } else if (type._kind === "map") {
761
+ const mapSnapshot = {};
739
762
  const children = node.getChildren();
740
763
  for (const [key, childNode] of children) {
741
- snapshot[key] = getSnapshotFromNode(childNode);
764
+ mapSnapshot[key] = getSnapshotFromNode(childNode);
742
765
  }
743
- return snapshot;
744
- }
745
- if (type._kind === "reference") {
746
- return node.identifierValue ?? value;
766
+ snapshot = mapSnapshot;
767
+ } else if (type._kind === "reference") {
768
+ snapshot = node.identifierValue ?? value;
769
+ } else {
770
+ snapshot = value;
747
771
  }
748
- return value;
772
+ node.cachedSnapshot = snapshot;
773
+ node.isSnapshotDirty = false;
774
+ return snapshot;
749
775
  }
750
776
  function applySnapshotToNode(node, snapshot) {
751
777
  if (!node.$isAlive) {
package/dist/index.mjs CHANGED
@@ -56,7 +56,7 @@ import {
56
56
  tryResolve,
57
57
  unfreeze,
58
58
  walk
59
- } from "./chunk-3TQNT4MR.mjs";
59
+ } from "./chunk-UOYV4J7H.mjs";
60
60
 
61
61
  // src/primitives.ts
62
62
  function createSimpleType(name, validator, defaultValue) {
package/dist/react.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { ComponentType, FC, ReactNode } from 'react';
2
- import { ag as getGlobalStore } from './tree-B2tSEN1S.mjs';
3
- export { az as hasStateTreeNode } from './tree-B2tSEN1S.mjs';
2
+ import { ag as getGlobalStore } from './tree-BV2K9utF.mjs';
3
+ export { az as hasStateTreeNode } from './tree-BV2K9utF.mjs';
4
4
  import 'jotai/vanilla/internals';
5
5
  import 'jotai';
6
6
 
package/dist/react.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { ComponentType, FC, ReactNode } from 'react';
2
- import { ag as getGlobalStore } from './tree-B2tSEN1S.js';
3
- export { az as hasStateTreeNode } from './tree-B2tSEN1S.js';
2
+ import { ag as getGlobalStore } from './tree-BV2K9utF.js';
3
+ export { az as hasStateTreeNode } from './tree-BV2K9utF.js';
4
4
  import 'jotai/vanilla/internals';
5
5
  import 'jotai';
6
6
 
package/dist/react.js CHANGED
@@ -126,6 +126,9 @@ var StateTreeNode = class {
126
126
  this.patchListeners = /* @__PURE__ */ new Set();
127
127
  /** Volatile state (non-serialized) */
128
128
  this.volatileState = {};
129
+ /** Cached snapshot for structural sharing optimization */
130
+ this.cachedSnapshot = void 0;
131
+ this.isSnapshotDirty = true;
129
132
  this.$id = generateNodeId();
130
133
  this.$type = type;
131
134
  this.$env = env ?? parent?.$env;
@@ -135,6 +138,15 @@ var StateTreeNode = class {
135
138
  nodeRegistry.set(this.$id, { node: new WeakRef(this), instance: null });
136
139
  nodeFinalizationRegistry.register(this, this.$id, this);
137
140
  }
141
+ invalidateSnapshot() {
142
+ if (!this.isSnapshotDirty) {
143
+ this.isSnapshotDirty = true;
144
+ this.cachedSnapshot = void 0;
145
+ if (this.$parent) {
146
+ this.$parent.invalidateSnapshot();
147
+ }
148
+ }
149
+ }
138
150
  /** Set the instance reference */
139
151
  setInstance(instance) {
140
152
  const entry = nodeRegistry.get(this.$id);
@@ -159,6 +171,9 @@ var StateTreeNode = class {
159
171
  );
160
172
  }
161
173
  const oldValue = this.getValue();
174
+ if (oldValue === value) {
175
+ return;
176
+ }
162
177
  globalStore.set(this.valueAtom, value);
163
178
  this.notifyPatch(
164
179
  { op: "replace", path: this.$path, value },
@@ -176,6 +191,7 @@ var StateTreeNode = class {
176
191
  child.$env = child.$env ?? this.$env;
177
192
  this.children.set(key, child);
178
193
  lifecycleHookHandlers.runAfterAttach?.(child);
194
+ this.invalidateSnapshot();
179
195
  }
180
196
  /** Recursively update the path of a node and all its children */
181
197
  updatePathRecursively(node, newPath) {
@@ -191,6 +207,7 @@ var StateTreeNode = class {
191
207
  if (child) {
192
208
  child.destroy();
193
209
  this.children.delete(key);
210
+ this.invalidateSnapshot();
194
211
  }
195
212
  }
196
213
  /** Get a child node */
@@ -253,6 +270,7 @@ var StateTreeNode = class {
253
270
  }
254
271
  /** Notify snapshot listeners */
255
272
  notifySnapshotChange() {
273
+ this.invalidateSnapshot();
256
274
  let current = this;
257
275
  while (current) {
258
276
  const snapshot = getSnapshotFromNode(current);
@@ -310,6 +328,7 @@ var StateTreeNode = class {
310
328
  detach() {
311
329
  if (this.$parent) {
312
330
  lifecycleHookHandlers.runBeforeDetach?.(this);
331
+ const parent = this.$parent;
313
332
  for (const [key, child] of this.$parent.children) {
314
333
  if (child === this) {
315
334
  this.$parent.children.delete(key);
@@ -318,6 +337,7 @@ var StateTreeNode = class {
318
337
  }
319
338
  this.$parent = null;
320
339
  this.$path = "";
340
+ parent.invalidateSnapshot();
321
341
  }
322
342
  }
323
343
  };
@@ -332,38 +352,44 @@ function hasStateTreeNode(instance) {
332
352
  return instance !== null && typeof instance === "object" && $treenode in instance;
333
353
  }
334
354
  function getSnapshotFromNode(node) {
355
+ if (!node.isSnapshotDirty && node.cachedSnapshot !== void 0) {
356
+ return node.cachedSnapshot;
357
+ }
335
358
  const type = node.$type;
336
359
  const value = node.getValue();
360
+ let snapshot;
337
361
  if (type._kind === "model") {
338
- const snapshot = {};
362
+ const modelSnapshot = {};
339
363
  const children = node.getChildren();
340
364
  for (const [key, childNode] of children) {
341
- snapshot[key] = getSnapshotFromNode(childNode);
365
+ modelSnapshot[key] = getSnapshotFromNode(childNode);
342
366
  }
343
367
  if (node.postProcessor) {
344
- return node.postProcessor(snapshot);
368
+ snapshot = node.postProcessor(modelSnapshot);
369
+ } else {
370
+ snapshot = modelSnapshot;
345
371
  }
346
- return snapshot;
347
- }
348
- if (type._kind === "array") {
372
+ } else if (type._kind === "array") {
349
373
  const arr = value;
350
- return arr.map((_, index) => {
374
+ snapshot = arr.map((_, index) => {
351
375
  const childNode = node.getChild(String(index));
352
376
  return childNode ? getSnapshotFromNode(childNode) : arr[index];
353
377
  });
354
- }
355
- if (type._kind === "map") {
356
- const snapshot = {};
378
+ } else if (type._kind === "map") {
379
+ const mapSnapshot = {};
357
380
  const children = node.getChildren();
358
381
  for (const [key, childNode] of children) {
359
- snapshot[key] = getSnapshotFromNode(childNode);
382
+ mapSnapshot[key] = getSnapshotFromNode(childNode);
360
383
  }
361
- return snapshot;
362
- }
363
- if (type._kind === "reference") {
364
- return node.identifierValue ?? value;
384
+ snapshot = mapSnapshot;
385
+ } else if (type._kind === "reference") {
386
+ snapshot = node.identifierValue ?? value;
387
+ } else {
388
+ snapshot = value;
365
389
  }
366
- return value;
390
+ node.cachedSnapshot = snapshot;
391
+ node.isSnapshotDirty = false;
392
+ return snapshot;
367
393
  }
368
394
  function getSnapshot(target) {
369
395
  const node = getStateTreeNode(target);
package/dist/react.mjs CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  onPatch,
10
10
  onSnapshot,
11
11
  setActiveTrackingFn
12
- } from "./chunk-3TQNT4MR.mjs";
12
+ } from "./chunk-UOYV4J7H.mjs";
13
13
 
14
14
  // src/react.ts
15
15
  import React, {
@@ -343,6 +343,10 @@ declare class StateTreeNode implements IStateTreeNode {
343
343
  identifierValue?: string | number;
344
344
  /** Type name for identifier registry */
345
345
  identifierTypeName?: string;
346
+ /** Cached snapshot for structural sharing optimization */
347
+ cachedSnapshot: unknown;
348
+ isSnapshotDirty: boolean;
349
+ invalidateSnapshot(): void;
346
350
  constructor(type: IAnyType, initialValue: unknown, env?: unknown, parent?: StateTreeNode, pathSegment?: string);
347
351
  /** Set the instance reference */
348
352
  setInstance(instance: unknown): void;
@@ -343,6 +343,10 @@ declare class StateTreeNode implements IStateTreeNode {
343
343
  identifierValue?: string | number;
344
344
  /** Type name for identifier registry */
345
345
  identifierTypeName?: string;
346
+ /** Cached snapshot for structural sharing optimization */
347
+ cachedSnapshot: unknown;
348
+ isSnapshotDirty: boolean;
349
+ invalidateSnapshot(): void;
346
350
  constructor(type: IAnyType, initialValue: unknown, env?: unknown, parent?: StateTreeNode, pathSegment?: string);
347
351
  /** Set the instance reference */
348
352
  setInstance(instance: unknown): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jotai-state-tree",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "MobX-State-Tree API compatible library powered by Jotai",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -270,7 +270,7 @@ describe("Performance", () => {
270
270
  },
271
271
  }));
272
272
 
273
- const instance = Model.create({ value: 0 });
273
+ const instance = Model.create({ value: -1 });
274
274
 
275
275
  // Add many listeners
276
276
  const disposers: (() => void)[] = [];
@@ -312,7 +312,7 @@ describe("Performance", () => {
312
312
  },
313
313
  }));
314
314
 
315
- const instance = Model.create({ value: 0 });
315
+ const instance = Model.create({ value: -1 });
316
316
 
317
317
  const disposers: (() => void)[] = [];
318
318
  let patchCount = 0;
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { types, getSnapshot, applySnapshot } from "../index";
3
+
4
+ describe("Snapshot Structural Sharing", () => {
5
+ it("should reuse snapshot references if nothing changes", () => {
6
+ const Child = types.model("Child", {
7
+ id: types.identifier,
8
+ value: types.string,
9
+ });
10
+
11
+ const Root = types.model("Root", {
12
+ child1: Child,
13
+ child2: Child,
14
+ });
15
+
16
+ const root = Root.create({
17
+ child1: { id: "1", value: "A" },
18
+ child2: { id: "2", value: "B" },
19
+ });
20
+
21
+ const snap1 = getSnapshot<any>(root);
22
+ const snap2 = getSnapshot<any>(root);
23
+
24
+ // If no changes occurred, snapshot references should be identical
25
+ expect(snap1).toBe(snap2);
26
+ expect(snap1.child1).toBe(snap2.child1);
27
+ expect(snap1.child2).toBe(snap2.child2);
28
+ });
29
+
30
+ it("should share unmodified branch references when a sub-branch changes", () => {
31
+ const Child = types
32
+ .model("Child", {
33
+ id: types.identifier,
34
+ value: types.string,
35
+ })
36
+ .actions((self) => ({
37
+ setValue(val: string) {
38
+ self.value = val;
39
+ },
40
+ }));
41
+
42
+ const Root = types.model("Root", {
43
+ child1: Child,
44
+ child2: Child,
45
+ });
46
+
47
+ const root = Root.create({
48
+ child1: { id: "1", value: "A" },
49
+ child2: { id: "2", value: "B" },
50
+ });
51
+
52
+ const snap1 = getSnapshot<any>(root);
53
+
54
+ // Modify child1
55
+ root.child1.setValue("A-modified");
56
+
57
+ const snap2 = getSnapshot<any>(root);
58
+
59
+ // The root and child1 snapshots should be new objects
60
+ expect(snap2).not.toBe(snap1);
61
+ expect(snap2.child1).not.toBe(snap1.child1);
62
+ expect(snap2.child1.value).toBe("A-modified");
63
+
64
+ // The child2 snapshot should be exactly the same object reference!
65
+ expect(snap2.child2).toBe(snap1.child2);
66
+ expect(snap2.child2.value).toBe("B");
67
+ });
68
+
69
+ it("should perform structural sharing with arrays", () => {
70
+ const Item = types
71
+ .model("Item", {
72
+ id: types.identifier,
73
+ name: types.string,
74
+ })
75
+ .actions((self) => ({
76
+ setName(name: string) {
77
+ self.name = name;
78
+ },
79
+ }));
80
+
81
+ const Root = types.model("Root", {
82
+ items: types.array(Item),
83
+ });
84
+
85
+ const root = Root.create({
86
+ items: [
87
+ { id: "1", name: "Item 1" },
88
+ { id: "2", name: "Item 2" },
89
+ ],
90
+ });
91
+
92
+ const snap1 = getSnapshot<any>(root);
93
+
94
+ // Modify Item 2
95
+ root.items[1].setName("Item 2-modified");
96
+
97
+ const snap2 = getSnapshot<any>(root);
98
+
99
+ expect(snap2).not.toBe(snap1);
100
+ expect(snap2.items).not.toBe(snap1.items);
101
+
102
+ // Unmodified Item 1 snapshot should have identical reference
103
+ expect(snap2.items[0]).toBe(snap1.items[0]);
104
+ // Modified Item 2 snapshot should have a new reference
105
+ expect(snap2.items[1]).not.toBe(snap1.items[1]);
106
+ });
107
+
108
+ it("should perform structural sharing with maps", () => {
109
+ const Item = types
110
+ .model("Item", {
111
+ id: types.identifier,
112
+ name: types.string,
113
+ })
114
+ .actions((self) => ({
115
+ setName(name: string) {
116
+ self.name = name;
117
+ },
118
+ }));
119
+
120
+ const Root = types.model("Root", {
121
+ items: types.map(Item),
122
+ });
123
+
124
+ const root = Root.create({
125
+ items: {
126
+ a: { id: "1", name: "Item A" },
127
+ b: { id: "2", name: "Item B" },
128
+ },
129
+ });
130
+
131
+ const snap1 = getSnapshot<any>(root);
132
+
133
+ // Modify Item B
134
+ root.items.get("b")!.setName("Item B-modified");
135
+
136
+ const snap2 = getSnapshot<any>(root);
137
+
138
+ expect(snap2).not.toBe(snap1);
139
+ expect(snap2.items).not.toBe(snap1.items);
140
+
141
+ // Unmodified Item A snapshot should have identical reference
142
+ expect(snap2.items.a).toBe(snap1.items.a);
143
+ // Modified Item B snapshot should have a new reference
144
+ expect(snap2.items.b).not.toBe(snap1.items.b);
145
+ });
146
+
147
+ it("should handle applySnapshot and correctly update caching", () => {
148
+ const Child = types.model("Child", {
149
+ value: types.string,
150
+ });
151
+
152
+ const Root = types.model("Root", {
153
+ child1: Child,
154
+ child2: Child,
155
+ });
156
+
157
+ const root = Root.create({
158
+ child1: { value: "A" },
159
+ child2: { value: "B" },
160
+ });
161
+
162
+ const snap1 = getSnapshot<any>(root);
163
+
164
+ // Apply snapshot updating only child1
165
+ applySnapshot(root, {
166
+ child1: { value: "A-updated" },
167
+ child2: { value: "B" },
168
+ });
169
+
170
+ const snap2 = getSnapshot<any>(root);
171
+
172
+ expect(snap2).not.toBe(snap1);
173
+ expect(snap2.child1).not.toBe(snap1.child1);
174
+ expect(snap2.child1.value).toBe("A-updated");
175
+
176
+ // child2 was not modified, so it should keep the same reference
177
+ expect(snap2.child2).toBe(snap1.child2);
178
+ });
179
+ });
package/src/tree.ts CHANGED
@@ -202,6 +202,20 @@ export class StateTreeNode implements IStateTreeNode {
202
202
  /** Type name for identifier registry */
203
203
  identifierTypeName?: string;
204
204
 
205
+ /** Cached snapshot for structural sharing optimization */
206
+ cachedSnapshot: unknown = undefined;
207
+ isSnapshotDirty = true;
208
+
209
+ invalidateSnapshot() {
210
+ if (!this.isSnapshotDirty) {
211
+ this.isSnapshotDirty = true;
212
+ this.cachedSnapshot = undefined;
213
+ if (this.$parent) {
214
+ this.$parent.invalidateSnapshot();
215
+ }
216
+ }
217
+ }
218
+
205
219
  constructor(
206
220
  type: IAnyType,
207
221
  initialValue: unknown,
@@ -254,6 +268,9 @@ export class StateTreeNode implements IStateTreeNode {
254
268
  }
255
269
 
256
270
  const oldValue = this.getValue();
271
+ if (oldValue === value) {
272
+ return;
273
+ }
257
274
  globalStore.set(this.valueAtom, value);
258
275
 
259
276
  // Notify patch listeners
@@ -276,6 +293,7 @@ export class StateTreeNode implements IStateTreeNode {
276
293
  child.$env = child.$env ?? this.$env;
277
294
  this.children.set(key, child);
278
295
  lifecycleHookHandlers.runAfterAttach?.(child);
296
+ this.invalidateSnapshot();
279
297
  }
280
298
 
281
299
  /** Recursively update the path of a node and all its children */
@@ -295,6 +313,7 @@ export class StateTreeNode implements IStateTreeNode {
295
313
  if (child) {
296
314
  child.destroy();
297
315
  this.children.delete(key);
316
+ this.invalidateSnapshot();
298
317
  }
299
318
  }
300
319
 
@@ -376,6 +395,7 @@ export class StateTreeNode implements IStateTreeNode {
376
395
 
377
396
  /** Notify snapshot listeners */
378
397
  private notifySnapshotChange() {
398
+ this.invalidateSnapshot();
379
399
  let current: StateTreeNode | null = this;
380
400
  while (current) {
381
401
  const snapshot = getSnapshotFromNode(current);
@@ -464,6 +484,7 @@ export class StateTreeNode implements IStateTreeNode {
464
484
  // Run beforeDetach hook
465
485
  lifecycleHookHandlers.runBeforeDetach?.(this);
466
486
 
487
+ const parent = this.$parent;
467
488
  // Find our key in parent's children
468
489
  for (const [key, child] of this.$parent.children) {
469
490
  if (child === this) {
@@ -473,6 +494,7 @@ export class StateTreeNode implements IStateTreeNode {
473
494
  }
474
495
  this.$parent = null;
475
496
  this.$path = "";
497
+ parent.invalidateSnapshot();
476
498
  }
477
499
  }
478
500
  }
@@ -501,50 +523,54 @@ export function hasStateTreeNode(instance: unknown): boolean {
501
523
 
502
524
  /** Get snapshot from a node */
503
525
  export function getSnapshotFromNode(node: StateTreeNode): unknown {
526
+ if (!node.isSnapshotDirty && node.cachedSnapshot !== undefined) {
527
+ return node.cachedSnapshot;
528
+ }
529
+
504
530
  const type = node.$type;
505
531
  const value = node.getValue();
532
+ let snapshot: unknown;
506
533
 
507
534
  // Handle based on type kind
508
535
  if (type._kind === "model") {
509
- const snapshot: Record<string, unknown> = {};
536
+ const modelSnapshot: Record<string, unknown> = {};
510
537
  const children = node.getChildren();
511
538
 
512
539
  for (const [key, childNode] of children) {
513
- snapshot[key] = getSnapshotFromNode(childNode);
540
+ modelSnapshot[key] = getSnapshotFromNode(childNode);
514
541
  }
515
542
 
516
543
  // Apply post processor if exists
517
544
  if (node.postProcessor) {
518
- return node.postProcessor(snapshot);
545
+ snapshot = node.postProcessor(modelSnapshot);
546
+ } else {
547
+ snapshot = modelSnapshot;
519
548
  }
520
-
521
- return snapshot;
522
- }
523
-
524
- if (type._kind === "array") {
549
+ } else if (type._kind === "array") {
525
550
  const arr = value as unknown[];
526
- return arr.map((_, index) => {
551
+ snapshot = arr.map((_, index) => {
527
552
  const childNode = node.getChild(String(index));
528
553
  return childNode ? getSnapshotFromNode(childNode) : arr[index];
529
554
  });
530
- }
531
-
532
- if (type._kind === "map") {
533
- const snapshot: Record<string, unknown> = {};
555
+ } else if (type._kind === "map") {
556
+ const mapSnapshot: Record<string, unknown> = {};
534
557
  const children = node.getChildren();
535
558
  for (const [key, childNode] of children) {
536
- snapshot[key] = getSnapshotFromNode(childNode);
559
+ mapSnapshot[key] = getSnapshotFromNode(childNode);
537
560
  }
538
- return snapshot;
539
- }
540
-
541
- if (type._kind === "reference") {
561
+ snapshot = mapSnapshot;
562
+ } else if (type._kind === "reference") {
542
563
  // Return the identifier, not the resolved value
543
- return node.identifierValue ?? value;
564
+ snapshot = node.identifierValue ?? value;
565
+ } else {
566
+ // For primitives and frozen, return the value directly
567
+ snapshot = value;
544
568
  }
545
569
 
546
- // For primitives and frozen, return the value directly
547
- return value;
570
+ node.cachedSnapshot = snapshot;
571
+ node.isSnapshotDirty = false;
572
+
573
+ return snapshot;
548
574
  }
549
575
 
550
576
  /** Apply snapshot to a node */