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.
Files changed (141) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/dist/mobx-keystone.esm.js +3627 -1540
  3. package/dist/mobx-keystone.esm.mjs +3627 -1540
  4. package/dist/mobx-keystone.umd.js +3638 -1543
  5. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  6. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  7. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
  8. package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
  9. package/dist/types/deepChange/onDeepChange.d.ts +9 -0
  10. package/dist/types/parent/arrayPaths.d.ts +1 -0
  11. package/dist/types/parent/detach.d.ts +1 -1
  12. package/dist/types/parent/findChildren.d.ts +1 -1
  13. package/dist/types/parent/treeOrder.d.ts +1 -0
  14. package/dist/types/patch/applyPatches.d.ts +3 -0
  15. package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
  16. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  17. package/dist/types/ref/nodesById.d.ts +1 -0
  18. package/dist/types/ref/refIdIndex.d.ts +1 -0
  19. package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
  20. package/dist/types/snapshot/applySnapshot.d.ts +1 -0
  21. package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
  22. package/dist/types/standardActions/arrayActions.d.ts +1 -1
  23. package/dist/types/treeUtils/sandbox.d.ts +0 -4
  24. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  25. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  26. package/dist/types/types/TypeCheckError.d.ts +1 -0
  27. package/dist/types/types/utility/typesRefinement.d.ts +2 -2
  28. package/dist/types/utils/ModelPool.d.ts +7 -1
  29. package/dist/types/utils/chunks.d.ts +1 -0
  30. package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
  31. package/dist/types/utils/index.d.ts +13 -0
  32. package/dist/types/utils/mapUtils.d.ts +2 -2
  33. package/dist/types/utils/setLike.d.ts +1 -0
  34. package/package.json +2 -2
  35. package/src/action/applyAction.ts +1 -1
  36. package/src/action/middleware.ts +17 -36
  37. package/src/action/modelFlow.ts +4 -5
  38. package/src/action/mutationBatch.ts +10 -10
  39. package/src/action/pendingActions.ts +3 -4
  40. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
  41. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
  42. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
  43. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
  44. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
  45. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
  46. package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
  47. package/src/actionMiddlewares/transactionMiddleware.ts +55 -25
  48. package/src/actionMiddlewares/undoMiddleware.ts +112 -59
  49. package/src/actionMiddlewares/utils.ts +2 -1
  50. package/src/computedTree/computedTree.ts +14 -3
  51. package/src/context/context.ts +17 -45
  52. package/src/dataModel/BaseDataModel.ts +20 -16
  53. package/src/dataModel/dataModelInstanceCache.ts +8 -0
  54. package/src/deepChange/onDeepChange.ts +179 -24
  55. package/src/frozen/Frozen.ts +50 -33
  56. package/src/model/BaseModel.ts +10 -13
  57. package/src/model/newModel.ts +21 -24
  58. package/src/model/utils.ts +2 -2
  59. package/src/modelShared/modelClassInitializer.ts +3 -2
  60. package/src/modelShared/modelDecorator.ts +39 -39
  61. package/src/modelShared/modelInfo.ts +1 -1
  62. package/src/modelShared/prop.ts +6 -9
  63. package/src/modelShared/sharedInternalModel.ts +16 -3
  64. package/src/parent/arrayPaths.ts +242 -0
  65. package/src/parent/coreObjectChildren.ts +300 -48
  66. package/src/parent/detach.ts +1 -1
  67. package/src/parent/findChildren.ts +4 -7
  68. package/src/parent/onChildAttachedTo.ts +157 -55
  69. package/src/parent/path.ts +15 -5
  70. package/src/parent/setParent.ts +34 -15
  71. package/src/parent/treeOrder.ts +53 -0
  72. package/src/parent/walkTree.ts +12 -101
  73. package/src/patch/applyPatches.ts +74 -34
  74. package/src/patch/emitPatch.ts +142 -26
  75. package/src/patch/jsonPatch.ts +5 -0
  76. package/src/patch/patchRecorder.ts +18 -14
  77. package/src/redux/connectReduxDevTools.ts +24 -18
  78. package/src/redux/redux.ts +5 -21
  79. package/src/ref/core.ts +81 -29
  80. package/src/ref/modelRefIdIndex.ts +131 -0
  81. package/src/ref/nodesById.ts +60 -0
  82. package/src/ref/refIdIndex.ts +209 -0
  83. package/src/rootStore/attachDetach.ts +27 -16
  84. package/src/rootStore/rootStore.ts +10 -6
  85. package/src/snapshot/SnapshotOf.ts +2 -2
  86. package/src/snapshot/applySnapshot.ts +27 -19
  87. package/src/snapshot/clone.ts +1 -2
  88. package/src/snapshot/cloneTreeValue.ts +1 -2
  89. package/src/snapshot/fromModelSnapshot.ts +6 -2
  90. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  91. package/src/snapshot/fromSnapshot.ts +2 -6
  92. package/src/snapshot/internal.ts +89 -19
  93. package/src/snapshot/onSnapshot.ts +12 -1
  94. package/src/snapshot/reconcileModelSnapshot.ts +6 -6
  95. package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
  96. package/src/standardActions/arrayActions.ts +12 -2
  97. package/src/standardActions/objectActions.ts +3 -2
  98. package/src/transforms/ImmutableDate.ts +4 -0
  99. package/src/transforms/asMap.ts +3 -1
  100. package/src/treeUtils/deepEquals.ts +15 -13
  101. package/src/treeUtils/draft.ts +8 -0
  102. package/src/treeUtils/sandbox.ts +31 -27
  103. package/src/tweaker/changeRollback.ts +218 -0
  104. package/src/tweaker/core.ts +33 -22
  105. package/src/tweaker/treeNodeMetadata.ts +12 -2
  106. package/src/tweaker/tweak.ts +183 -19
  107. package/src/tweaker/tweakArray.ts +176 -33
  108. package/src/tweaker/tweakPlainObject.ts +117 -39
  109. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  110. package/src/tweaker/typeChecking.ts +25 -15
  111. package/src/types/TypeCheckError.ts +17 -19
  112. package/src/types/arrayBased/typesArray.ts +10 -11
  113. package/src/types/createCachedTypeCheck.ts +541 -88
  114. package/src/types/objectBased/typesDataModelData.ts +4 -2
  115. package/src/types/objectBased/typesModel.ts +4 -2
  116. package/src/types/objectBased/typesObject.ts +20 -16
  117. package/src/types/objectBased/typesRecord.ts +8 -18
  118. package/src/types/objectBased/typesRef.ts +2 -2
  119. package/src/types/primitiveBased/typesEnum.ts +4 -2
  120. package/src/types/primitiveBased/typesPrimitive.ts +9 -4
  121. package/src/types/typeCheckErrorUtils.ts +1 -0
  122. package/src/types/utility/typesCodecSupport.ts +97 -56
  123. package/src/types/utility/typesMap.ts +24 -41
  124. package/src/types/utility/typesRefinement.ts +3 -2
  125. package/src/types/utility/typesSet.ts +24 -0
  126. package/src/utils/ModelPool.ts +78 -22
  127. package/src/utils/chainFns.ts +4 -0
  128. package/src/utils/chunks.ts +271 -0
  129. package/src/utils/decorators.ts +1 -1
  130. package/src/utils/errorDiagnostics.ts +6 -2
  131. package/src/utils/forEachWithDelayedThrow.ts +83 -0
  132. package/src/utils/index.ts +71 -2
  133. package/src/utils/mapUtils.ts +3 -7
  134. package/src/utils/setIfDifferent.ts +3 -2
  135. package/src/utils/setLike.ts +134 -0
  136. package/src/wrappers/ArraySet.ts +5 -9
  137. package/src/wrappers/ObjectMap.ts +15 -11
  138. package/src/wrappers/asMap.ts +30 -18
  139. package/src/wrappers/asSet.ts +36 -12
  140. package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
  141. package/src/action/modelFlowPromiseGenerator.ts +0 -127
