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
|
@@ -41,18 +41,14 @@ function makeMapProxy<TKeyStored, TKeyRuntime, TValueStored, TValueRuntime>(
|
|
|
41
41
|
return getValue(storedKey)
|
|
42
42
|
}
|
|
43
43
|
case "getOrInsert":
|
|
44
|
-
return (key: TKeyRuntime, defaultValue: TValueRuntime) => {
|
|
45
|
-
const storedKey = keyAdapter.toStored(key)
|
|
46
|
-
if (!target.has(storedKey)) {
|
|
47
|
-
target.set(storedKey, valueAdapter.toStored(defaultValue))
|
|
48
|
-
}
|
|
49
|
-
return getValue(storedKey)
|
|
50
|
-
}
|
|
51
44
|
case "getOrInsertComputed":
|
|
52
|
-
return (key: TKeyRuntime,
|
|
45
|
+
return (key: TKeyRuntime, valueOrCallback: unknown) => {
|
|
53
46
|
const storedKey = keyAdapter.toStored(key)
|
|
54
47
|
if (!target.has(storedKey)) {
|
|
55
|
-
const value =
|
|
48
|
+
const value =
|
|
49
|
+
prop === "getOrInsert"
|
|
50
|
+
? (valueOrCallback as TValueRuntime)
|
|
51
|
+
: (valueOrCallback as (key: TKeyRuntime) => TValueRuntime)(key)
|
|
56
52
|
target.set(storedKey, valueAdapter.toStored(value))
|
|
57
53
|
}
|
|
58
54
|
return getValue(storedKey)
|
|
@@ -114,10 +110,18 @@ function makeMapProxy<TKeyStored, TKeyRuntime, TValueStored, TValueRuntime>(
|
|
|
114
110
|
}) as unknown as Map<TKeyRuntime, TValueRuntime>
|
|
115
111
|
}
|
|
116
112
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
function makeMapTransform<
|
|
114
|
+
TStored extends object,
|
|
115
|
+
TKeyStored,
|
|
116
|
+
TKeyRuntime,
|
|
117
|
+
TValueStored,
|
|
118
|
+
TValueRuntime,
|
|
119
|
+
>(
|
|
120
|
+
keyAdapter: RuntimeAdapter<TKeyStored, TKeyRuntime>,
|
|
121
|
+
valueAdapter: RuntimeAdapter<TValueStored, TValueRuntime>,
|
|
122
|
+
toStored: (runtimeMap: Map<TKeyRuntime, TValueRuntime>) => TStored
|
|
123
|
+
): ModelPropTransform<TStored, Map<TKeyRuntime, TValueRuntime>> {
|
|
124
|
+
const storedByRuntime = new WeakMap<Map<TKeyRuntime, TValueRuntime>, TStored>()
|
|
121
125
|
|
|
122
126
|
return {
|
|
123
127
|
transform({ originalValue, cachedTransformedValue }) {
|
|
@@ -125,63 +129,51 @@ function makeObjectBackedMapTransform<TStoredValue, TRuntimeValue>(
|
|
|
125
129
|
return cachedTransformedValue
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
const runtimeMap = makeMapProxy(
|
|
132
|
+
const runtimeMap = makeMapProxy(
|
|
133
|
+
asMap(originalValue as any) as Map<TKeyStored, TValueStored>,
|
|
134
|
+
keyAdapter,
|
|
135
|
+
valueAdapter
|
|
136
|
+
)
|
|
129
137
|
|
|
130
138
|
storedByRuntime.set(runtimeMap, originalValue)
|
|
131
139
|
return runtimeMap
|
|
132
140
|
},
|
|
133
141
|
|
|
134
142
|
untransform({ transformedValue }) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
return storedByRuntime.get(transformedValue) ?? toStored(transformedValue)
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
139
147
|
|
|
148
|
+
function makeObjectBackedMapTransform<TStoredValue, TRuntimeValue>(
|
|
149
|
+
valueAdapter: RuntimeAdapter<TStoredValue, TRuntimeValue>
|
|
150
|
+
): ModelPropTransform<Record<string, TStoredValue>, Map<string, TRuntimeValue>> {
|
|
151
|
+
return makeMapTransform(
|
|
152
|
+
identityRuntimeAdapter as RuntimeAdapter<string, string>,
|
|
153
|
+
valueAdapter,
|
|
154
|
+
(runtimeMap) => {
|
|
140
155
|
const result: Record<string, TStoredValue> = {}
|
|
141
|
-
|
|
156
|
+
runtimeMap.forEach((value, key) => {
|
|
142
157
|
const storedValue = valueAdapter.toStored(value)
|
|
143
158
|
if (key === "__proto__") setProtoProp(result, storedValue)
|
|
144
159
|
else result[key] = storedValue
|
|
145
160
|
})
|
|
146
161
|
return result
|
|
147
|
-
}
|
|
148
|
-
|
|
162
|
+
}
|
|
163
|
+
)
|
|
149
164
|
}
|
|
150
165
|
|
|
151
166
|
function makeArrayBackedMapTransform<TKeyStored, TKeyRuntime, TValueStored, TValueRuntime>(
|
|
152
167
|
keyAdapter: RuntimeAdapter<TKeyStored, TKeyRuntime>,
|
|
153
168
|
valueAdapter: RuntimeAdapter<TValueStored, TValueRuntime>
|
|
154
169
|
): ModelPropTransform<Array<[TKeyStored, TValueStored]>, Map<TKeyRuntime, TValueRuntime>> {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (cachedTransformedValue) {
|
|
163
|
-
return cachedTransformedValue
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const runtimeMap = makeMapProxy(asMap(originalValue), keyAdapter, valueAdapter)
|
|
167
|
-
|
|
168
|
-
storedByRuntime.set(runtimeMap, originalValue)
|
|
169
|
-
return runtimeMap
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
untransform({ transformedValue }) {
|
|
173
|
-
const cachedStored = storedByRuntime.get(transformedValue)
|
|
174
|
-
if (cachedStored) {
|
|
175
|
-
return cachedStored
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const result: Array<[TKeyStored, TValueStored]> = []
|
|
179
|
-
transformedValue.forEach((value, key) => {
|
|
180
|
-
result.push([keyAdapter.toStored(key), valueAdapter.toStored(value)])
|
|
181
|
-
})
|
|
182
|
-
return result
|
|
183
|
-
},
|
|
184
|
-
}
|
|
170
|
+
return makeMapTransform(keyAdapter, valueAdapter, (runtimeMap) => {
|
|
171
|
+
const result: Array<[TKeyStored, TValueStored]> = []
|
|
172
|
+
runtimeMap.forEach((value, key) => {
|
|
173
|
+
result.push([keyAdapter.toStored(key), valueAdapter.toStored(value)])
|
|
174
|
+
})
|
|
175
|
+
return result
|
|
176
|
+
})
|
|
185
177
|
}
|
|
186
178
|
|
|
187
179
|
export function typesMapFromObject<TValueType extends AnyType>(
|
|
@@ -62,6 +62,11 @@ export function typesRefinement<T extends AnyType>(
|
|
|
62
62
|
return `${refinementName}<${baseTypeName}>`
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
const processorPlan = snapshotProcessorPlan(
|
|
66
|
+
() => [baseChecker],
|
|
67
|
+
([processor]) => processor
|
|
68
|
+
)
|
|
69
|
+
|
|
65
70
|
const thisTc: TypeChecker = new TypeChecker(
|
|
66
71
|
baseChecker.baseType,
|
|
67
72
|
|
|
@@ -103,14 +108,8 @@ export function typesRefinement<T extends AnyType>(
|
|
|
103
108
|
// we cannot check refinement here since it checks data instances, not snapshots
|
|
104
109
|
(sn) => baseChecker.snapshotType(sn),
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
([processor]) => processor
|
|
109
|
-
),
|
|
110
|
-
snapshotProcessorPlan(
|
|
111
|
-
() => [baseChecker],
|
|
112
|
-
([processor]) => processor
|
|
113
|
-
)
|
|
111
|
+
processorPlan,
|
|
112
|
+
processorPlan
|
|
114
113
|
)
|
|
115
114
|
|
|
116
115
|
return thisTc
|
|
@@ -42,20 +42,19 @@ export function typesSkipCheck<T extends AnyType>(baseType: T): T {
|
|
|
42
42
|
return `skipCheck<${baseTypeName}>`
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
const processorPlan = snapshotProcessorPlan(
|
|
46
|
+
() => [baseChecker],
|
|
47
|
+
([processor]) => processor
|
|
48
|
+
)
|
|
49
|
+
|
|
45
50
|
const thisTc: TypeChecker = new TypeChecker(
|
|
46
51
|
baseChecker.baseType,
|
|
47
52
|
(_data, _path, _typeCheckedValue) => null, // always passes validation
|
|
48
53
|
getTypeName,
|
|
49
54
|
typeInfoGen,
|
|
50
55
|
(sn) => baseChecker.snapshotType(sn),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
([processor]) => processor
|
|
54
|
-
),
|
|
55
|
-
snapshotProcessorPlan(
|
|
56
|
-
() => [baseChecker],
|
|
57
|
-
([processor]) => processor
|
|
58
|
-
)
|
|
56
|
+
processorPlan,
|
|
57
|
+
processorPlan
|
|
59
58
|
)
|
|
60
59
|
thisTc.skipCheck = true
|
|
61
60
|
|
|
@@ -52,20 +52,19 @@ export function typesTag<T extends AnyType, A>(baseType: T, tag: A, typeName?: s
|
|
|
52
52
|
return `${taggedName}<${baseTypeName}>`
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
const processorPlan = snapshotProcessorPlan(
|
|
56
|
+
() => [baseChecker],
|
|
57
|
+
([processor]) => processor
|
|
58
|
+
)
|
|
59
|
+
|
|
55
60
|
const thisTc: TypeChecker = new TypeChecker(
|
|
56
61
|
baseChecker.baseType,
|
|
57
62
|
(data, path, typeCheckedValue) => baseChecker.check(data, path, typeCheckedValue),
|
|
58
63
|
getTypeName,
|
|
59
64
|
typeInfoGen,
|
|
60
65
|
(sn) => baseChecker.snapshotType(sn),
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
([processor]) => processor
|
|
64
|
-
),
|
|
65
|
-
snapshotProcessorPlan(
|
|
66
|
-
() => [baseChecker],
|
|
67
|
-
([processor]) => processor
|
|
68
|
-
)
|
|
66
|
+
processorPlan,
|
|
67
|
+
processorPlan
|
|
69
68
|
)
|
|
70
69
|
|
|
71
70
|
return thisTc
|
package/src/utils/ModelPool.ts
CHANGED
|
@@ -1,74 +1,113 @@
|
|
|
1
1
|
import type { AnyModel } from "../model/BaseModel"
|
|
2
|
-
import { getModelIdPropertyName } from "../model/getModelMetadata"
|
|
3
2
|
import { modelIdKey, modelTypeKey } from "../model/metadata"
|
|
4
|
-
import {
|
|
5
|
-
import type { ModelClass } from "../modelShared/BaseModelShared"
|
|
6
|
-
import { getModelInfoForName } from "../modelShared/modelInfo"
|
|
3
|
+
import { isModel } from "../model/utils"
|
|
7
4
|
import { dataToModelNode } from "../parent/core"
|
|
8
5
|
import {
|
|
9
|
-
|
|
6
|
+
type DeepObjectChildrenExtensionAccessor,
|
|
7
|
+
type PinnedDeepObjectChildrenExtensionData,
|
|
10
8
|
registerDeepObjectChildrenExtension,
|
|
11
9
|
} from "../parent/coreObjectChildren"
|
|
10
|
+
import { getOrCreate } from "./mapUtils"
|
|
12
11
|
|
|
13
12
|
type ModelsById = Map<string, AnyModel>
|
|
14
13
|
type ModelsByTypeAndId = Map<string, ModelsById>
|
|
15
14
|
|
|
15
|
+
interface DeepChildrenModels {
|
|
16
|
+
readonly byTypeAndId: ModelsByTypeAndId
|
|
17
|
+
/** Id each model is indexed under, since ids can change later. */
|
|
18
|
+
readonly indexedIds: Map<AnyModel, string>
|
|
19
|
+
/** Whether some model was shadowed by another one with the same type and id. */
|
|
20
|
+
hasDuplicates: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A view of the models under a node, as they were when the pool was created.
|
|
25
|
+
* Must be released once no longer used.
|
|
26
|
+
*/
|
|
16
27
|
export class ModelPool {
|
|
17
|
-
private pool: ReadonlyMap<string, ReadonlyMap<string, AnyModel>>
|
|
28
|
+
private readonly pool: ReadonlyMap<string, ReadonlyMap<string, AnyModel>>
|
|
29
|
+
private readonly pinned: PinnedDeepObjectChildrenExtensionData<DeepChildrenModels>
|
|
18
30
|
|
|
19
31
|
constructor(root: object) {
|
|
20
32
|
// make sure we don't use the sub-data $ object
|
|
21
33
|
root = dataToModelNode(root)
|
|
22
34
|
|
|
23
|
-
this.
|
|
35
|
+
this.pinned = getDeepChildrenModels().pin(root)
|
|
36
|
+
this.pool = this.pinned.data.byTypeAndId
|
|
24
37
|
}
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
release(): void {
|
|
40
|
+
this.pinned.release()
|
|
28
41
|
}
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (modelType === undefined) {
|
|
33
|
-
return undefined
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const modelInfo = getModelInfoForName(modelType)!
|
|
37
|
-
const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
|
|
38
|
-
|
|
39
|
-
return modelIdPropertyName !== undefined
|
|
40
|
-
? this.findModelByTypeAndId(modelType, (sn as any)[modelIdPropertyName])
|
|
41
|
-
: undefined
|
|
43
|
+
findModelByTypeAndId(modelType: string, modelId: string | undefined): AnyModel | undefined {
|
|
44
|
+
return modelId !== undefined ? this.pool.get(modelType)?.get(modelId) : undefined
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
let deepChildrenModels: DeepObjectChildrenExtensionAccessor<DeepChildrenModels> | undefined
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const getRegisteredDeepChildrenModels = registerDeepObjectChildrenExtension<ModelsByTypeAndId>({
|
|
50
|
+
// Registered on first use to keep ModelPool out of the coreObjectChildren
|
|
51
|
+
// module-initialization cycle.
|
|
52
|
+
function getDeepChildrenModels() {
|
|
53
|
+
deepChildrenModels ??= registerDeepObjectChildrenExtension<DeepChildrenModels>({
|
|
52
54
|
initData() {
|
|
53
|
-
return
|
|
55
|
+
return {
|
|
56
|
+
byTypeAndId: new Map(),
|
|
57
|
+
indexedIds: new Map(),
|
|
58
|
+
hasDuplicates: false,
|
|
59
|
+
}
|
|
54
60
|
},
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
// last one wins when rebuilding
|
|
63
|
+
addNode(node, data) {
|
|
57
64
|
if (isModel(node)) {
|
|
58
65
|
const id = node[modelIdKey]
|
|
59
66
|
if (id !== undefined) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
const modelsById = getOrCreate(data.byTypeAndId, node[modelTypeKey], () => new Map())
|
|
68
|
+
const previous = modelsById.get(id)
|
|
69
|
+
if (previous) {
|
|
70
|
+
data.hasDuplicates = true
|
|
71
|
+
data.indexedIds.delete(previous)
|
|
65
72
|
}
|
|
66
73
|
modelsById.set(id, node)
|
|
74
|
+
data.indexedIds.set(node, id)
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
},
|
|
70
|
-
})
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
addNodeIncrementally(node, data) {
|
|
80
|
+
if (isModel(node)) {
|
|
81
|
+
const id = node[modelIdKey]
|
|
82
|
+
if (id !== undefined) {
|
|
83
|
+
const modelsById = getOrCreate(data.byTypeAndId, node[modelTypeKey], () => new Map())
|
|
84
|
+
if (modelsById.has(id)) {
|
|
85
|
+
// which duplicate wins depends on the deep children order
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
modelsById.set(id, node)
|
|
89
|
+
data.indexedIds.set(node, id)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return true
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
removeNode(node, data) {
|
|
96
|
+
if (isModel(node)) {
|
|
97
|
+
if (data.hasDuplicates) {
|
|
98
|
+
// a shadowed duplicate might need to take its place
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
const id = data.indexedIds.get(node)
|
|
102
|
+
if (id !== undefined) {
|
|
103
|
+
data.indexedIds.delete(node)
|
|
104
|
+
data.byTypeAndId.get(node[modelTypeKey])!.delete(id)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return true
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
dependsOnModelIds: true,
|
|
111
|
+
})
|
|
112
|
+
return deepChildrenModels
|
|
74
113
|
}
|
|
@@ -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
|
@@ -109,7 +109,7 @@ export function decorateWrapMethodOrField(
|
|
|
109
109
|
const propertyKey: string = args[1]
|
|
110
110
|
const baseDescriptor: PropertyDescriptor | undefined = args[2]
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
checkModelDecoratorTarget(decoratorName, target)
|
|
113
113
|
checkDecoratorContext(decoratorName, propertyKey, false)
|
|
114
114
|
|
|
115
115
|
const data = getActionNameAndContextOverride(target, propertyKey, true)
|
|
@@ -166,7 +166,7 @@ export function decorateWrapMethodOrField(
|
|
|
166
166
|
inited = true
|
|
167
167
|
|
|
168
168
|
const target = this
|
|
169
|
-
|
|
169
|
+
checkModelDecoratorTarget(decoratorName, target)
|
|
170
170
|
|
|
171
171
|
// find the deepest proto that matches the value
|
|
172
172
|
let proto = this
|
|
@@ -195,7 +195,7 @@ export function decorateWrapMethodOrField(
|
|
|
195
195
|
const instance = this
|
|
196
196
|
|
|
197
197
|
if (!data) {
|
|
198
|
-
|
|
198
|
+
checkModelDecoratorTarget(decoratorName, instance)
|
|
199
199
|
data = getActionNameAndContextOverride(instance, propertyKey, false)
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -207,7 +207,7 @@ export function decorateWrapMethodOrField(
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
default:
|
|
210
|
-
throw failure(`@${decoratorName} can only be used on fields or methods
|
|
210
|
+
throw failure(`@${decoratorName} can only be used on fields or methods`)
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
}
|
|
@@ -270,7 +270,7 @@ function getActionNameAndContextOverride(
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
function
|
|
273
|
+
function checkModelDecoratorTarget(decoratorName: string, target: any) {
|
|
274
274
|
if (!inDevMode) {
|
|
275
275
|
return
|
|
276
276
|
}
|