mobx-keystone 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/dist/mobx-keystone.esm.js +2561 -1214
  4. package/dist/mobx-keystone.esm.mjs +2561 -1214
  5. package/dist/mobx-keystone.umd.js +2566 -1220
  6. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  7. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +11 -12
  8. package/dist/types/modelShared/modelDecorator.d.ts +5 -2
  9. package/dist/types/modelShared/prop.d.ts +42 -29
  10. package/dist/types/parent/arrayPaths.d.ts +1 -0
  11. package/dist/types/parent/treeOrder.d.ts +1 -0
  12. package/dist/types/patch/applyPatches.d.ts +2 -1
  13. package/dist/types/ref/Ref.d.ts +1 -4
  14. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  15. package/dist/types/ref/nodesById.d.ts +1 -0
  16. package/dist/types/ref/refIdIndex.d.ts +1 -0
  17. package/dist/types/snapshot/SnapshotOf.d.ts +4 -4
  18. package/dist/types/snapshot/applySnapshot.d.ts +2 -1
  19. package/dist/types/snapshot/fromSnapshot.d.ts +2 -5
  20. package/dist/types/snapshot/getSnapshot.d.ts +1 -1
  21. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  22. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  23. package/dist/types/types/index.d.ts +1 -0
  24. package/dist/types/types/schemas.d.ts +41 -37
  25. package/dist/types/types/tProp.d.ts +12 -6
  26. package/dist/types/types/types.d.ts +6 -6
  27. package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
  28. package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
  29. package/dist/types/utils/ModelPool.d.ts +7 -2
  30. package/dist/types/utils/chunks.d.ts +1 -0
  31. package/dist/types/utils/types.d.ts +6 -0
  32. package/dist/types/wrappers/ArraySet.d.ts +1 -0
  33. package/package.json +5 -3
  34. package/src/action/modelFlow.ts +35 -66
  35. package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
  36. package/src/actionMiddlewares/transactionMiddleware.ts +41 -42
  37. package/src/actionMiddlewares/undoMiddleware.ts +63 -35
  38. package/src/context/context.ts +15 -36
  39. package/src/dataModel/utils.ts +5 -28
  40. package/src/deepChange/onDeepChange.ts +44 -126
  41. package/src/model/BaseModel.ts +7 -5
  42. package/src/model/utils.ts +5 -10
  43. package/src/modelShared/modelDecorator.ts +16 -30
  44. package/src/modelShared/prop.ts +115 -111
  45. package/src/modelShared/sharedInternalModel.ts +2 -2
  46. package/src/parent/arrayPaths.ts +239 -0
  47. package/src/parent/coreObjectChildren.ts +300 -48
  48. package/src/parent/onChildAttachedTo.ts +130 -44
  49. package/src/parent/path.ts +10 -12
  50. package/src/parent/setParent.ts +27 -15
  51. package/src/parent/treeOrder.ts +53 -0
  52. package/src/parent/walkTree.ts +0 -73
  53. package/src/patch/applyPatches.ts +31 -26
  54. package/src/patch/patchRecorder.ts +13 -28
  55. package/src/ref/core.ts +69 -46
  56. package/src/ref/modelRefIdIndex.ts +131 -0
  57. package/src/ref/nodesById.ts +60 -0
  58. package/src/ref/refIdIndex.ts +209 -0
  59. package/src/rootStore/rootStore.ts +1 -3
  60. package/src/snapshot/SnapshotOf.ts +24 -48
  61. package/src/snapshot/applySnapshot.ts +27 -19
  62. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  63. package/src/snapshot/fromSnapshot.ts +12 -9
  64. package/src/snapshot/getSnapshot.ts +1 -1
  65. package/src/snapshot/internal.ts +67 -12
  66. package/src/snapshot/reconcileModelSnapshot.ts +5 -3
  67. package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
  68. package/src/treeUtils/draft.ts +6 -1
  69. package/src/treeUtils/sandbox.ts +10 -7
  70. package/src/tweaker/changeRollback.ts +218 -0
  71. package/src/tweaker/core.ts +33 -20
  72. package/src/tweaker/treeNodeMetadata.ts +12 -2
  73. package/src/tweaker/tweak.ts +181 -18
  74. package/src/tweaker/tweakArray.ts +127 -77
  75. package/src/tweaker/tweakPlainObject.ts +65 -46
  76. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  77. package/src/tweaker/typeChecking.ts +5 -104
  78. package/src/types/arrayBased/typesArray.ts +23 -30
  79. package/src/types/arrayBased/typesTuple.ts +16 -25
  80. package/src/types/createCachedTypeCheck.ts +509 -94
  81. package/src/types/index.ts +1 -0
  82. package/src/types/objectBased/typesDataModelData.ts +7 -8
  83. package/src/types/objectBased/typesObject.ts +11 -15
  84. package/src/types/objectBased/typesRecord.ts +12 -19
  85. package/src/types/schemas.ts +59 -71
  86. package/src/types/tProp.ts +29 -29
  87. package/src/types/utility/typeToStoredData.ts +22 -21
  88. package/src/types/utility/typesCodecCore.ts +5 -0
  89. package/src/types/utility/typesCodecSupport.ts +3 -3
  90. package/src/types/utility/typesMap.ts +43 -51
  91. package/src/types/utility/typesRefinement.ts +7 -8
  92. package/src/types/utility/typesSkipCheck.ts +7 -8
  93. package/src/types/utility/typesTag.ts +7 -8
  94. package/src/utils/ModelPool.ts +76 -37
  95. package/src/utils/chunks.ts +271 -0
  96. package/src/utils/decorators.ts +5 -5
  97. package/src/utils/index.ts +38 -20
  98. package/src/utils/types.ts +5 -0
  99. package/src/wrappers/ArraySet.ts +27 -14
  100. package/src/wrappers/ObjectMap.ts +1 -6
  101. package/src/wrappers/asMap.ts +1 -21
  102. package/src/wrappers/asSet.ts +0 -8
