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.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/dist/mobx-keystone.esm.js +2561 -1214
- package/dist/mobx-keystone.esm.mjs +2561 -1214
- package/dist/mobx-keystone.umd.js +2566 -1220
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +11 -12
- package/dist/types/modelShared/modelDecorator.d.ts +5 -2
- package/dist/types/modelShared/prop.d.ts +42 -29
- package/dist/types/parent/arrayPaths.d.ts +1 -0
- package/dist/types/parent/treeOrder.d.ts +1 -0
- package/dist/types/patch/applyPatches.d.ts +2 -1
- package/dist/types/ref/Ref.d.ts +1 -4
- package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
- package/dist/types/ref/nodesById.d.ts +1 -0
- package/dist/types/ref/refIdIndex.d.ts +1 -0
- package/dist/types/snapshot/SnapshotOf.d.ts +4 -4
- package/dist/types/snapshot/applySnapshot.d.ts +2 -1
- package/dist/types/snapshot/fromSnapshot.d.ts +2 -5
- package/dist/types/snapshot/getSnapshot.d.ts +1 -1
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/schemas.d.ts +41 -37
- package/dist/types/types/tProp.d.ts +12 -6
- package/dist/types/types/types.d.ts +6 -6
- package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
- package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
- package/dist/types/utils/ModelPool.d.ts +7 -2
- package/dist/types/utils/chunks.d.ts +1 -0
- package/dist/types/utils/types.d.ts +6 -0
- package/dist/types/wrappers/ArraySet.d.ts +1 -0
- package/package.json +5 -3
- package/src/action/modelFlow.ts +35 -66
- package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
- package/src/actionMiddlewares/transactionMiddleware.ts +41 -42
- package/src/actionMiddlewares/undoMiddleware.ts +63 -35
- package/src/context/context.ts +15 -36
- package/src/dataModel/utils.ts +5 -28
- package/src/deepChange/onDeepChange.ts +44 -126
- package/src/model/BaseModel.ts +7 -5
- package/src/model/utils.ts +5 -10
- package/src/modelShared/modelDecorator.ts +16 -30
- package/src/modelShared/prop.ts +115 -111
- package/src/modelShared/sharedInternalModel.ts +2 -2
- package/src/parent/arrayPaths.ts +239 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/onChildAttachedTo.ts +130 -44
- package/src/parent/path.ts +10 -12
- package/src/parent/setParent.ts +27 -15
- package/src/parent/treeOrder.ts +53 -0
- package/src/parent/walkTree.ts +0 -73
- package/src/patch/applyPatches.ts +31 -26
- package/src/patch/patchRecorder.ts +13 -28
- package/src/ref/core.ts +69 -46
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- package/src/rootStore/rootStore.ts +1 -3
- package/src/snapshot/SnapshotOf.ts +24 -48
- package/src/snapshot/applySnapshot.ts +27 -19
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +12 -9
- package/src/snapshot/getSnapshot.ts +1 -1
- package/src/snapshot/internal.ts +67 -12
- package/src/snapshot/reconcileModelSnapshot.ts +5 -3
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
- package/src/treeUtils/draft.ts +6 -1
- package/src/treeUtils/sandbox.ts +10 -7
- package/src/tweaker/changeRollback.ts +218 -0
- package/src/tweaker/core.ts +33 -20
- package/src/tweaker/treeNodeMetadata.ts +12 -2
- package/src/tweaker/tweak.ts +181 -18
- package/src/tweaker/tweakArray.ts +127 -77
- package/src/tweaker/tweakPlainObject.ts +65 -46
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +5 -104
- package/src/types/arrayBased/typesArray.ts +23 -30
- package/src/types/arrayBased/typesTuple.ts +16 -25
- package/src/types/createCachedTypeCheck.ts +509 -94
- package/src/types/index.ts +1 -0
- package/src/types/objectBased/typesDataModelData.ts +7 -8
- package/src/types/objectBased/typesObject.ts +11 -15
- package/src/types/objectBased/typesRecord.ts +12 -19
- package/src/types/schemas.ts +59 -71
- package/src/types/tProp.ts +29 -29
- package/src/types/utility/typeToStoredData.ts +22 -21
- package/src/types/utility/typesCodecCore.ts +5 -0
- package/src/types/utility/typesCodecSupport.ts +3 -3
- package/src/types/utility/typesMap.ts +43 -51
- package/src/types/utility/typesRefinement.ts +7 -8
- package/src/types/utility/typesSkipCheck.ts +7 -8
- package/src/types/utility/typesTag.ts +7 -8
- package/src/utils/ModelPool.ts +76 -37
- package/src/utils/chunks.ts +271 -0
- package/src/utils/decorators.ts +5 -5
- package/src/utils/index.ts +38 -20
- package/src/utils/types.ts +5 -0
- package/src/wrappers/ArraySet.ts +27 -14
- package/src/wrappers/ObjectMap.ts +1 -6
- package/src/wrappers/asMap.ts +1 -21
- 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
|
-
|
|
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]>
|
|
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
|
|
40
|
-
?
|
|
41
|
-
?
|
|
42
|
-
:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
: T
|
|
48
|
-
|
|
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]>
|
|
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
|
|
85
|
-
?
|
|
86
|
-
?
|
|
87
|
-
:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
: T
|
|
93
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,
|
|
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,
|
|
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
|
-
}
|