@@ -10,7 +10,7 @@ import type {
10
10
  import { modelInfoByClass } from "../../modelShared/modelInfo"
11
11
  import { getInternalModelClassPropsInfo } from "../../modelShared/modelPropsInfo"
12
12
  import { noDefaultValue } from "../../modelShared/prop"
13
- import { failure, lazy } from "../../utils"
13
+ import { failure, lazy, setProtoProp } from "../../utils"
14
14
  import { getTypeInfo } from "../getTypeInfo"
15
15
  import {
16
16
  registerStandardTypeResolver,
@@ -168,12 +168,14 @@ export class DataModelDataTypeInfo extends TypeInfo {
168
168
  hasDefault = true
169
169
  }
170
170
 
171
- propTypes[propName] = {
171
+ const propInfo = {
172
172
  type,
173
173
  typeInfo,
174
174
  hasDefault,
175
175
  default: defaultValue,
176
176
  }
177
+ if (propName === "__proto__") setProtoProp(propTypes, propInfo)
178
+ else propTypes[propName] = propInfo
177
179
  })
178
180
  return propTypes
179
181
  })
@@ -7,7 +7,7 @@ import type { ModelClass } from "../../modelShared/BaseModelShared"
7
7
  import { modelInfoByClass } from "../../modelShared/modelInfo"
