mobx-keystone 1.25.0 → 2.1.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 +145 -0
- package/dist/mobx-keystone.esm.js +3627 -1540
- package/dist/mobx-keystone.esm.mjs +3627 -1540
- package/dist/mobx-keystone.umd.js +3638 -1543
- package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
- package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
- package/dist/types/deepChange/onDeepChange.d.ts +9 -0
- package/dist/types/parent/arrayPaths.d.ts +1 -0
- package/dist/types/parent/detach.d.ts +1 -1
- package/dist/types/parent/findChildren.d.ts +1 -1
- package/dist/types/parent/treeOrder.d.ts +1 -0
- package/dist/types/patch/applyPatches.d.ts +3 -0
- package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
- 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 +2 -2
- package/dist/types/snapshot/applySnapshot.d.ts +1 -0
- package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
- package/dist/types/standardActions/arrayActions.d.ts +1 -1
- package/dist/types/treeUtils/sandbox.d.ts +0 -4
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/types/TypeCheckError.d.ts +1 -0
- package/dist/types/types/utility/typesRefinement.d.ts +2 -2
- package/dist/types/utils/ModelPool.d.ts +7 -1
- package/dist/types/utils/chunks.d.ts +1 -0
- package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
- package/dist/types/utils/index.d.ts +13 -0
- package/dist/types/utils/mapUtils.d.ts +2 -2
- package/dist/types/utils/setLike.d.ts +1 -0
- package/package.json +2 -2
- package/src/action/applyAction.ts +1 -1
- package/src/action/middleware.ts +17 -36
- package/src/action/modelFlow.ts +4 -5
- package/src/action/mutationBatch.ts +10 -10
- package/src/action/pendingActions.ts +3 -4
- package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
- package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
- package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
- package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
- package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
- package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
- package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
- package/src/actionMiddlewares/transactionMiddleware.ts +55 -25
- package/src/actionMiddlewares/undoMiddleware.ts +112 -59
- package/src/actionMiddlewares/utils.ts +2 -1
- package/src/computedTree/computedTree.ts +14 -3
- package/src/context/context.ts +17 -45
- package/src/dataModel/BaseDataModel.ts +20 -16
- package/src/dataModel/dataModelInstanceCache.ts +8 -0
- package/src/deepChange/onDeepChange.ts +179 -24
- package/src/frozen/Frozen.ts +50 -33
- package/src/model/BaseModel.ts +10 -13
- package/src/model/newModel.ts +21 -24
- package/src/model/utils.ts +2 -2
- package/src/modelShared/modelClassInitializer.ts +3 -2
- package/src/modelShared/modelDecorator.ts +39 -39
- package/src/modelShared/modelInfo.ts +1 -1
- package/src/modelShared/prop.ts +6 -9
- package/src/modelShared/sharedInternalModel.ts +16 -3
- package/src/parent/arrayPaths.ts +242 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/detach.ts +1 -1
- package/src/parent/findChildren.ts +4 -7
- package/src/parent/onChildAttachedTo.ts +157 -55
- package/src/parent/path.ts +15 -5
- package/src/parent/setParent.ts +34 -15
- package/src/parent/treeOrder.ts +53 -0
- package/src/parent/walkTree.ts +12 -101
- package/src/patch/applyPatches.ts +74 -34
- package/src/patch/emitPatch.ts +142 -26
- package/src/patch/jsonPatch.ts +5 -0
- package/src/patch/patchRecorder.ts +18 -14
- package/src/redux/connectReduxDevTools.ts +24 -18
- package/src/redux/redux.ts +5 -21
- package/src/ref/core.ts +81 -29
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- package/src/rootStore/attachDetach.ts +27 -16
- package/src/rootStore/rootStore.ts +10 -6
- package/src/snapshot/SnapshotOf.ts +2 -2
- package/src/snapshot/applySnapshot.ts +27 -19
- package/src/snapshot/clone.ts +1 -2
- package/src/snapshot/cloneTreeValue.ts +1 -2
- package/src/snapshot/fromModelSnapshot.ts +6 -2
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +2 -6
- package/src/snapshot/internal.ts +89 -19
- package/src/snapshot/onSnapshot.ts +12 -1
- package/src/snapshot/reconcileModelSnapshot.ts +6 -6
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
- package/src/standardActions/arrayActions.ts +12 -2
- package/src/standardActions/objectActions.ts +3 -2
- package/src/transforms/ImmutableDate.ts +4 -0
- package/src/transforms/asMap.ts +3 -1
- package/src/treeUtils/deepEquals.ts +15 -13
- package/src/treeUtils/draft.ts +8 -0
- package/src/treeUtils/sandbox.ts +31 -27
- package/src/tweaker/changeRollback.ts +218 -0
- package/src/tweaker/core.ts +33 -22
- package/src/tweaker/treeNodeMetadata.ts +12 -2
- package/src/tweaker/tweak.ts +183 -19
- package/src/tweaker/tweakArray.ts +176 -33
- package/src/tweaker/tweakPlainObject.ts +117 -39
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +25 -15
- package/src/types/TypeCheckError.ts +17 -19
- package/src/types/arrayBased/typesArray.ts +10 -11
- package/src/types/createCachedTypeCheck.ts +541 -88
- package/src/types/objectBased/typesDataModelData.ts +4 -2
- package/src/types/objectBased/typesModel.ts +4 -2
- package/src/types/objectBased/typesObject.ts +20 -16
- package/src/types/objectBased/typesRecord.ts +8 -18
- package/src/types/objectBased/typesRef.ts +2 -2
- package/src/types/primitiveBased/typesEnum.ts +4 -2
- package/src/types/primitiveBased/typesPrimitive.ts +9 -4
- package/src/types/typeCheckErrorUtils.ts +1 -0
- package/src/types/utility/typesCodecSupport.ts +97 -56
- package/src/types/utility/typesMap.ts +24 -41
- package/src/types/utility/typesRefinement.ts +3 -2
- package/src/types/utility/typesSet.ts +24 -0
- package/src/utils/ModelPool.ts +78 -22
- package/src/utils/chainFns.ts +4 -0
- package/src/utils/chunks.ts +271 -0
- package/src/utils/decorators.ts +1 -1
- package/src/utils/errorDiagnostics.ts +6 -2
- package/src/utils/forEachWithDelayedThrow.ts +83 -0
- package/src/utils/index.ts +71 -2
- package/src/utils/mapUtils.ts +3 -7
- package/src/utils/setIfDifferent.ts +3 -2
- package/src/utils/setLike.ts +134 -0
- package/src/wrappers/ArraySet.ts +5 -9
- package/src/wrappers/ObjectMap.ts +15 -11
- package/src/wrappers/asMap.ts +30 -18
- package/src/wrappers/asSet.ts +36 -12
- package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
- package/src/action/modelFlowPromiseGenerator.ts +0 -127
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ModelPropTransform } from "../../modelShared/prop"
|
|
2
|
-
import { isMap } from "../../utils"
|
|
2
|
+
import { isMap, setProtoProp } from "../../utils"
|
|
3
3
|
import { asMap } from "../../wrappers/asMap"
|
|
4
4
|
import { typesArray } from "../arrayBased/typesArray"
|
|
5
5
|
import { typesTuple } from "../arrayBased/typesTuple"
|
|
@@ -19,6 +19,15 @@ function makeMapProxy<TKeyStored, TKeyRuntime, TValueStored, TValueRuntime>(
|
|
|
19
19
|
keyAdapter: RuntimeAdapter<TKeyStored, TKeyRuntime>,
|
|
20
20
|
valueAdapter: RuntimeAdapter<TValueStored, TValueRuntime>
|
|
21
21
|
): Map<TKeyRuntime, TValueRuntime> {
|
|
22
|
+
// Writes through the decoded value must land back on the entry it came from.
|
|
23
|
+
const toRuntimeValue = (storedKey: TKeyStored, storedValue: TValueStored): TValueRuntime =>
|
|
24
|
+
valueAdapter.toRuntime(storedValue, (newStoredValue) => {
|
|
25
|
+
storedMap.set(storedKey, newStoredValue)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const getValue = (storedKey: TKeyStored): TValueRuntime =>
|
|
29
|
+
toRuntimeValue(storedKey, storedMap.get(storedKey) as TValueStored)
|
|
30
|
+
|
|
22
31
|
return new Proxy(storedMap, {
|
|
23
32
|
get(target, prop, receiver) {
|
|
24
33
|
switch (prop) {
|
|
@@ -29,43 +38,24 @@ function makeMapProxy<TKeyStored, TKeyRuntime, TValueStored, TValueRuntime>(
|
|
|
29
38
|
return undefined
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
return
|
|
33
|
-
target.get(storedKey) as TValueStored,
|
|
34
|
-
(newStoredValue) => {
|
|
35
|
-
target.set(storedKey, newStoredValue)
|
|
36
|
-
}
|
|
37
|
-
)
|
|
41
|
+
return getValue(storedKey)
|
|
38
42
|
}
|
|
39
43
|
case "getOrInsert":
|
|
40
44
|
return (key: TKeyRuntime, defaultValue: TValueRuntime) => {
|
|
41
45
|
const storedKey = keyAdapter.toStored(key)
|
|
42
|
-
if (target.has(storedKey)) {
|
|
43
|
-
|
|
44
|
-
target.get(storedKey) as TValueStored,
|
|
45
|
-
(newStoredValue) => {
|
|
46
|
-
target.set(storedKey, newStoredValue)
|
|
47
|
-
}
|
|
48
|
-
)
|
|
46
|
+
if (!target.has(storedKey)) {
|
|
47
|
+
target.set(storedKey, valueAdapter.toStored(defaultValue))
|
|
49
48
|
}
|
|
50
|
-
|
|
51
|
-
target.set(storedKey, valueAdapter.toStored(defaultValue))
|
|
52
|
-
return defaultValue
|
|
49
|
+
return getValue(storedKey)
|
|
53
50
|
}
|
|
54
51
|
case "getOrInsertComputed":
|
|
55
52
|
return (key: TKeyRuntime, callback: (key: TKeyRuntime) => TValueRuntime) => {
|
|
56
53
|
const storedKey = keyAdapter.toStored(key)
|
|
57
|
-
if (target.has(storedKey)) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
(newStoredValue) => {
|
|
61
|
-
target.set(storedKey, newStoredValue)
|
|
62
|
-
}
|
|
63
|
-
)
|
|
54
|
+
if (!target.has(storedKey)) {
|
|
55
|
+
const value = callback(key)
|
|
56
|
+
target.set(storedKey, valueAdapter.toStored(value))
|
|
64
57
|
}
|
|
65
|
-
|
|
66
|
-
const value = callback(key)
|
|
67
|
-
target.set(storedKey, valueAdapter.toStored(value))
|
|
68
|
-
return value
|
|
58
|
+
return getValue(storedKey)
|
|
69
59
|
}
|
|
70
60
|
case "set":
|
|
71
61
|
return (key: TKeyRuntime, value: TValueRuntime) => {
|
|
@@ -92,9 +82,7 @@ function makeMapProxy<TKeyStored, TKeyRuntime, TValueStored, TValueRuntime>(
|
|
|
92
82
|
target.forEach((value, key) => {
|
|
93
83
|
callback.call(
|
|
94
84
|
thisArg,
|
|
95
|
-
|
|
96
|
-
target.set(key, newStoredValue)
|
|
97
|
-
}),
|
|
85
|
+
toRuntimeValue(key, value),
|
|
98
86
|
keyAdapter.toRuntime(key),
|
|
99
87
|
receiver
|
|
100
88
|
)
|
|
@@ -104,12 +92,7 @@ function makeMapProxy<TKeyStored, TKeyRuntime, TValueStored, TValueRuntime>(
|
|
|
104
92
|
case Symbol.iterator:
|
|
105
93
|
return function* () {
|
|
106
94
|
for (const [key, value] of target.entries()) {
|
|
107
|
-
yield [
|
|
108
|
-
keyAdapter.toRuntime(key),
|
|
109
|
-
valueAdapter.toRuntime(value, (newStoredValue) => {
|
|
110
|
-
target.set(key, newStoredValue)
|
|
111
|
-
}),
|
|
112
|
-
] as const
|
|
95
|
+
yield [keyAdapter.toRuntime(key), toRuntimeValue(key, value)] as const
|
|
113
96
|
}
|
|
114
97
|
}
|
|
115
98
|
case "keys":
|
|
@@ -121,9 +104,7 @@ function makeMapProxy<TKeyStored, TKeyRuntime, TValueStored, TValueRuntime>(
|
|
|
121
104
|
case "values":
|
|
122
105
|
return function* () {
|
|
123
106
|
for (const [key, value] of target.entries()) {
|
|
124
|
-
yield
|
|
125
|
-
target.set(key, newStoredValue)
|
|
126
|
-
})
|
|
107
|
+
yield toRuntimeValue(key, value)
|
|
127
108
|
}
|
|
128
109
|
}
|
|
129
110
|
default:
|
|
@@ -158,7 +139,9 @@ function makeObjectBackedMapTransform<TStoredValue, TRuntimeValue>(
|
|
|
158
139
|
|
|
159
140
|
const result: Record<string, TStoredValue> = {}
|
|
160
141
|
transformedValue.forEach((value, key) => {
|
|
161
|
-
|
|
142
|
+
const storedValue = valueAdapter.toStored(value)
|
|
143
|
+
if (key === "__proto__") setProtoProp(result, storedValue)
|
|
144
|
+
else result[key] = storedValue
|
|
162
145
|
})
|
|
163
146
|
return result
|
|
164
147
|
},
|
|
@@ -41,8 +41,8 @@ import {
|
|
|
41
41
|
* @template T Base type.
|
|
42
42
|
* @param baseType Base type.
|
|
43
43
|
* @param checkFn Function that will receive the data (if it passes the base type
|
|
44
|
-
* check) and return null or
|
|
45
|
-
*
|
|
44
|
+
* check) and return null or true if there were no errors or either a TypeCheckError instance or
|
|
45
|
+
* false if there were.
|
|
46
46
|
* @returns
|
|
47
47
|
*/
|
|
48
48
|
export function typesRefinement<T extends AnyType>(
|
|
@@ -89,6 +89,7 @@ export function typesRefinement<T extends AnyType>(
|
|
|
89
89
|
// override typeCheckedValue
|
|
90
90
|
return new TypeCheckError({
|
|
91
91
|
path: refinementErr.path,
|
|
92
|
+
modelTrail: refinementErr.modelTrail,
|
|
92
93
|
expectedTypeName: refinementErr.expectedTypeName,
|
|
93
94
|
actualValue: refinementErr.actualValue,
|
|
94
95
|
typeCheckedValue,
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { ObservableSet } from "mobx"
|
|
2
2
|
import type { ModelPropTransform } from "../../modelShared/prop"
|
|
3
3
|
import { isSet } from "../../utils"
|
|
4
|
+
import {
|
|
5
|
+
isSetLikeDisjointFrom,
|
|
6
|
+
isSetLikeSubsetOf,
|
|
7
|
+
isSetLikeSupersetOf,
|
|
8
|
+
setLikeDifference,
|
|
9
|
+
setLikeIntersection,
|
|
10
|
+
setLikeSymmetricDifference,
|
|
11
|
+
setLikeUnion,
|
|
12
|
+
} from "../../utils/setLike"
|
|
4
13
|
import { asSet } from "../../wrappers/asSet"
|
|
5
14
|
import { typesArray } from "../arrayBased/typesArray"
|
|
6
15
|
import type { AnyType, ArrayType, TypeToData } from "../schemas"
|
|
@@ -52,6 +61,21 @@ function makeArrayBackedSetTransform<TStored, TRuntime>(
|
|
|
52
61
|
callback.call(thisArg, runtimeValue, runtimeValue, receiver)
|
|
53
62
|
})
|
|
54
63
|
}
|
|
64
|
+
case "union":
|
|
65
|
+
return (other: ReadonlySetLike<unknown>) => setLikeUnion(receiver, other)
|
|
66
|
+
case "intersection":
|
|
67
|
+
return (other: ReadonlySetLike<unknown>) => setLikeIntersection(receiver, other)
|
|
68
|
+
case "difference":
|
|
69
|
+
return (other: ReadonlySetLike<unknown>) => setLikeDifference(receiver, other)
|
|
70
|
+
case "symmetricDifference":
|
|
71
|
+
return (other: ReadonlySetLike<unknown>) =>
|
|
72
|
+
setLikeSymmetricDifference(receiver, other)
|
|
73
|
+
case "isSubsetOf":
|
|
74
|
+
return (other: ReadonlySetLike<unknown>) => isSetLikeSubsetOf(receiver, other)
|
|
75
|
+
case "isSupersetOf":
|
|
76
|
+
return (other: ReadonlySetLike<unknown>) => isSetLikeSupersetOf(receiver, other)
|
|
77
|
+
case "isDisjointFrom":
|
|
78
|
+
return (other: ReadonlySetLike<unknown>) => isSetLikeDisjointFrom(receiver, other)
|
|
55
79
|
case "entries":
|
|
56
80
|
return function* () {
|
|
57
81
|
for (const value of target.values()) {
|
package/src/utils/ModelPool.ts
CHANGED
|
@@ -6,25 +6,45 @@ import type { ModelClass } from "../modelShared/BaseModelShared"
|
|
|
6
6
|
import { getModelInfoForName } from "../modelShared/modelInfo"
|
|
7
7
|
import { dataToModelNode } from "../parent/core"
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
type DeepObjectChildrenExtensionAccessor,
|
|
10
|
+
type PinnedDeepObjectChildrenExtensionData,
|
|
10
11
|
registerDeepObjectChildrenExtension,
|
|
11
12
|
} from "../parent/coreObjectChildren"
|
|
13
|
+
import { getOrCreate } from "./mapUtils"
|
|
12
14
|
|
|
13
15
|
type ModelsById = Map<string, AnyModel>
|
|
14
16
|
type ModelsByTypeAndId = Map<string, ModelsById>
|
|
15
17
|
|
|
18
|
+
interface DeepChildrenModels {
|
|
19
|
+
readonly byTypeAndId: ModelsByTypeAndId
|
|
20
|
+
/** Id each model is indexed under, since ids can change later. */
|
|
21
|
+
readonly indexedIds: Map<AnyModel, string>
|
|
22
|
+
/** Whether some model was shadowed by another one with the same type and id. */
|
|
23
|
+
hasDuplicates: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A view of the models under a node, as they were when the pool was created.
|
|
28
|
+
* Must be released once no longer used.
|
|
29
|
+
*/
|
|
16
30
|
export class ModelPool {
|
|
17
|
-
private pool: ReadonlyMap<string, ReadonlyMap<string, AnyModel>>
|
|
31
|
+
private readonly pool: ReadonlyMap<string, ReadonlyMap<string, AnyModel>>
|
|
32
|
+
private readonly pinned: PinnedDeepObjectChildrenExtensionData<DeepChildrenModels>
|
|
18
33
|
|
|
19
34
|
constructor(root: object) {
|
|
20
35
|
// make sure we don't use the sub-data $ object
|
|
21
36
|
root = dataToModelNode(root)
|
|
22
37
|
|
|
23
|
-
this.
|
|
38
|
+
this.pinned = getDeepChildrenModels().pin(root)
|
|
39
|
+
this.pool = this.pinned.data.byTypeAndId
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
release(): void {
|
|
43
|
+
this.pinned.release()
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
findModelByTypeAndId(modelType: string, modelId: string | undefined): AnyModel | undefined {
|
|
27
|
-
return modelId ? this.pool.get(modelType)?.get(modelId) : undefined
|
|
47
|
+
return modelId !== undefined ? this.pool.get(modelType)?.get(modelId) : undefined
|
|
28
48
|
}
|
|
29
49
|
|
|
30
50
|
findModelForSnapshot(sn: any): AnyModel | undefined {
|
|
@@ -36,39 +56,75 @@ export class ModelPool {
|
|
|
36
56
|
const modelInfo = getModelInfoForName(modelType)!
|
|
37
57
|
const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
|
|
38
58
|
|
|
39
|
-
return modelIdPropertyName
|
|
59
|
+
return modelIdPropertyName !== undefined
|
|
40
60
|
? this.findModelByTypeAndId(modelType, (sn as any)[modelIdPropertyName])
|
|
41
61
|
: undefined
|
|
42
62
|
}
|
|
43
63
|
}
|
|
44
64
|
|
|
45
|
-
|
|
65
|
+
let deepChildrenModels: DeepObjectChildrenExtensionAccessor<DeepChildrenModels> | undefined
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const getRegisteredDeepChildrenModels = registerDeepObjectChildrenExtension<ModelsByTypeAndId>({
|
|
67
|
+
// Registered on first use to keep ModelPool out of the coreObjectChildren
|
|
68
|
+
// module-initialization cycle.
|
|
69
|
+
function getDeepChildrenModels() {
|
|
70
|
+
deepChildrenModels ??= registerDeepObjectChildrenExtension<DeepChildrenModels>({
|
|
52
71
|
initData() {
|
|
53
|
-
return
|
|
72
|
+
return {
|
|
73
|
+
byTypeAndId: new Map(),
|
|
74
|
+
indexedIds: new Map(),
|
|
75
|
+
hasDuplicates: false,
|
|
76
|
+
}
|
|
54
77
|
},
|
|
55
78
|
|
|
56
|
-
|
|
79
|
+
// last one wins when rebuilding
|
|
80
|
+
addNode(node, data) {
|
|
57
81
|
if (isModel(node)) {
|
|
58
82
|
const id = node[modelIdKey]
|
|
59
|
-
if (id) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
if (id !== undefined) {
|
|
84
|
+
const modelsById = getOrCreate(data.byTypeAndId, node[modelTypeKey], () => new Map())
|
|
85
|
+
const previous = modelsById.get(id)
|
|
86
|
+
if (previous) {
|
|
87
|
+
data.hasDuplicates = true
|
|
88
|
+
data.indexedIds.delete(previous)
|
|
65
89
|
}
|
|
66
90
|
modelsById.set(id, node)
|
|
91
|
+
data.indexedIds.set(node, id)
|
|
67
92
|
}
|
|
68
93
|
}
|
|
69
94
|
},
|
|
70
|
-
})
|
|
71
95
|
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
addNodeIncrementally(node, data) {
|
|
97
|
+
if (isModel(node)) {
|
|
98
|
+
const id = node[modelIdKey]
|
|
99
|
+
if (id !== undefined) {
|
|
100
|
+
const modelsById = getOrCreate(data.byTypeAndId, node[modelTypeKey], () => new Map())
|
|
101
|
+
if (modelsById.has(id)) {
|
|
102
|
+
// which duplicate wins depends on the deep children order
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
modelsById.set(id, node)
|
|
106
|
+
data.indexedIds.set(node, id)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return true
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
removeNode(node, data) {
|
|
113
|
+
if (isModel(node)) {
|
|
114
|
+
if (data.hasDuplicates) {
|
|
115
|
+
// a shadowed duplicate might need to take its place
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
const id = data.indexedIds.get(node)
|
|
119
|
+
if (id !== undefined) {
|
|
120
|
+
data.indexedIds.delete(node)
|
|
121
|
+
data.byTypeAndId.get(node[modelTypeKey])!.delete(id)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return true
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
dependsOnModelIds: true,
|
|
128
|
+
})
|
|
129
|
+
return deepChildrenModels
|
|
74
130
|
}
|
package/src/utils/chainFns.ts
CHANGED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Target size of the chunks large collections are split in, so a change only
|
|
3
|
+
* needs to revisit one chunk instead of every item.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export const chunkSize = 128
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export interface SequenceChunk<T> {
|
|
13
|
+
items: T[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An ordered sequence split in chunks, which splices keep near `chunkSize`:
|
|
18
|
+
* chunks are split above twice that size and merged below a quarter of it.
|
|
19
|
+
* Subclasses decide what a chunk keeps track of through the hooks.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export abstract class ChunkedSequence<T, C extends SequenceChunk<T>> {
|
|
24
|
+
chunks: C[] = []
|
|
25
|
+
|
|
26
|
+
protected abstract createChunk(items: T[]): C
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Called once items were removed from or inserted into a chunk, before
|
|
30
|
+
* resizing it. The items from `from` on changed their position in it.
|
|
31
|
+
*/
|
|
32
|
+
protected abstract onSpliced(chunk: C, from: number): void
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Called when a resize moves items to another chunk, starting at `from`.
|
|
36
|
+
* The chunk they come from was already passed to `onSpliced`.
|
|
37
|
+
*/
|
|
38
|
+
protected abstract onItemsMoved(chunk: C, from: number): void
|
|
39
|
+
|
|
40
|
+
/** Called for each chunk after the spliced ones when a splice changes the length. */
|
|
41
|
+
protected onShifted(_chunk: C): void {}
|
|
42
|
+
|
|
43
|
+
/** Splits items in new chunks, appended to the current ones. */
|
|
44
|
+
protected appendChunks(items: readonly T[]): void {
|
|
45
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
46
|
+
this.chunks.push(this.createChunk(items.slice(i, i + chunkSize)))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Replaces every chunk with new ones for the items. */
|
|
51
|
+
protected reset(items: readonly T[]): void {
|
|
52
|
+
this.chunks = []
|
|
53
|
+
this.appendChunks(items)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Chunk containing the index, and the index within it. Appends go to the last chunk. */
|
|
57
|
+
locate(index: number): [number, number] {
|
|
58
|
+
const chunks = this.chunks
|
|
59
|
+
let c = 0
|
|
60
|
+
let local = index
|
|
61
|
+
while (c < chunks.length - 1 && local >= chunks[c].items.length) {
|
|
62
|
+
local -= chunks[c].items.length
|
|
63
|
+
c++
|
|
64
|
+
}
|
|
65
|
+
return [c, local]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Removes and inserts items, like `Array.prototype.splice`. `onRemoved` is
|
|
70
|
+
* called with each removed item, the chunk it was in and its index there.
|
|
71
|
+
*/
|
|
72
|
+
protected splice(
|
|
73
|
+
index: number,
|
|
74
|
+
removedCount: number,
|
|
75
|
+
added: readonly T[],
|
|
76
|
+
onRemoved?: (item: T, chunk: C, local: number) => void
|
|
77
|
+
): void {
|
|
78
|
+
const chunks = this.chunks
|
|
79
|
+
if (chunks.length === 0) {
|
|
80
|
+
if (added.length === 0) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
chunks.push(this.createChunk([]))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const [first, start] = this.locate(index)
|
|
87
|
+
let last = first
|
|
88
|
+
let remaining = removedCount
|
|
89
|
+
let local = start
|
|
90
|
+
for (let c = first; remaining > 0; c++) {
|
|
91
|
+
const chunk = chunks[c]
|
|
92
|
+
const count = Math.min(remaining, chunk.items.length - local)
|
|
93
|
+
const removed = chunk.items.splice(local, count)
|
|
94
|
+
if (onRemoved) {
|
|
95
|
+
for (let i = 0; i < removed.length; i++) {
|
|
96
|
+
onRemoved(removed[i], chunk, local + i)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
remaining -= count
|
|
100
|
+
local = 0
|
|
101
|
+
last = c
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const firstChunk = chunks[first]
|
|
105
|
+
if (added.length > 0) {
|
|
106
|
+
firstChunk.items = firstChunk.items
|
|
107
|
+
.slice(0, start)
|
|
108
|
+
.concat(added as T[], firstChunk.items.slice(start))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.onSpliced(firstChunk, start + added.length)
|
|
112
|
+
for (let c = first + 1; c <= last; c++) {
|
|
113
|
+
this.onSpliced(chunks[c], 0)
|
|
114
|
+
}
|
|
115
|
+
if (added.length !== removedCount) {
|
|
116
|
+
for (let c = last + 1; c < chunks.length; c++) {
|
|
117
|
+
this.onShifted(chunks[c])
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// backwards, so merging or splitting a chunk does not move the ones left to visit
|
|
122
|
+
for (let c = last; c >= first; c--) {
|
|
123
|
+
this.resize(c)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Splits, merges or drops a changed chunk to keep its size near the target. */
|
|
128
|
+
private resize(c: number) {
|
|
129
|
+
const chunks = this.chunks
|
|
130
|
+
const chunk = chunks[c]
|
|
131
|
+
const length = chunk.items.length
|
|
132
|
+
|
|
133
|
+
if (length > 2 * chunkSize) {
|
|
134
|
+
const pieceCount = Math.ceil(length / chunkSize)
|
|
135
|
+
const pieceSize = Math.ceil(length / pieceCount)
|
|
136
|
+
const pieces: C[] = []
|
|
137
|
+
for (let i = pieceSize; i < length; i += pieceSize) {
|
|
138
|
+
const piece = this.createChunk(chunk.items.slice(i, i + pieceSize))
|
|
139
|
+
this.onItemsMoved(piece, 0)
|
|
140
|
+
pieces.push(piece)
|
|
141
|
+
}
|
|
142
|
+
chunk.items = chunk.items.slice(0, pieceSize)
|
|
143
|
+
this.chunks = chunks.slice(0, c + 1).concat(pieces, chunks.slice(c + 1))
|
|
144
|
+
} else if (length < chunkSize / 4) {
|
|
145
|
+
const previous = chunks[c - 1] as C | undefined
|
|
146
|
+
const next = chunks[c + 1] as C | undefined
|
|
147
|
+
if (previous && previous.items.length + length <= 2 * chunkSize) {
|
|
148
|
+
const from = previous.items.length
|
|
149
|
+
previous.items = previous.items.concat(chunk.items)
|
|
150
|
+
this.onItemsMoved(previous, from)
|
|
151
|
+
chunks.splice(c, 1)
|
|
152
|
+
} else if (next && next.items.length + length <= 2 * chunkSize) {
|
|
153
|
+
next.items = chunk.items.concat(next.items)
|
|
154
|
+
this.onItemsMoved(next, 0)
|
|
155
|
+
chunks.splice(c, 1)
|
|
156
|
+
} else if (length === 0) {
|
|
157
|
+
chunks.splice(c, 1)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* An unordered set split in chunks. Items are added to the last chunk, and a
|
|
165
|
+
* chunk left under a quarter of `chunkSize` by removals is merged into a
|
|
166
|
+
* neighbor (or dropped once empty). Subclasses decide what a chunk keeps track
|
|
167
|
+
* of through the hooks.
|
|
168
|
+
*
|
|
169
|
+
* @internal
|
|
170
|
+
*/
|
|
171
|
+
export abstract class ChunkedSet<T, C extends SequenceChunk<T>> {
|
|
172
|
+
chunks: C[] = []
|
|
173
|
+
/** Chunk of each item, only needed (and kept) once an item is looked up. */
|
|
174
|
+
private chunkOf: Map<T, C> | undefined
|
|
175
|
+
|
|
176
|
+
protected abstract createChunk(items: T[]): C
|
|
177
|
+
|
|
178
|
+
/** Called when items were added to or removed from a chunk. */
|
|
179
|
+
protected abstract onChunkChanged(chunk: C): void
|
|
180
|
+
|
|
181
|
+
/** Called when chunks were added or removed. */
|
|
182
|
+
protected abstract onChunksChanged(): void
|
|
183
|
+
|
|
184
|
+
/** Called for a chunk removed after its items were moved to another one. */
|
|
185
|
+
protected onChunkDropped(_chunk: C): void {}
|
|
186
|
+
|
|
187
|
+
/** Splits items in new chunks, appended to the current ones. */
|
|
188
|
+
protected appendChunks(items: readonly T[]): void {
|
|
189
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
190
|
+
this.chunks.push(this.createChunk(items.slice(i, i + chunkSize)))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Replaces every chunk with new ones for the items. */
|
|
195
|
+
protected reset(items: readonly T[]): void {
|
|
196
|
+
this.chunks = []
|
|
197
|
+
this.chunkOf = undefined
|
|
198
|
+
this.appendChunks(items)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
add(item: T): void {
|
|
202
|
+
let chunk = this.chunks[this.chunks.length - 1] as C | undefined
|
|
203
|
+
if (chunk && chunk.items.length < chunkSize) {
|
|
204
|
+
chunk.items.push(item)
|
|
205
|
+
this.onChunkChanged(chunk)
|
|
206
|
+
} else {
|
|
207
|
+
chunk = this.createChunk([item])
|
|
208
|
+
this.chunks.push(chunk)
|
|
209
|
+
this.onChunksChanged()
|
|
210
|
+
}
|
|
211
|
+
this.chunkOf?.set(item, chunk)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Chunk containing the item, if any. */
|
|
215
|
+
getChunkOf(item: T): C | undefined {
|
|
216
|
+
return this.getChunkOfMap().get(item)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private getChunkOfMap(): Map<T, C> {
|
|
220
|
+
if (!this.chunkOf) {
|
|
221
|
+
this.chunkOf = new Map()
|
|
222
|
+
for (const chunk of this.chunks) {
|
|
223
|
+
for (const i of chunk.items) {
|
|
224
|
+
this.chunkOf.set(i, chunk)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return this.chunkOf
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Returns false if the item was not in the set. */
|
|
232
|
+
delete(item: T): boolean {
|
|
233
|
+
const chunkOf = this.getChunkOfMap()
|
|
234
|
+
const chunk = chunkOf.get(item)
|
|
235
|
+
if (!chunk) {
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
chunkOf.delete(item)
|
|
239
|
+
// (the order within a chunk does not matter, so the last item fills the gap)
|
|
240
|
+
const items = chunk.items
|
|
241
|
+
const last = items.pop()!
|
|
242
|
+
if (last !== item) {
|
|
243
|
+
items[items.lastIndexOf(item)] = last
|
|
244
|
+
}
|
|
245
|
+
this.onChunkChanged(chunk)
|
|
246
|
+
if (chunk.items.length >= chunkSize / 4) {
|
|
247
|
+
return true
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// merge it into a neighbor, or drop it once empty
|
|
251
|
+
const chunks = this.chunks
|
|
252
|
+
const c = chunks.indexOf(chunk)
|
|
253
|
+
if (chunk.items.length > 0) {
|
|
254
|
+
const neighbor = [chunks[c - 1], chunks[c + 1]].find(
|
|
255
|
+
(n) => n && n.items.length + chunk.items.length <= 2 * chunkSize
|
|
256
|
+
)
|
|
257
|
+
if (!neighbor) {
|
|
258
|
+
return true
|
|
259
|
+
}
|
|
260
|
+
for (const i of chunk.items) {
|
|
261
|
+
chunkOf.set(i, neighbor)
|
|
262
|
+
}
|
|
263
|
+
neighbor.items = neighbor.items.concat(chunk.items)
|
|
264
|
+
this.onChunkChanged(neighbor)
|
|
265
|
+
}
|
|
266
|
+
chunks.splice(c, 1)
|
|
267
|
+
this.onChunkDropped(chunk)
|
|
268
|
+
this.onChunksChanged()
|
|
269
|
+
return true
|
|
270
|
+
}
|
|
271
|
+
}
|
package/src/utils/decorators.ts
CHANGED
|
@@ -110,7 +110,7 @@ export function decorateWrapMethodOrField(
|
|
|
110
110
|
const baseDescriptor: PropertyDescriptor | undefined = args[2]
|
|
111
111
|
|
|
112
112
|
checkModelDecoratorTaget(decoratorName, target)
|
|
113
|
-
checkDecoratorContext(
|
|
113
|
+
checkDecoratorContext(decoratorName, propertyKey, false)
|
|
114
114
|
|
|
115
115
|
const data = getActionNameAndContextOverride(target, propertyKey, true)
|
|
116
116
|
|
|
@@ -190,7 +190,7 @@ function formatErrorPathForMessage(path: Path | undefined = getErrorPathSnapshot
|
|
|
190
190
|
/**
|
|
191
191
|
* @internal
|
|
192
192
|
*/
|
|
193
|
-
function getSafeErrorValuePreview(value: unknown, maxLen = 200): string {
|
|
193
|
+
export function getSafeErrorValuePreview(value: unknown, maxLen = 200): string {
|
|
194
194
|
let str: string | undefined
|
|
195
195
|
try {
|
|
196
196
|
str = JSON.stringify(value)
|
|
@@ -203,7 +203,11 @@ function getSafeErrorValuePreview(value: unknown, maxLen = 200): string {
|
|
|
203
203
|
try {
|
|
204
204
|
str = String(value)
|
|
205
205
|
} catch {
|
|
206
|
-
|
|
206
|
+
try {
|
|
207
|
+
str = Object.prototype.toString.call(value)
|
|
208
|
+
} catch {
|
|
209
|
+
str = "<unprintable value>"
|
|
210
|
+
}
|
|
207
211
|
}
|
|
208
212
|
}
|
|
209
213
|
|