@@ -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, callback: (key: TKeyRuntime) => TValueRuntime) => {
45
+ return (key: TKeyRuntime, valueOrCallback: unknown) => {
53
46
  const storedKey = keyAdapter.toStored(key)
54
47
  if (!target.has(storedKey)) {
55
- const value = callback(key)
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 makeObjectBackedMapTransform<TStoredValue, TRuntimeValue>(
118
- valueAdapter: RuntimeAdapter<TStoredValue, TRuntimeValue>
119
- ): ModelPropTransform<Record<string, TStoredValue>, Map<string, TRuntimeValue>> {
120
- const storedByRuntime = new WeakMap<Map<string, TRuntimeValue>, Record<string, TStoredValue>>()
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(asMap(originalValue), identityRuntimeAdapter, valueAdapter)
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
- const cachedStored = storedByRuntime.get(transformedValue)
136
- if (cachedStored) {
137
- return cachedStored
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
- transformedValue.forEach((value, key) => {
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
- const storedByRuntime = new WeakMap<
156
- Map<TKeyRuntime, TValueRuntime>,
157
- Array<[TKeyStored, TValueStored]>
158
- >()
159
-
160
- return {
161
- transform({ originalValue, cachedTransformedValue }) {
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
- snapshotProcessorPlan(
107
- () => [baseChecker],
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
- snapshotProcessorPlan(
52
- () => [baseChecker],
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
- snapshotProcessorPlan(
62
- () => [baseChecker],
63
- ([processor]) => processor
64
- ),
65
- snapshotProcessorPlan(
66
- () => [baseChecker],
67
- ([processor]) => processor
68
- )
66
+ processorPlan,
67
+ processorPlan
69
68
  )
70
69
 
71
70
  return thisTc
@@ -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 { getSnapshotModelType, isModel } from "../model/utils"
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
- getDeepObjectChildren,
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.pool = getDeepChildrenModels(getDeepObjectChildren(root))
35
+ this.pinned = getDeepChildrenModels().pin(root)
36
+ this.pool = this.pinned.data.byTypeAndId
24
37
  }
25
38
 
26
- findModelByTypeAndId(modelType: string, modelId: string | undefined): AnyModel | undefined {
27
- return modelId !== undefined ? this.pool.get(modelType)?.get(modelId) : undefined
39
+ release(): void {
40
+ this.pinned.release()
28
41
  }
29
42
 
30
- findModelForSnapshot(sn: any): AnyModel | undefined {
31
- const modelType = getSnapshotModelType(sn)
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
- type GetDeepChildrenModels = (data: ReturnType<typeof getDeepObjectChildren>) => ModelsByTypeAndId
48
+ let deepChildrenModels: DeepObjectChildrenExtensionAccessor<DeepChildrenModels> | undefined
46
49
 
47
- let getDeepChildrenModels: GetDeepChildrenModels = (data) => {
48
- // Register on first use to keep ModelPool out of the coreObjectChildren
49
- // module-initialization cycle. The extension getter backfills its index from
50
- // data.deep, so registering after a deep-children set already exists is safe.
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 new Map()
55
+ return {
56
+ byTypeAndId: new Map(),
57
+ indexedIds: new Map(),
58
+ hasDuplicates: false,
59
+ }
54
60
  },
55
61
 
56
- addNode(node, extensionData) {
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 modelType = node[modelTypeKey]
61
- let modelsById = extensionData.get(modelType)
62
- if (!modelsById) {
63
- modelsById = new Map()
64
- extensionData.set(modelType, modelsById)
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
- getDeepChildrenModels = getRegisteredDeepChildrenModels
73
- return getRegisteredDeepChildrenModels(data)
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
+ }
@@ -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
- checkModelDecoratorTaget(decoratorName, target)
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
- checkModelDecoratorTaget(decoratorName, target)
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
- checkModelDecoratorTaget(decoratorName, instance)
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 checkModelDecoratorTaget(decoratorName: string, target: any) {
273
+ function checkModelDecoratorTarget(decoratorName: string, target: any) {
274
274
  if (!inDevMode) {
275
275
  return
276
276
  }