8
8
  import { getInternalModelClassPropsInfo } from "../../modelShared/modelPropsInfo"
9
9
  import { noDefaultValue } from "../../modelShared/prop"
10
- import { isObject, lazy } from "../../utils"
10
+ import { isObject, lazy, setProtoProp } from "../../utils"
11
11
  import { getTypeInfo } from "../getTypeInfo"
12
12
  import { registerStandardTypeResolver, resolveTypeChecker } from "../resolveTypeChecker"
13
13
  import type { AnyStandardType, ModelType } from "../schemas"
@@ -178,12 +178,14 @@ export class ModelTypeInfo extends TypeInfo {
178
178
  hasDefault = true
179
179
  }
180
180
 
181
- propTypes[propName] = {
181
+ const propInfo = {
182
182
  type,
183
183
  typeInfo,
184
184
  hasDefault,
185
185
  default: defaultValue,
186
186
  }
187
+ if (propName === "__proto__") setProtoProp(propTypes, propInfo)
188
+ else propTypes[propName] = propInfo
187
189
  })
188
190
  return propTypes
189
191
  })
@@ -2,7 +2,16 @@ import { isObservableObject, keys } from "mobx"
2
2
  import type { O } from "ts-toolbelt"
3
3
  import { Frozen } from "../../frozen/Frozen"
4
4
  import type { Path } from "../../parent/pathTypes"
5
- import { assertIsFunction, assertIsObject, getMobxVersion, isObject, lazy } from "../../utils"
5
+ import {
6
+ assertIsFunction,
7
+ assertIsObject,
8
+ copyOwnEnumerableProps,
9
+ getMobxVersion,
10
+ isArray,
11
+ isObject,
12
+ lazy,
13
+ setProtoProp,
14
+ } from "../../utils"
6
15
  import { withErrorPathSegment } from "../../utils/errorDiagnostics"
7
16
  import { createIndexedPerEntryCachedCheck } from "../createCachedTypeCheck"
8
17
  import { getTypeInfo } from "../getTypeInfo"
@@ -106,27 +115,20 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
106
115
  obj: Record<string, unknown>,
107
116
  processors: ReadonlyArray<SnapshotProcessor | undefined>
108
117
  ) => {
109
- const newObj: typeof obj = {}
110
-
111
118
  // note: we allow excess properties when checking objects
112
- const keys = Object.keys(obj)
113
- for (let i = 0; i < keys.length; i++) {
114
- const k = keys[i]
119
+ return copyOwnEnumerableProps({}, obj, (value, k) => {
115
120
  const schemaEntry = schemaEntryByPropName[k]
116
121
  if (schemaEntry) {
117
122
  const processor = processors[schemaEntry.processorIndex]
118
- newObj[k] = processor ? withErrorPathSegment(k, () => processor(obj[k])) : obj[k]
119
- } else {
120
- // unknown prop, copy as is
121
- newObj[k] = obj[k]
123
+ return processor ? withErrorPathSegment(k, () => processor(value)) : value
122
124
  }
123
- }
124
-
125
- return newObj
125
+ // unknown prop, copy as is
126
+ return value
127
+ })
126
128
  }
127
129
 
