mobx-keystone 2.0.0 → 2.2.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/dist/mobx-keystone.esm.js +2561 -1214
  4. package/dist/mobx-keystone.esm.mjs +2561 -1214
  5. package/dist/mobx-keystone.umd.js +2566 -1220
  6. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  7. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +11 -12
  8. package/dist/types/modelShared/modelDecorator.d.ts +5 -2
  9. package/dist/types/modelShared/prop.d.ts +42 -29
  10. package/dist/types/parent/arrayPaths.d.ts +1 -0
  11. package/dist/types/parent/treeOrder.d.ts +1 -0
  12. package/dist/types/patch/applyPatches.d.ts +2 -1
  13. package/dist/types/ref/Ref.d.ts +1 -4
  14. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  15. package/dist/types/ref/nodesById.d.ts +1 -0
  16. package/dist/types/ref/refIdIndex.d.ts +1 -0
  17. package/dist/types/snapshot/SnapshotOf.d.ts +4 -4
  18. package/dist/types/snapshot/applySnapshot.d.ts +2 -1
  19. package/dist/types/snapshot/fromSnapshot.d.ts +2 -5
  20. package/dist/types/snapshot/getSnapshot.d.ts +1 -1
  21. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  22. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  23. package/dist/types/types/index.d.ts +1 -0
  24. package/dist/types/types/schemas.d.ts +41 -37
  25. package/dist/types/types/tProp.d.ts +12 -6
  26. package/dist/types/types/types.d.ts +6 -6
  27. package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
  28. package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
  29. package/dist/types/utils/ModelPool.d.ts +7 -2
  30. package/dist/types/utils/chunks.d.ts +1 -0
  31. package/dist/types/utils/types.d.ts +6 -0
  32. package/dist/types/wrappers/ArraySet.d.ts +1 -0
  33. package/package.json +5 -3
  34. package/src/action/modelFlow.ts +35 -66
  35. package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
  36. package/src/actionMiddlewares/transactionMiddleware.ts +41 -42
  37. package/src/actionMiddlewares/undoMiddleware.ts +63 -35
  38. package/src/context/context.ts +15 -36
  39. package/src/dataModel/utils.ts +5 -28
  40. package/src/deepChange/onDeepChange.ts +44 -126
  41. package/src/model/BaseModel.ts +7 -5
  42. package/src/model/utils.ts +5 -10
  43. package/src/modelShared/modelDecorator.ts +16 -30
  44. package/src/modelShared/prop.ts +115 -111
  45. package/src/modelShared/sharedInternalModel.ts +2 -2
  46. package/src/parent/arrayPaths.ts +239 -0
  47. package/src/parent/coreObjectChildren.ts +300 -48
  48. package/src/parent/onChildAttachedTo.ts +130 -44
  49. package/src/parent/path.ts +10 -12
  50. package/src/parent/setParent.ts +27 -15
  51. package/src/parent/treeOrder.ts +53 -0
  52. package/src/parent/walkTree.ts +0 -73
  53. package/src/patch/applyPatches.ts +31 -26
  54. package/src/patch/patchRecorder.ts +13 -28
  55. package/src/ref/core.ts +69 -46
  56. package/src/ref/modelRefIdIndex.ts +131 -0
  57. package/src/ref/nodesById.ts +60 -0
  58. package/src/ref/refIdIndex.ts +209 -0
  59. package/src/rootStore/rootStore.ts +1 -3
  60. package/src/snapshot/SnapshotOf.ts +24 -48
  61. package/src/snapshot/applySnapshot.ts +27 -19
  62. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  63. package/src/snapshot/fromSnapshot.ts +12 -9
  64. package/src/snapshot/getSnapshot.ts +1 -1
  65. package/src/snapshot/internal.ts +67 -12
  66. package/src/snapshot/reconcileModelSnapshot.ts +5 -3
  67. package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
  68. package/src/treeUtils/draft.ts +6 -1
  69. package/src/treeUtils/sandbox.ts +10 -7
  70. package/src/tweaker/changeRollback.ts +218 -0
  71. package/src/tweaker/core.ts +33 -20
  72. package/src/tweaker/treeNodeMetadata.ts +12 -2
  73. package/src/tweaker/tweak.ts +181 -18
  74. package/src/tweaker/tweakArray.ts +127 -77
  75. package/src/tweaker/tweakPlainObject.ts +65 -46
  76. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  77. package/src/tweaker/typeChecking.ts +5 -104
  78. package/src/types/arrayBased/typesArray.ts +23 -30
  79. package/src/types/arrayBased/typesTuple.ts +16 -25
  80. package/src/types/createCachedTypeCheck.ts +509 -94
  81. package/src/types/index.ts +1 -0
  82. package/src/types/objectBased/typesDataModelData.ts +7 -8
  83. package/src/types/objectBased/typesObject.ts +11 -15
  84. package/src/types/objectBased/typesRecord.ts +12 -19
  85. package/src/types/schemas.ts +59 -71
  86. package/src/types/tProp.ts +29 -29
  87. package/src/types/utility/typeToStoredData.ts +22 -21
  88. package/src/types/utility/typesCodecCore.ts +5 -0
  89. package/src/types/utility/typesCodecSupport.ts +3 -3
  90. package/src/types/utility/typesMap.ts +43 -51
  91. package/src/types/utility/typesRefinement.ts +7 -8
  92. package/src/types/utility/typesSkipCheck.ts +7 -8
  93. package/src/types/utility/typesTag.ts +7 -8
  94. package/src/utils/ModelPool.ts +76 -37
  95. package/src/utils/chunks.ts +271 -0
  96. package/src/utils/decorators.ts +5 -5
  97. package/src/utils/index.ts +38 -20
  98. package/src/utils/types.ts +5 -0
  99. package/src/wrappers/ArraySet.ts +27 -14
  100. package/src/wrappers/ObjectMap.ts +1 -6
  101. package/src/wrappers/asMap.ts +1 -21
  102. package/src/wrappers/asSet.ts +0 -8