128
130
  const checkObjectType = (obj: any, path: Path, typeCheckedValue: any) => {
129
- if (!isObject(obj) || (frozen && !(obj instanceof Frozen))) {
131
+ if (!isObject(obj) || isArray(obj) || (frozen && !(obj instanceof Frozen))) {
130
132
  return new TypeCheckError({
131
133
  path,
132
134
  expectedTypeName: getTypeName(thisTc),
@@ -187,7 +189,7 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
187
189
  typeInfoGen,
188
190
 
189
191
  (obj) => {
190
- if (!isObject(obj)) {
192
+ if (!isObject(obj) || isArray(obj)) {
191
193
  return null
192
194
  }
193
195
 
@@ -278,7 +280,9 @@ export class ObjectTypeInfo extends TypeInfo {
278
280
  const propTypes: O.Writable<ObjectTypeInfoProps> = {}
279
281
  Object.keys(objSchema).forEach((propName) => {
280
282
  const type = resolveStandardType(objSchema[propName])
281
- propTypes[propName] = { type, typeInfo: getTypeInfo(type) }
283
+ const propInfo = { type, typeInfo: getTypeInfo(type) }
284
+ if (propName === "__proto__") setProtoProp(propTypes, propInfo)
285
+ else propTypes[propName] = propInfo
282
286
  })
283
287
  return propTypes
284
288
  })
@@ -1,8 +1,8 @@
1
1
  import { isObservableObject, keys } from "mobx"
2
2
  import type { Path } from "../../parent/pathTypes"
3
- import { failure, isObject } from "../../utils"
3
+ import { copyOwnEnumerableProps, failure, isArray, isObject } from "../../utils"
4
4
  import { withErrorPathSegment } from "../../utils/errorDiagnostics"
5
- import { createAdaptiveRecordCachedCheck } from "../createCachedTypeCheck"
5
+ import { createChunkedRecordCachedCheck } from "../createCachedTypeCheck"
6
6
  import { getTypeInfo } from "../getTypeInfo"
7
7
  import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
8
8
  import type { AnyStandardType, AnyType, RecordType } from "../schemas"
@@ -48,16 +48,9 @@ export function typesRecord<T extends AnyType>(valueType: T): RecordType<T> {
48
48
  return obj
49
49
  }
50
50
 
51
- const newObj: typeof obj = {}
52
-
53
- const keys = Object.keys(obj)
54
- for (let i = 0; i < keys.length; i++) {
55
- const k = keys[i]
56
- const v = withErrorPathSegment(k, () => processor(obj[k]))
57
- newObj[k] = v
58
- }
59
-
60
- return newObj
51
+ return copyOwnEnumerableProps({}, obj, (value, key) =>
52
+ withErrorPathSegment(key, () => processor(value))
53
+ )
61
54
  }
62
55
 
63
56
  const iterateRecordEntries = (
@@ -88,16 +81,13 @@ export function typesRecord<T extends AnyType>(valueType: T): RecordType<T> {
88
81
  return error ? prependPathElementToTypeCheckError(error, path, key, typeCheckedValue) : null
89
82
  }
90
83
 
91
- const checkRecordValues = createAdaptiveRecordCachedCheck(
92
- iterateRecordEntries,
93
- checkRecordEntry
94
- )
84
+ const checkRecordValues = createChunkedRecordCachedCheck(iterateRecordEntries, checkRecordEntry)
95
85
 
96
86
  const thisTc: TypeChecker = new TypeChecker(
97
87
  TypeCheckerBaseType.Object,
98
88
 
99
89
  (obj, path, typeCheckedValue) => {
100
- if (!isObject(obj)) {
90
+ if (!isObject(obj) || isArray(obj)) {
101
91
  return new TypeCheckError({
102
92
  path,
103
93
  expectedTypeName: getTypeName(thisTc),
@@ -117,7 +107,7 @@ export function typesRecord<T extends AnyType>(valueType: T): RecordType<T> {
117
107
  typeInfoGen,
118
108
 
119
109
  (obj) => {
120
- if (!isObject(obj)) {
110
+ if (!isObject(obj) || isArray(obj)) {
121
111
  return null
122
112
  }
123
113
 
@@ -1,6 +1,6 @@
1
1
  import { modelTypeKey } from "../../model/metadata"
2
2
  import { modelInfoByClass } from "../../modelShared/modelInfo"
3
- import { Ref, type RefConstructor } from "../../ref/Ref"
3
+ import type { Ref, RefConstructor } from "../../ref/Ref"
4
4
  import { isObject } from "../../utils"
5
5
  import { typesString } from "../primitiveBased/typesPrimitive"
6
6
  import { resolveTypeChecker } from "../resolveTypeChecker"
@@ -36,7 +36,7 @@ export function typesRef<O extends object>(refConstructor: RefConstructor<O>): M
36
36
  TypeCheckerBaseType.Object,
37
37
 
38
38
  (value, path, typeCheckedValue) => {
39
- if (!(value instanceof Ref)) {
39
+ if (!(value instanceof refConstructor.refClass)) {
40
40
  return new TypeCheckError({
41
41
  path,
42
42
  expectedTypeName: typeName,
@@ -1,4 +1,4 @@
1
- import { assertIsObject } from "../../utils"
1
+ import { assertIsObject, isEqualOrBothNaN } from "../../utils"
2
2
  import type { IdentityType } from "../schemas"
3
3
  import { typesOr } from "../utility/typesOr"
4
4
  import { typesLiteral } from "./typesPrimitive"
@@ -17,6 +17,7 @@ export interface EnumLike {
17
17
  */
18
18
  export function enumValues(e: EnumLike): (string | number)[] {
19
19
  const vals: (string | number)[] = []
20
+ const seen = new Set<string | number>()
20
21
  for (const k of Object.keys(e)) {
21
22
  const v = e[k]
22
23
  // we have to do this since TS does something weird
@@ -24,7 +25,8 @@ export function enumValues(e: EnumLike): (string | number)[] {
24
25
  // Hi = 0 -> { Hi: 0, 0: "Hi" }
25
26
  // and SWC currently generates enum code inconsistent with TS/Babel
26
27
  // https://github.com/swc-project/swc/issues/3711
27
- if (!vals.includes(v) && ((typeof v !== "string" && v !== +k) || e[v] !== +k)) {
28
+ if (!seen.has(v) && ((typeof v !== "string" && v !== +k) || !isEqualOrBothNaN(e[v], +k))) {
29
+ seen.add(v)
28
30
  vals.push(v)
29
31
  }
30
32
  }
@@ -1,4 +1,4 @@
1
- import { assertIsPrimitive } from "../../utils"
1
+ import { assertIsPrimitive, isEqualOrBothNaN } from "../../utils"
2
2
  import type { PrimitiveValue } from "../../utils/types"
3
3
  import { registerStandardTypeResolver, type StandardTypeResolverFn } from "../resolveTypeChecker"
4
4
  import type { AnyStandardType, IdentityType } from "../schemas"
@@ -31,7 +31,12 @@ export function typesLiteral<T extends PrimitiveValue>(literal: T): IdentityType
31
31
  typeName = "null"
32
32
  break
33
33
  default:
34
- typeName = JSON.stringify(literal)
34
+ typeName =
35
+ typeof literal === "bigint"
36
+ ? `${literal}n`
37
+ : typeof literal === "number" && !Number.isFinite(literal)
38
+ ? String(literal)
39
+ : JSON.stringify(literal)
35
40
  break
36
41
  }
37
42
 
@@ -41,7 +46,7 @@ export function typesLiteral<T extends PrimitiveValue>(literal: T): IdentityType
41
46
  TypeCheckerBaseType.Primitive,
42
47
 
43
48
  (value, path, typeCheckedValue) =>
44
- value === literal
49
+ isEqualOrBothNaN(value, literal)
45
50
  ? null
46
51
  : new TypeCheckError({
47
52
  path,
@@ -53,7 +58,7 @@ export function typesLiteral<T extends PrimitiveValue>(literal: T): IdentityType
53
58
  () => typeName,
54
59
  typeInfoGen,
55
60
 
56
- (value) => (value === literal ? thisTc : null),
61
+ (value) => (isEqualOrBothNaN(value, literal) ? thisTc : null),
57
62
  undefined,
58
63
  undefined
59
64
  )
@@ -16,6 +16,7 @@ export function prependPathElementToTypeCheckError(
16
16
 
17
17
  return new TypeCheckError({
18
18
  path: fullPath,
19
+ modelTrail: valueError.modelTrail,
19
20
  expectedTypeName: valueError.expectedTypeName,
20
21
  actualValue: valueError.actualValue,
21
22
  typeCheckedValue,
@@ -1,6 +1,6 @@
1
1
  import { isObservableArray, isObservableObject, remove } from "mobx"
2
2
  import type { ModelPropTransform } from "../../modelShared/prop"
3
- import { isArray, isObject, lazy } from "../../utils"
3
+ import { copyOwnEnumerableProps, isArray, isObject, lazy } from "../../utils"
4
4
  import { setIfDifferent } from "../../utils/setIfDifferent"
5
5
  import { ArrayTypeInfo, typesArray } from "../arrayBased/typesArray"
6
6
  import { TupleTypeInfo, typesTuple } from "../arrayBased/typesTuple"
@@ -19,23 +19,26 @@ import {
19
19
  type RuntimeAdapter,
20
20
  } from "./typesCodecCore"
21
21
  import { OrTypeInfo, typesOr } from "./typesOr"
22
- import { RefinementTypeInfo } from "./typesRefinement"
22
+ import { RefinementTypeInfo, typesRefinement } from "./typesRefinement"
23
23
  import { SkipCheckTypeInfo, typesSkipCheck } from "./typesSkipCheck"
24
24
  import { TagTypeInfo, typesTag } from "./typesTag"
25
25
 
26
26
  const codecSupportCache = new WeakMap<AnyStandardType, ResolvedCodecSupport>()
27
27
 
28
28
  function getArrayIndex(prop: PropertyKey): number | undefined {
29
- // Proxy get/set traps always receive string|symbol keys (ECMAScript ToPropertyKey),
30
- // so we only need to check for string numeric indices.
31
- if (typeof prop === "string" && /^(0|[1-9]\d*)$/.test(prop)) {
32
- return Number(prop)
33
- }
34
-
35
- return undefined
29
+ // Proxy traps receive string or symbol keys, including strings for numeric indices.
30
+ if (typeof prop !== "string") return undefined
31
+ const first = prop.charCodeAt(0)
32
+ if (first < 48 || first > 57) return undefined
33
+
34
+ const index = Number(prop)
35
+ return Number.isInteger(index) && index >= 0 && index < 0xffffffff && String(index) === prop
36
+ ? index
37
+ : undefined
36
38
  }
37
39
 
38
40
  function normalizeArrayIndex(index: number, length: number): number {
41
+ index = Math.trunc(index) || 0
39
42
  if (index < 0) {
40
43
  return Math.max(length + index, 0)
41
44
  }
@@ -44,6 +47,7 @@ function normalizeArrayIndex(index: number, length: number): number {
44
47
  }
45
48
 
46
49
  function resolveArrayAtIndex(index: number, length: number): number | undefined {
50
+ index = Math.trunc(index) || 0
47
51
  const resolvedIndex = index < 0 ? length + index : index
48
52
 
49
53
  if (resolvedIndex < 0 || resolvedIndex >= length) {
@@ -90,11 +94,7 @@ function objectLikeToStored(
90
94
  return runtime as never
91
95
  }
92
96
 
93
- const stored: Record<string, unknown> = {}
94
- for (const key of Object.keys(runtime)) {
95
- stored[key] = convertValue(key, runtime[key])
96
- }
97
- return stored
97
+ return copyOwnEnumerableProps({}, runtime, (value, key) => convertValue(key, value))
98
98
  }
99
99
 
100
100
  /**
@@ -155,8 +155,7 @@ function createObjectRuntimeAdapter(
155
155
  }
156
156
  }
157
157
 
158
- Reflect.set(target, prop, value)
159
- return true
158
+ return Reflect.set(target, prop, value)
160
159
  },
161
160
 
162
161
  ...objectRecordProxyTraps,
@@ -217,8 +216,7 @@ function createRecordRuntimeAdapter(
217
216
  return true
218
217
  }
219
218
 
220
- Reflect.set(target, prop, value)
221
- return true
219
+ return Reflect.set(target, prop, value)
222
220
  },
223
221
 
224
222
  ...objectRecordProxyTraps,
@@ -250,7 +248,9 @@ function createArrayLikeRuntimeAdapter(
250
248
  const storedByRuntime = new WeakMap<unknown[], unknown[]>()
251
249
 
252
250
  const decodeIndex = (target: unknown[], index: number) =>
253
- index < 0 || index >= target.length || !Reflect.has(target, index)
251
+ index < 0 ||
252
+ index >= target.length ||
253
+ (!isObservableArray(target) && !Reflect.has(target, index))
254
254
  ? undefined
255
255
  : getItemAdapter(index).toRuntime(target[index], (newStoredValue) => {
256
256
  target[index] = newStoredValue
@@ -265,13 +265,18 @@ function createArrayLikeRuntimeAdapter(
265
265
  return Reflect.apply(method, receiver, args)
266
266
  }
267
267
  const materializeRuntimeValues = (target: unknown[]) =>
268
- Array.from({ length: target.length }, (_, index) => decodeIndex(target, index))
268
+ target.map((_value, index) => decodeIndex(target, index))
269
269
  const replaceStoredValues = (target: unknown[], runtimeValues: unknown[]) => {
270
- target.splice(
271
- 0,
272
- target.length,
273
- ...runtimeValues.map((value, index) => getItemAdapter(index).toStored(value))
274
- )
270
+ const storedValues = runtimeValues.map((value, index) => getItemAdapter(index).toStored(value))
271
+ if (isObservableArray(target)) {
272
+ target.replace(storedValues)
273
+ } else {
274
+ target.length = storedValues.length
275
+ for (let i = 0; i < storedValues.length; i++) {
276
+ if (i in storedValues) target[i] = storedValues[i]
277
+ else delete target[i]
278
+ }
279
+ }
275
280
  }
276
281
  const mutateViaRuntime = (
277
282
  target: unknown[],
@@ -296,6 +301,11 @@ function createArrayLikeRuntimeAdapter(
296
301
  }
297
302
 
298
303
  const proxy: unknown[] = new Proxy(stored, {
304
+ has(target, prop) {
305
+ const index = getArrayIndex(prop)
306
+ if (index !== undefined && isObservableArray(target)) return index < target.length
307
+ return Reflect.has(target, prop)
308
+ },
299
309
  get(target, prop, receiver) {
300
310
  const index = getArrayIndex(prop)
301
311
  if (index !== undefined) {
@@ -332,8 +342,9 @@ function createArrayLikeRuntimeAdapter(
332
342
  }
333
343
 
334
344
  const indexValue = target.length - 1
345
+ const present = isObservableArray(target) || indexValue in target
335
346
  const value = target.pop()
336
- return getItemAdapter(indexValue).toRuntime(value)
347
+ return present ? getItemAdapter(indexValue).toRuntime(value) : undefined
337
348
  }
338
349
  case "shift":
339
350
  return () => {
@@ -341,8 +352,9 @@ function createArrayLikeRuntimeAdapter(
341
352
  return undefined
342
353
  }
343
354
 
355
+ const present = isObservableArray(target) || 0 in target
344
356
  const value = target.shift()
345
- return getItemAdapter(0).toRuntime(value)
357
+ return present ? getItemAdapter(0).toRuntime(value) : undefined
346
358
  }
347
359
  case "unshift":
348
360
  return (...items: unknown[]) =>
@@ -350,12 +362,21 @@ function createArrayLikeRuntimeAdapter(
350
362
  ...items.map((item, indexValue) => getItemAdapter(indexValue).toStored(item))
351
363
  )
352
364
  case "splice":
353
- return (start: number, deleteCount?: number, ...items: unknown[]) => {
365
+ return (...args: [start?: number, deleteCount?: number, ...items: unknown[]]) => {
366
+ const [start = 0, deleteCount, ...items] = args
354
367
  const normalizedStart = normalizeArrayIndex(start, target.length)
355
368
  const actualDeleteCount =
356
- deleteCount === undefined
357
- ? target.length - normalizedStart
358
- : Math.max(0, Math.min(deleteCount, target.length - normalizedStart))
369
+ args.length === 0
370
+ ? 0
371
+ : args.length === 1
372
+ ? target.length - normalizedStart
373
+ : Math.max(
374
+ 0,
375
+ Math.min(
376
+ Math.trunc(deleteCount ?? 0) || 0,
377
+ target.length - normalizedStart
378
+ )
379
+ )
359
380
 
360
381
  const removedValues = target
361
382
  .slice(normalizedStart, normalizedStart + actualDeleteCount)
@@ -443,8 +464,7 @@ function createArrayLikeRuntimeAdapter(
443
464
  return true
444
465
  }
445
466
 
446
- Reflect.set(target, prop, value)
447
- return true
467
+ return Reflect.set(target, prop, value)
448
468
  },
449
469
  })
450
470
 
@@ -461,7 +481,7 @@ function createArrayLikeRuntimeAdapter(
461
481
  return cachedStored
462
482
  }
463
483
 
464
- return Array.from(runtime, (value, index) => getItemAdapter(index).toStored(value))
484
+ return runtime.map((value, index) => getItemAdapter(index).toStored(value))
465
485
  }
466
486
 
467
487
  return runtime as never
@@ -490,44 +510,55 @@ function createCodecLeafRuntimeAdapter(
490
510
  toRuntime(stored, setStored) {
491
511
  const cachedRuntime = isObject(stored) ? runtimeByStored.get(stored) : undefined
492
512
 
493
- const runtime = transform.transform({
513
+ // Declared up front (rather than initialized from the call) so that a setter invoked
514
+ // while decoding can read it without hitting the temporal dead zone.
515
+ let runtime: unknown
516
+ let storedChanged = false
517
+ runtime = transform.transform({
494
518
  originalValue: stored,
495
519
  cachedTransformedValue: cachedRuntime,
496
520
  setOriginalValue(newStoredValue) {
497
521
  if (setStored) {
498
- setStored(newStoredValue)
522
+ try {
523
+ setStored(newStoredValue)
524
+ } finally {
525
+ // A retained codec setter can replace the value after decoding. Neither
526
+ // direction of the cache may keep associating it with the old stored value.
527
+ storedChanged = true
528
+ if (isObject(runtime)) storedByRuntime.delete(runtime)
529
+ if (isObject(stored)) runtimeByStored.delete(stored)
530
+ }
499
531
  }
500
532
  },
501
533
  })
502
534
 
503
- if (isObject(runtime)) {
504
- storedByRuntime.set(runtime, stored)
505
- }
506
-
507
- if (isObject(stored)) {
508
- runtimeByStored.set(stored, runtime)
535
+ if (!storedChanged) {
536
+ if (isObject(runtime)) storedByRuntime.set(runtime, stored)
537
+ if (isObject(stored)) runtimeByStored.set(stored, runtime)
509
538
  }
510
539
 
511
540
  return runtime
512
541
  },
513
542
 
514
543
  toStored(runtime) {
515
- const cachedStored = isObject(runtime) ? storedByRuntime.get(runtime) : undefined
516
- if (cachedStored !== undefined) {
517
- return cachedStored
544
+ if (isObject(runtime)) {
545
+ const cachedStored = storedByRuntime.get(runtime)
546
+ if (cachedStored !== undefined || storedByRuntime.has(runtime)) {
547
+ return cachedStored
548
+ }
518
549
  }
519
550
 
551
+ let cacheResult = false
520
552
  const stored = transform.untransform({
521
553
  transformedValue: runtime,
522
- cacheTransformedValue: () => {},
554
+ cacheTransformedValue: () => {
555
+ cacheResult = true
556
+ },
523
557
  })
524
558
 
525
- if (isObject(runtime)) {
526
- storedByRuntime.set(runtime, stored)
527
- }
528
-
529
- if (isObject(stored)) {
530
- runtimeByStored.set(stored, runtime)
559
+ if (cacheResult) {
560
+ if (isObject(runtime)) storedByRuntime.set(runtime, stored)
561
+ if (isObject(stored)) runtimeByStored.set(stored, runtime)
531
562
  }
532
563
 
533
564
  return stored
@@ -536,7 +567,7 @@ function createCodecLeafRuntimeAdapter(
536
567
  toStoredIfCached(runtime) {
537
568
  if (isObject(runtime)) {
538
569
  const cachedStored = storedByRuntime.get(runtime)
539
- if (cachedStored !== undefined) {
570
+ if (cachedStored !== undefined || storedByRuntime.has(runtime)) {
540
571
  return { found: true, value: cachedStored }
541
572
  }
542
573
  }
@@ -675,7 +706,7 @@ function resolveCodecSupportForStandardType(type: AnyStandardType): ResolvedCode
675
706
  }
676
707
  } else if (typeInfo instanceof ObjectTypeInfo) {
677
708
  const childSupports = lazy(() => {
678
- const supports: Record<string, ResolvedCodecSupport> = {}
709
+ const supports: Record<string, ResolvedCodecSupport> = Object.create(null)
679
710
  const props = typeInfo.props
680
711
  for (const propName of Object.keys(props)) {
681
712
  supports[propName] = resolveCodecSupportForStandardType(props[propName].type)
@@ -690,7 +721,7 @@ function resolveCodecSupportForStandardType(type: AnyStandardType): ResolvedCode
690
721
  : {
691
722
  hasCodec: true,
692
723
  storedType: typesObject(() => {
693
- const storedProps: Record<string, AnyStandardType> = {}
724
+ const storedProps: Record<string, AnyStandardType> = Object.create(null)
694
725
  const props = typeInfo.props
695
726
  for (const propName of Object.keys(props)) {
696
727
  storedProps[propName] = childSupports()[propName].storedType
@@ -722,7 +753,17 @@ function resolveCodecSupportForStandardType(type: AnyStandardType): ResolvedCode
722
753
  } else if (typeInfo instanceof RefinementTypeInfo) {
723
754
  const baseSupport = resolveCodecSupportForStandardType(typeInfo.baseType)
724
755
 
725
- resolvedSupport = baseSupport.hasCodec ? baseSupport : noCodecSupport(type)
756
+ resolvedSupport = !baseSupport.hasCodec
757
+ ? noCodecSupport(type)
758
+ : {
759
+ hasCodec: true,
760
+ storedType: typesRefinement(
761
+ baseSupport.storedType,
762
+ (stored) => typeInfo.checkFunction(baseSupport.adapter.toRuntime(stored)),
763
+ typeInfo.typeName
764
+ ),
765
+ adapter: baseSupport.adapter,
766
+ }
726
767
  } else if (typeInfo instanceof SkipCheckTypeInfo) {
727
768
  const baseSupport = resolveCodecSupportForStandardType(typeInfo.baseType)
728
769