@@ -0,0 +1,131 @@
1
+ import { createAtom, type IAtom } from "mobx"
2
+ import { type AnyModel, BaseModel } from "../model/BaseModel"
3
+ import { modelIdKey } from "../model/metadata"
4
+ import { registerDeepObjectChildrenExtension } from "../parent/coreObjectChildren"
5
+ import { NodesById } from "./nodesById"
6
+
7
+ interface ModelRefIdIndex {
8
+ readonly byId: NodesById<AnyModel>
9
+ /** Id each model is indexed under, since ids can change later. */
10
+ readonly indexedIds: Map<AnyModel, string>
11
+ /** Models with a custom `getRefId`, which cannot be indexed. */
12
+ customRefIdModels: number
13
+ /** Changed when models with a custom `getRefId` appear or disappear. */
14
+ readonly customRefIdAtom: IAtom
15
+ /** Changed when the models with a given id change. Only kept while observed. */
16
+ readonly idAtoms: Map<string, IAtom>
17
+ }
18
+
19
+ function hasCustomRefId(model: AnyModel) {
20
+ return model.getRefId !== BaseModel.prototype.getRefId
21
+ }
22
+
23
+ function reportIdChanged(index: ModelRefIdIndex, id: string) {
24
+ index.idAtoms.get(id)?.reportChanged()
25
+ }
26
+
27
+ const modelRefIdIndex = registerDeepObjectChildrenExtension<ModelRefIdIndex>({
28
+ initData() {
29
+ return {
30
+ byId: new NodesById(),
31
+ indexedIds: new Map(),
32
+ customRefIdModels: 0,
33
+ customRefIdAtom: createAtom("customRefIdModels"),
34
+ idAtoms: new Map(),
35
+ }
36
+ },
37
+
38
+ addNode(node, index) {
39
+ if (!(node instanceof BaseModel)) {
40
+ return
41
+ }
42
+ if (hasCustomRefId(node)) {
43
+ index.customRefIdModels++
44
+ if (index.customRefIdModels === 1) {
45
+ index.customRefIdAtom.reportChanged()
46
+ }
47
+ return
48
+ }
49
+
50
+ const id = node[modelIdKey]
51
+ if (id === undefined) {
52
+ return
53
+ }
54
+ index.byId.add(id, node)
55
+ index.indexedIds.set(node, id)
56
+ reportIdChanged(index, id)
57
+ },
58
+
59
+ removeNode(node, index) {
60
+ if (!(node instanceof BaseModel)) {
61
+ return true
62
+ }
63
+ if (hasCustomRefId(node)) {
64
+ index.customRefIdModels--
65
+ if (index.customRefIdModels === 0) {
66
+ index.customRefIdAtom.reportChanged()
67
+ }
68
+ return true
69
+ }
70
+
71
+ const id = index.indexedIds.get(node)
72
+ if (id === undefined) {
73
+ return true
74
+ }
75
+ index.indexedIds.delete(node)
76
+ index.byId.delete(id, node)
77
+ reportIdChanged(index, id)
78
+ return true
79
+ },
80
+
81
+ dependsOnModelIds: true,
82
+ })
83
+
84
+ function observeId(index: ModelRefIdIndex, id: string) {
85
+ const existing = index.idAtoms.get(id)
86
+ if (existing) {
87
+ existing.reportObserved()
88
+ return
89
+ }
90
+ const atom = createAtom("modelRefId", undefined, () => {
91
+ if (index.idAtoms.get(id) === atom) {
92
+ index.idAtoms.delete(id)
93
+ }
94
+ })
95
+ // only keep atoms something observes, since nothing else would remove them
96
+ if (atom.reportObserved()) {
97
+ index.idAtoms.set(id, atom)
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Result of `resolveModelRefId` when the tree has models with a custom
103
+ * `getRefId`, which it cannot index.
104
+ *
105
+ * @internal
106
+ */
107
+ export const modelRefIdNotIndexed = Symbol("modelRefIdNotIndexed")
108
+
109
+ /**
110
+ * Resolves a model below the root (excluding it) by its default reference id
111
+ * (`getModelRefId`), reactively, in O(1), or O(models × depth) when several
112
+ * models have that id.
113
+ * Returns `modelRefIdNotIndexed` when the tree has models with a custom
114
+ * `getRefId`.
115
+ *
116
+ * @internal
117
+ */
118
+ export function resolveModelRefId(
119
+ root: object,
120
+ id: string
121
+ ): object | undefined | typeof modelRefIdNotIndexed {
122
+ const index = modelRefIdIndex.get(root, false)
123
+
124
+ index.customRefIdAtom.reportObserved()
125
+ if (index.customRefIdModels > 0) {
126
+ return modelRefIdNotIndexed
127
+ }
128
+
129
+ observeId(index, id)
130
+ return index.byId.resolve(root, id)
131
+ }
@@ -0,0 +1,60 @@
1
+ import { compareTreePaths, getPathFromAncestor } from "../parent/treeOrder"
2
+
3
+ /**
4
+ * The nodes below a root indexed by id, where several nodes may share an id.
5
+ *
6
+ * @internal
7
+ */
8
+ export class NodesById<T extends object> {
9
+ // (tree nodes are never sets)
10
+ private readonly byId = new Map<string, T | Set<T>>()
11
+
12
+ add(id: string, node: T): void {
13
+ const existing = this.byId.get(id)
14
+ if (existing === undefined) {
15
+ this.byId.set(id, node)
16
+ } else if (existing instanceof Set) {
17
+ existing.add(node)
18
+ } else {
19
+ this.byId.set(id, new Set([existing, node]))
20
+ }
21
+ }
22
+
23
+ delete(id: string, node: T): void {
24
+ const existing = this.byId.get(id)
25
+ if (existing instanceof Set) {
26
+ existing.delete(node)
27
+ if (existing.size === 1) {
28
+ this.byId.set(id, existing.values().next().value!)
29
+ }
30
+ } else if (existing === node) {
31
+ this.byId.delete(id)
32
+ }
33
+ }
34
+
35
+ /** Resolves the node with the id, picking one when several share it. */
36
+ resolve(root: object, id: string): T | undefined {
37
+ const found = this.byId.get(id)
38
+ return found instanceof Set ? resolveDuplicatedId(root, found) : found
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Picks which of several nodes below the root sharing an id it resolves to:
44
+ * an ancestor wins over its descendants, and otherwise the node below the
45
+ * later attached child of their closest common ancestor wins. That is the last
46
+ * one a post-order walk of the tree would find. Reactive, in
47
+ * O(nodes × depth).
48
+ */
49
+ function resolveDuplicatedId<T extends object>(root: object, nodes: Iterable<T>): T {
50
+ let winner: T | undefined
51
+ let winnerPath: object[] | undefined
52
+ for (const node of nodes) {
53
+ const path = getPathFromAncestor(root, node, true)!
54
+ if (!winnerPath || compareTreePaths(path, winnerPath, false) > 0) {
55
+ winner = node
56
+ winnerPath = path
57
+ }
58
+ }
59
+ return winner!
60
+ }
@@ -0,0 +1,209 @@
1
+ import { computed, createAtom, type IComputedValue, Reaction } from "mobx"
2
+ import {
3
+ type DeepObjectChildrenExtensionAccessor,
4
+ registerDeepObjectChildrenExtension,
5
+ } from "../parent/coreObjectChildren"
6
+ import { ChunkedSet, type SequenceChunk } from "../utils/chunks"
7
+ import { getOrCreate } from "../utils/mapUtils"
8
+ import { NodesById } from "./nodesById"
9
+
10
+ type GetId = (target: object) => string | undefined
11
+
12
+ /**
13
+ * A group of nodes, with a computed map of their current ids, so an id change
14
+ * only re-reads the ids of the nodes in its chunk.
15
+ */
16
+ class IdChunk implements SequenceChunk<object> {
17
+ items: object[]
18
+ /** Changed when nodes are added or removed. */
19
+ readonly atom = createAtom("refIdChunk")
20
+ readonly ids: IComputedValue<Map<string, object | object[]>>
21
+ /** Ids last copied to the index. */
22
+ syncedIds: Map<string, object | object[]> | undefined
23
+
24
+ constructor(nodes: object[], getId: GetId) {
25
+ this.items = nodes
26
+ this.ids = computed(() => {
27
+ this.atom.reportObserved()
28
+ const ids = new Map<string, object | object[]>()
29
+ for (const node of this.items) {
30
+ const id = getId(node)
31
+ if (id === undefined) {
32
+ continue
33
+ }
34
+ const existing = ids.get(id)
35
+ if (existing === undefined) {
36
+ ids.set(id, node)
37
+ } else if (Array.isArray(existing)) {
38
+ existing.push(node)
39
+ } else {
40
+ ids.set(id, [existing, node])
41
+ }
42
+ }
43
+ return ids
44
+ })
45
+ }
46
+ }
47
+
48
+ class RefIdIndex extends ChunkedSet<object, IdChunk> {
49
+ /** Changed when chunks are added or removed. */
50
+ readonly chunksAtom = createAtom("refIdChunks")
51
+ readonly byId = new NodesById<object>()
52
+ /** Copies the chunks' id changes into `byId`. Changes when `byId` does. */
53
+ readonly synced: IComputedValue<number>
54
+ /** Set when `byId` changed outside of a sync, so the version must change. */
55
+ private changedOutsideSync = false
56
+ keeper: Reaction | undefined
57
+ private readonly getId: GetId
58
+
59
+ constructor(getId: GetId) {
60
+ super()
61
+ this.getId = getId
62
+ let version = 0
63
+ this.synced = computed(
64
+ () => {
65
+ this.chunksAtom.reportObserved()
66
+ let changed = this.changedOutsideSync
67
+ this.changedOutsideSync = false
68
+ for (const chunk of this.chunks) {
69
+ if (this.syncChunk(chunk)) {
70
+ changed = true
71
+ }
72
+ }
73
+ if (changed) {
74
+ version++
75
+ }
76
+ return version
77
+ },
78
+ { name: "refIdIndex" }
79
+ )
80
+ }
81
+
82
+ protected createChunk(nodes: object[]): IdChunk {
83
+ return new IdChunk(nodes, this.getId)
84
+ }
85
+
86
+ protected onChunkChanged(chunk: IdChunk): void {
87
+ chunk.atom.reportChanged()
88
+ }
89
+
90
+ protected onChunksChanged(): void {
91
+ this.chunksAtom.reportChanged()
92
+ }
93
+
94
+ protected onChunkDropped(chunk: IdChunk): void {
95
+ // its nodes (if any) moved to a chunk that will add their ids again
96
+ const synced = chunk.syncedIds
97
+ if (synced && synced.size > 0) {
98
+ for (const [id, value] of synced) {
99
+ forEachNode(value, (node) => this.byId.delete(id, node))
100
+ }
101
+ this.changedOutsideSync = true
102
+ }
103
+ }
104
+
105
+ private syncChunk(chunk: IdChunk): boolean {
106
+ const ids = chunk.ids.get()
107
+ const previous = chunk.syncedIds
108
+ if (ids === previous) {
109
+ return false
110
+ }
111
+ chunk.syncedIds = ids
112
+
113
+ let changed = false
114
+ if (previous) {
115
+ for (const [id, value] of previous) {
116
+ if (!sameNodes(ids.get(id), value)) {
117
+ forEachNode(value, (node) => this.byId.delete(id, node))
118
+ changed = true
119
+ }
120
+ }
121
+ }
122
+ for (const [id, value] of ids) {
123
+ if (!sameNodes(previous?.get(id), value)) {
124
+ forEachNode(value, (node) => this.byId.add(id, node))
125
+ changed = true
126
+ }
127
+ }
128
+ return changed
129
+ }
130
+ }
131
+
132
+ function forEachNode(value: object | object[], fn: (node: object) => void) {
133
+ if (Array.isArray(value)) {
134
+ value.forEach(fn)
135
+ } else {
136
+ fn(value)
137
+ }
138
+ }
139
+
140
+ function sameNodes(a: object | object[] | undefined, b: object | object[]) {
141
+ if (a === b) {
142
+ return true
143
+ }
144
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
145
+ return false
146
+ }
147
+ return a.every((node, i) => node === b[i])
148
+ }
149
+
150
+ function createRefIdIndexExtension(getId: GetId) {
151
+ return registerDeepObjectChildrenExtension<RefIdIndex>({
152
+ initData() {
153
+ return new RefIdIndex(getId)
154
+ },
155
+
156
+ addNode(node, index) {
157
+ index.add(node)
158
+ },
159
+
160
+ removeNode(node, index) {
161
+ index.delete(node)
162
+ return true
163
+ },
164
+ })
165
+ }
166
+
167
+ const refIdIndexes = new WeakMap<GetId, DeepObjectChildrenExtensionAccessor<RefIdIndex>>()
168
+
169
+ // Outside reactions and actions MobX does not cache computeds, so every
170
+ // untracked resolution would re-read every id. Keep the index observed until
171
+ // any of its ids change, then release it, so ids read from outside the tree
172
+ // cannot keep it alive.
173
+ function keepAliveUntilChanged(index: RefIdIndex) {
174
+ if (index.keeper) {
175
+ return
176
+ }
177
+ const keeper = new Reaction("refIdIndex keeper", () => {
178
+ keeper.dispose()
179
+ if (index.keeper === keeper) {
180
+ index.keeper = undefined
181
+ }
182
+ })
183
+ index.keeper = keeper
184
+ keeper.track(() => {
185
+ try {
186
+ index.synced.get()
187
+ } catch {
188
+ // the caller's own read rethrows the cached error
189
+ }
190
+ })
191
+ }
192
+
193
+ /**
194
+ * Resolves a node below the root (excluding it) whose `getId` is the given
195
+ * id, reactively, in O(tree size / 128) plus the cost of re-reading the ids of
196
+ * any chunk of 128 nodes that changed, plus O(nodes × depth) when several
197
+ * nodes have that id.
198
+ *
199
+ * @internal
200
+ */
201
+ export function resolveRefIdByIndex(root: object, id: string, getId: GetId): object | undefined {
202
+ const accessor = getOrCreate(refIdIndexes, getId, () => createRefIdIndexExtension(getId))
203
+ const index = accessor.get(root, false)
204
+
205
+ keepAliveUntilChanged(index)
206
+ index.synced.get()
207
+
208
+ return index.byId.resolve(root, id)
209
+ }
@@ -84,9 +84,7 @@ export function isRootStore(node: object): boolean {
84
84
  assertTweakedObject(node, "node")
85
85
 
86
86
  const entry = getOrCreateRootStoreEntry(node)
87
- if (!entry.atom) {
88
- entry.atom = createAtom("rootStore")
89
- }
87
+ entry.atom ??= createAtom("rootStore")
90
88
  entry.atom.reportObserved()
91
89
  return entry.is
92
90
  }
@@ -13,10 +13,8 @@ export type FrozenData<D> = {
13
13
 
14
14
  // snapshot out
15
15
 
16
- // infer is there just to cache type generation
17
-
18
16
  export type SnapshotOutOfObject<T> = {
19
- [k in keyof T]: SnapshotOutOf<T[k]> extends infer R ? R : never
17
+ [k in keyof T]: SnapshotOutOf<T[k]>
20
18
  }
21
19
 
22
20
  export type SnapshotOutOfModel<M extends AnyModel> = ModelToSnapshot<M>
@@ -35,33 +33,22 @@ export interface SnapshotOutOfArraySet<V> {
35
33
  [modelIdKey]: string
36
34
  }
37
35
 
38
- export type SnapshotOutOf<T> =
39
- T extends ObjectMap<infer V>
40
- ? SnapshotOutOfObjectMap<V> extends infer R
41
- ? R
42
- : never
43
- : T extends ArraySet<infer V>
44
- ? SnapshotOutOfArraySet<V> extends infer R
45
- ? R
46
- : never
47
- : T extends AnyModel
48
- ? SnapshotOutOfModel<T> extends infer R
49
- ? R
50
- : never
51
- : T extends Frozen<any>
52
- ? SnapshotOutOfFrozen<T> extends infer R
53
- ? R
54
- : never
55
- : T extends object
56
- ? SnapshotOutOfObject<T> extends infer R
57
- ? R
58
- : never
59
- : T
36
+ export type SnapshotOutOf<T> = T extends object
37
+ ? T extends AnyModel
38
+ ? T extends ObjectMap<infer V>
39
+ ? SnapshotOutOfObjectMap<V>
40
+ : T extends ArraySet<infer V>
41
+ ? SnapshotOutOfArraySet<V>
42
+ : SnapshotOutOfModel<T>
43
+ : T extends Frozen<any>
44
+ ? SnapshotOutOfFrozen<T>
45
+ : SnapshotOutOfObject<T>
46
+ : T
60
47
 
61
48
  // snapshot in
62
49
 
63
50
  export type SnapshotInOfObject<T> = {
64
- [k in keyof T]: SnapshotInOf<T[k]> extends infer R ? R : never
51
+ [k in keyof T]: SnapshotInOf<T[k]>
65
52
  }
66
53
 
67
54
  export type SnapshotInOfModel<M extends AnyModel> = ModelFromSnapshot<M>
@@ -80,25 +67,14 @@ export interface SnapshotInOfArraySet<V> {
80
67
  [modelIdKey]: string
81
68
  }
82
69
 
83
- export type SnapshotInOf<T> =
84
- T extends ObjectMap<infer V>
85
- ? SnapshotInOfObjectMap<V> extends infer R
86
- ? R
87
- : never
88
- : T extends ArraySet<infer V>
89
- ? SnapshotInOfArraySet<V> extends infer R
90
- ? R
91
- : never
92
- : T extends AnyModel
93
- ? SnapshotInOfModel<T> extends infer R
94
- ? R
95
- : never
96
- : T extends Frozen<any>
97
- ? SnapshotInOfFrozen<T> extends infer R
98
- ? R
99
- : never
100
- : T extends object
101
- ? SnapshotInOfObject<T> extends infer R
102
- ? R
103
- : never
104
- : T
70
+ export type SnapshotInOf<T> = T extends object
71
+ ? T extends AnyModel
72
+ ? T extends ObjectMap<infer V>
73
+ ? SnapshotInOfObjectMap<V>
74
+ : T extends ArraySet<infer V>
75
+ ? SnapshotInOfArraySet<V>
76
+ : SnapshotInOfModel<T>
77
+ : T extends Frozen<any>
78
+ ? SnapshotInOfFrozen<T>
79
+ : SnapshotInOfObject<T>
80
+ : T
@@ -13,6 +13,7 @@ import {
13
13
  getModelNotRegisteredErrorMessage,
14
14
  modelInfoByClass,
15
15
  } from "../modelShared/modelInfo"
16
+ import { withChangeBatch } from "../tweaker/changeRollback"
16
17
  import { assertTweakedObject } from "../tweaker/core"
17
18
  import { assertIsObject, inDevMode, isArray, isMap, isPlainObject, isSet, lazy } from "../utils"
18
19
  import { runWithErrorDiagnosticsContext } from "../utils/errorDiagnostics"
@@ -24,6 +25,7 @@ import { SnapshotProcessingError } from "./SnapshotProcessingError"
24
25
 
25
26
  /**
26
27
  * Applies a full snapshot over an object, reconciling it with the current contents of the object.
28
+ * If it throws, the changes it made are rolled back (errors thrown by listeners do not roll them back).
27
29
  *
28
30
  * @template T Object type.
29
31
  * @param node Target object (model object, object or array).
@@ -31,7 +33,7 @@ import { SnapshotProcessingError } from "./SnapshotProcessingError"
31
33
  */
32
34
  export function applySnapshot<T extends object>(
33
35
  node: T,
34
- snapshot: SnapshotInOf<T> | SnapshotOutOf<T>
36
+ snapshot: NoInfer<SnapshotInOf<T> | SnapshotOutOf<T>>
35
37
  ): void
36
38
 
37
39
  export function applySnapshot(node: object, snapshot: unknown): void {
@@ -52,27 +54,33 @@ export function internalApplySnapshot<T extends object>(
52
54
  ): void {
53
55
  const obj = this
54
56
 
55
- const reconcile = () => {
56
- // Avoid building a model pool (and its deep children traversal) when the
57
- // node already holds this exact snapshot. Child updates may still await
58
- // propagation within an action, so flush before trusting the reference.
59
- flushInternalSnapshot(obj, false)
60
- if (getInternalSnapshot(obj)?.transformed === sn) {
61
- return
62
- }
57
+ const reconcile = () =>
58
+ withChangeBatch(() => {
59
+ // Avoid building a model pool (and its deep children traversal) when the
60
+ // node already holds this exact snapshot. Child updates may still await
61
+ // propagation within an action, so flush before trusting the reference.
62
+ flushInternalSnapshot(obj, false)
63
+ if (getInternalSnapshot(obj)?.transformed === sn) {
64
+ return
65
+ }
63
66
 
64
- const modelPool = new ModelPool(obj)
65
- const ret = reconcileSnapshot(obj, sn, modelPool, undefined)
67
+ const modelPool = new ModelPool(obj)
68
+ let ret: unknown
69
+ try {
70
+ ret = reconcileSnapshot(obj, sn, modelPool, undefined)
71
+ } finally {
72
+ modelPool.release()
73
+ }
66
74
 
67
- if (inDevMode) {
68
- if (ret !== obj) {
69
- throw new SnapshotProcessingError({
70
- message: "assertion failed: reconciled object has to be the same",
71
- actualSnapshot: sn,
72
- })
75
+ if (inDevMode) {
76
+ if (ret !== obj) {
77
+ throw new SnapshotProcessingError({
78
+ message: "assertion failed: reconciled object has to be the same",
79
+ actualSnapshot: sn,
80
+ })
81
+ }
73
82
  }
74
- }
75
- }
83
+ })
76
84
 
77
85
  if (isArray(sn)) {
78
86
  if (!isArray(obj)) {
@@ -1,13 +1,8 @@
1
1
  import { observable } from "mobx"
2
2
  import { tweakPlainObject } from "../tweaker/tweakPlainObject"
3
- import { isPlainObject, setProtoProp } from "../utils"
3
+ import { isPlainObject, nodeObservableOptions, setProtoProp } from "../utils"
4
4
  import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
5
- import {
6
- type FromSnapshotContext,
7
- internalFromSnapshot,
8
- observableOptions,
9
- registerSnapshotter,
10
- } from "./fromSnapshot"
5
+ import { type FromSnapshotContext, internalFromSnapshot, registerSnapshotter } from "./fromSnapshot"
11
6
  import type { SnapshotInOfObject } from "./SnapshotOf"
12
7
  import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPriority"
13
8
 
@@ -37,7 +32,7 @@ function fromPlainObjectSnapshot(sn: SnapshotInOfObject<any>, ctx: FromSnapshotC
37
32
  }
38
33
  }
39
34
  return tweakPlainObject(
40
- observable.object(plainObj, undefined, observableOptions),
35
+ observable.object(plainObj, undefined, nodeObservableOptions),
41
36
  undefined,
42
37
  undefined,
43
38
  true,
@@ -1,11 +1,12 @@
1
1
  import { action, observable } from "mobx"
2
2
  import type { AnyModel } from "../model/BaseModel"
3
3
  import { isReservedModelKey } from "../model/metadata"
4
+ import { isTweakedObject } from "../tweaker/core"
4
5
  import { resolveStandardTypeNoThrow, resolveTypeChecker } from "../types/resolveTypeChecker"
5
- import type { AnyType, TypeToData, TypeToSnapshotIn } from "../types/schemas"
6
+ import type { AnyType, TypeToData, TypeToSnapshotIn, TypeToSnapshotOut } from "../types/schemas"
6
7
  import { isLateTypeChecker, TypeChecker } from "../types/TypeChecker"
7
8
  import { resolveCodecSupport } from "../types/utility/typesCodec"
8
- import { isMap, isPrimitive, isSet, setProtoProp } from "../utils"
9
+ import { isMap, isPrimitive, isSet, nodeObservableOptions, setProtoProp } from "../utils"
9
10
  import {
10
11
  getCurrentErrorDiagnosticsContext,
11
12
  runWithErrorDiagnosticsContext,
@@ -60,7 +61,7 @@ export interface FromSnapshotContext {
60
61
  */
61
62
  export function fromSnapshot<TType extends AnyType>(
62
63
  type: TType,
63
- snapshot: TypeToSnapshotIn<TType>,
64
+ snapshot: NoInfer<TypeToSnapshotIn<TType> | TypeToSnapshotOut<TType>>,
64
65
  options?: Partial<FromSnapshotOptions>
65
66
  ): TypeToData<TType>
66
67
 
@@ -122,7 +123,6 @@ const fromSnapshotAction = action(
122
123
  ): T => {
123
124
  const opts = {
124
125
  generateNewIds: false,
125
- overrideRootModelId: undefined,
126
126
  ...options,
127
127
  }
128
128
 
@@ -172,6 +172,13 @@ export function internalFromSnapshot<T>(
172
172
  })
173
173
  }
174
174
 
175
+ if (isTweakedObject(sn, true)) {
176
+ throw new SnapshotProcessingError({
177
+ message: "a snapshot must not contain tree nodes (use getSnapshot to get their snapshots)",
178
+ actualSnapshot: sn,
179
+ })
180
+ }
181
+
175
182
  throw new SnapshotProcessingError({
176
183
  message: "unsupported snapshot",
177
184
  actualSnapshot: sn,
@@ -210,13 +217,9 @@ function snapshotToInitialData(
210
217
  }
211
218
  }
212
219
  }
213
- const observableInitialData = observable.object(initialData, undefined, observableOptions)
220
+ const observableInitialData = observable.object(initialData, undefined, nodeObservableOptions)
214
221
  if (allValuesPrimitive) {
215
222
  setModelInitialDataSnapshot(observableInitialData, initialData)
216
223
  }
217
224
  return observableInitialData
218
225
  }
219
-
220
- export const observableOptions = {
221
- deep: false,
222
- }
@@ -23,7 +23,7 @@ import type { SnapshotOutOf } from "./SnapshotOf"
23
23
  */
24
24
  export function getSnapshot<T extends AnyType>(
25
25
  type: T,
26
- nodeOrPrimitive: TypeToData<T>
26
+ nodeOrPrimitive: NoInfer<TypeToData<T>>
27
27
  ): TypeToSnapshotOut<T>
28
28
 
29
29
  /**