mobx-keystone 2.4.0 → 2.4.1

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.
@@ -28,9 +28,10 @@ export interface Serializer {
28
28
  *
29
29
  * @param root Root of the tree.
30
30
  * @param json Value to decode.
31
+ * @param options Options.
31
32
  * @returns The decoded value.
32
33
  */
33
- decodeValue(root: object, json: JsonValue): unknown;
34
+ decodeValue(root: object, json: JsonValue, options?: DecodeOptions): unknown;
34
35
  /**
35
36
  * Encodes a call (an `ActionCall`, e.g. from `onActionMiddleware`'s `onStart`, or a `Call`)
36
37
  * as JSON. The target always goes as a reference; for a standalone action, it isn't repeated
@@ -50,9 +51,22 @@ export interface Serializer {
50
51
  *
51
52
  * @param root Root of the tree.
52
53
  * @param json The encoded call.
54
+ * @param options Options, applied when the arguments are decoded.
53
55
  * @returns The resolved call, to pass to `applyCall` or `applyCallAsync`.
54
56
  */
55
- decodeCall(root: object, json: EncodedCall | JsonValue): ResolvedCall;
57
+ decodeCall(root: object, json: EncodedCall | JsonValue, options?: DecodeOptions): ResolvedCall;
58
+ }
59
+ /**
60
+ * Options of `Serializer.decodeValue` and `Serializer.decodeCall`.
61
+ */
62
+ export interface DecodeOptions {
63
+ /**
64
+ * Pass `true` to give new model ids to the models decoded from snapshots (nested ones
65
+ * included), as `fromSnapshot(snapshot, { generateNewIds: true })` does, e.g. on a server that
66
+ * accepts snapshots from clients. References still resolve to the nodes they point to.
67
+ * Defaults to `false` (the ids in the snapshots are kept).
68
+ */
69
+ generateNewIds?: boolean;
56
70
  }
57
71
  /**
58
72
  * Options of `createSerializer`.
@@ -63,8 +77,8 @@ export interface SerializerOptions {
63
77
  */
64
78
  serializers?: readonly ValueSerializer[];
65
79
  /**
66
- * Whether to append the built-in serializers (`builtinSerializers`) to the list.
67
- * Defaults to `true`.
80
+ * Whether to append the built-in serializers (`builtinSerializers`) to the list, except those
81
+ * whose tag a serializer in `serializers` takes, which it replaces. Defaults to `true`.
68
82
  */
69
83
  builtins?: boolean;
70
84
  }
@@ -30,6 +30,11 @@ export interface DecodeContext {
30
30
  * Root of the tree the value is decoded against.
31
31
  */
32
32
  readonly root: object;
33
+ /**
34
+ * Whether models decoded from snapshots must get new model ids (the `generateNewIds` option
35
+ * of the decoding). A serializer that creates models honors it; references ignore it.
36
+ */
37
+ readonly generateNewIds: boolean;
33
38
  /**
34
39
  * Decodes a nested value with the same serializer.
35
40
  *
@@ -3,14 +3,18 @@ import type { JsonValue } from "./JsonValue";
3
3
  import type { ValueSerializer } from "./ValueSerializer";
4
4
  /**
5
5
  * The built-in value serializers, appended in this order to every serializer's list (unless it is
6
- * created with `builtins: false`). Pick some of them to build a list by hand, e.g.
6
+ * created with `builtins: false`), except those whose tag a serializer of the list takes. Pick some of them to build a list by hand, e.g.
7
7
  * `createSerializer({ serializers: [builtinSerializers.date], builtins: false })`.
8
8
  */
9
9
  export declare const builtinSerializers: Readonly<{
10
10
  /**
11
11
  * Models and frozen data that aren't references, as their snapshots (unmarked, since snapshots
12
12
  * mark themselves with `$modelType` / `$frozen`), decoded with `fromSnapshot` into new
13
- * instances. Tree arrays and plain objects that aren't references are walked like any other.
13
+ * instances (with new model ids when decoded with `generateNewIds`). Tree arrays and plain
14
+ * objects that aren't references are walked like any other.
15
+ *
16
+ * A custom serializer tagged `snapshot` replaces it: its encoded value must be a snapshot too,
17
+ * and it decodes every value with `$modelType` / `$frozen`.
14
18
  */
15
19
  snapshot: ValueSerializer<object, any>;
16
20
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobx-keystone",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more",
5
5
  "keywords": [
6
6
  "mobx",
@@ -56,9 +56,10 @@ export interface Serializer {
56
56
  *
57
57
  * @param root Root of the tree.
58
58
  * @param json Value to decode.
59
+ * @param options Options.
59
60
  * @returns The decoded value.
60
61
  */
61
- decodeValue(root: object, json: JsonValue): unknown
62
+ decodeValue(root: object, json: JsonValue, options?: DecodeOptions): unknown
62
63
 
63
64
  /**
64
65
  * Encodes a call (an `ActionCall`, e.g. from `onActionMiddleware`'s `onStart`, or a `Call`)
@@ -80,9 +81,23 @@ export interface Serializer {
80
81
  *
81
82
  * @param root Root of the tree.
82
83
  * @param json The encoded call.
84
+ * @param options Options, applied when the arguments are decoded.
83
85
  * @returns The resolved call, to pass to `applyCall` or `applyCallAsync`.
84
86
  */
85
- decodeCall(root: object, json: EncodedCall | JsonValue): ResolvedCall
87
+ decodeCall(root: object, json: EncodedCall | JsonValue, options?: DecodeOptions): ResolvedCall
88
+ }
89
+
90
+ /**
91
+ * Options of `Serializer.decodeValue` and `Serializer.decodeCall`.
92
+ */
93
+ export interface DecodeOptions {
94
+ /**
95
+ * Pass `true` to give new model ids to the models decoded from snapshots (nested ones
96
+ * included), as `fromSnapshot(snapshot, { generateNewIds: true })` does, e.g. on a server that
97
+ * accepts snapshots from clients. References still resolve to the nodes they point to.
98
+ * Defaults to `false` (the ids in the snapshots are kept).
99
+ */
100
+ generateNewIds?: boolean
86
101
  }
87
102
 
88
103
  /**
@@ -94,8 +109,8 @@ export interface SerializerOptions {
94
109
  */
95
110
  serializers?: readonly ValueSerializer[]
96
111
  /**
97
- * Whether to append the built-in serializers (`builtinSerializers`) to the list.
98
- * Defaults to `true`.
112
+ * Whether to append the built-in serializers (`builtinSerializers`) to the list, except those
113
+ * whose tag a serializer in `serializers` takes, which it replaces. Defaults to `true`.
99
114
  */
100
115
  builtins?: boolean
101
116
  }
@@ -137,12 +152,16 @@ export function createSerializer(options: SerializerOptions = {}): Serializer {
137
152
  const custom = options.serializers ? [...options.serializers] : []
138
153
  custom.forEach(assertIsValueSerializer)
139
154
 
155
+ const customTags = new Set(custom.map((s) => s.tag))
140
156
  const serializers: readonly ValueSerializer[] =
141
157
  options.builtins === false
142
158
  ? custom
143
159
  : [
144
160
  ...custom,
145
- ...Object.values<ValueSerializer>(builtinSerializers).filter((s) => !custom.includes(s)),
161
+ // a custom serializer replaces the built-in with its tag
162
+ ...Object.values<ValueSerializer>(builtinSerializers).filter(
163
+ (s) => !customTags.has(s.tag)
164
+ ),
146
165
  ]
147
166
 
148
167
  const byTag = new Map<string, ValueSerializer>()
@@ -214,9 +233,11 @@ export function createSerializer(options: SerializerOptions = {}): Serializer {
214
233
  )
215
234
  },
216
235
 
217
- decodeValue(root, json) {
236
+ decodeValue(root, json, options) {
218
237
  assertIsTreeRoot(root)
219
- return runWithNewErrorDiagnosticsContext(() => createDecodeContext(config, root).decode(json))
238
+ return runWithNewErrorDiagnosticsContext(() =>
239
+ createDecodeContext(config, root, !!options?.generateNewIds).decode(json)
240
+ )
220
241
  },
221
242
 
222
243
  encodeCall(root, call) {
@@ -273,7 +294,7 @@ export function createSerializer(options: SerializerOptions = {}): Serializer {
273
294
  })
274
295
  },
275
296
 
276
- decodeCall(root, json) {
297
+ decodeCall(root, json, options) {
277
298
  assertIsTreeRoot(root)
278
299
 
279
300
  return runWithNewErrorDiagnosticsContext(() => {
@@ -292,7 +313,7 @@ export function createSerializer(options: SerializerOptions = {}): Serializer {
292
313
 
293
314
  const name = json.name
294
315
  const rawArgs = json.args as JsonValue[] | undefined
295
- const ctx = createDecodeContext(config, root)
316
+ const ctx = createDecodeContext(config, root, !!options?.generateNewIds)
296
317
 
297
318
  const target = withErrorPathSegment("target", () => decodeTarget(json.target, ctx))
298
319
  const resolution = resolveCallNameOrThrow(target, name)
@@ -35,6 +35,12 @@ export interface DecodeContext {
35
35
  */
36
36
  readonly root: object
37
37
 
38
+ /**
39
+ * Whether models decoded from snapshots must get new model ids (the `generateNewIds` option
40
+ * of the decoding). A serializer that creates models honors it; references ignore it.
41
+ */
42
+ readonly generateNewIds: boolean
43
+
38
44
  /**
39
45
  * Decodes a nested value with the same serializer.
40
46
  *
@@ -14,13 +14,13 @@ import type { ValueSerializer } from "./ValueSerializer"
14
14
  /**
15
15
  * @internal
16
16
  *
17
- * Marks a serializer whose encoded value is written as it is, without the marker
18
- * (snapshots already mark themselves).
17
+ * The tag of the serializer whose encoded value is written as it is, without the marker
18
+ * (snapshots already mark themselves), and that decodes every value with `$modelType` / `$frozen`.
19
19
  */
20
- export const unmarkedEncodingSymbol = Symbol("unmarkedEncoding")
20
+ export const snapshotTag = "snapshot"
21
21
 
22
22
  const snapshotSerializer: ValueSerializer<object, any> = {
23
- tag: "snapshot",
23
+ tag: snapshotTag,
24
24
 
25
25
  is(value): value is object {
26
26
  return isModel(value) || value instanceof Frozen
@@ -30,11 +30,10 @@ const snapshotSerializer: ValueSerializer<object, any> = {
30
30
  return getSnapshot(value)
31
31
  },
32
32
 
33
- decode(json) {
34
- return fromSnapshot(json)
33
+ decode(json, ctx) {
34
+ return fromSnapshot<object>(json, { generateNewIds: ctx.generateNewIds })
35
35
  },
36
36
  }
37
- ;(snapshotSerializer as unknown as Record<symbol, boolean>)[unmarkedEncodingSymbol] = true
38
37
 
39
38
  const dataModelSerializer: ValueSerializer<AnyDataModel, { type: string; data: JsonValue }> = {
40
39
  tag: "dataModel",
@@ -198,14 +197,18 @@ const undefinedSerializer: ValueSerializer<undefined, undefined> = {
198
197
 
199
198
  /**
200
199
  * The built-in value serializers, appended in this order to every serializer's list (unless it is
201
- * created with `builtins: false`). Pick some of them to build a list by hand, e.g.
200
+ * created with `builtins: false`), except those whose tag a serializer of the list takes. Pick some of them to build a list by hand, e.g.
202
201
  * `createSerializer({ serializers: [builtinSerializers.date], builtins: false })`.
203
202
  */
204
203
  export const builtinSerializers = Object.freeze({
205
204
  /**
206
205
  * Models and frozen data that aren't references, as their snapshots (unmarked, since snapshots
207
206
  * mark themselves with `$modelType` / `$frozen`), decoded with `fromSnapshot` into new
208
- * instances. Tree arrays and plain objects that aren't references are walked like any other.
207
+ * instances (with new model ids when decoded with `generateNewIds`). Tree arrays and plain
208
+ * objects that aren't references are walked like any other.
209
+ *
210
+ * A custom serializer tagged `snapshot` replaces it: its encoded value must be a snapshot too,
211
+ * and it decodes every value with `$modelType` / `$frozen`.
209
212
  */
210
213
  snapshot: snapshotSerializer,
211
214
  /**
@@ -3,7 +3,7 @@ import { modelTypeKey } from "../model/metadata"
3
3
  import type { PathElement } from "../parent/pathTypes"
4
4
  import { copyOwnEnumerableProps, hasOwnProp, isArray, isObject, isPlainObject } from "../utils"
5
5
  import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
6
- import { builtinEncodedTypeof, unmarkedEncodingSymbol } from "./builtinSerializers"
6
+ import { builtinEncodedTypeof, snapshotTag } from "./builtinSerializers"
7
7
  import { badPayloadError, CallError, CallErrorCode } from "./CallError"
8
8
  import type { JsonValue } from "./JsonValue"
9
9
  import type { DecodeContext, EncodeContext, ValueSerializer } from "./ValueSerializer"
@@ -33,8 +33,8 @@ export interface SerializerConfig {
33
33
  readonly byTypeof: ReadonlyMap<SerializerGroup, readonly ValueSerializer[]>
34
34
  readonly byTag: ReadonlyMap<string, ValueSerializer>
35
35
  /**
36
- * The serializer that decodes values with `$modelType` / `$frozen` (the snapshot serializer),
37
- * if in the list.
36
+ * The serializer that decodes values with `$modelType` / `$frozen` (the one tagged
37
+ * `snapshot`), if in the list.
38
38
  */
39
39
  readonly unmarked: ValueSerializer | undefined
40
40
  }
@@ -114,7 +114,13 @@ function withPathSegment<A, R>(key: PathElement, fn: (arg: A) => R, arg: A): R {
114
114
  * @internal
115
115
  */
116
116
  export function isUnmarkedSerializer(serializer: ValueSerializer): boolean {
117
- return unmarkedEncodingSymbol in serializer
117
+ return serializer.tag === snapshotTag
118
+ }
119
+
120
+ function isSnapshotJson(payload: unknown): boolean {
121
+ return (
122
+ isPlainObject(payload) && (hasOwnProp(payload, modelTypeKey) || hasOwnProp(payload, frozenKey))
123
+ )
118
124
  }
119
125
 
120
126
  /**
@@ -180,7 +186,18 @@ export function createEncodeContext(config: SerializerConfig, root: object): Enc
180
186
  } catch (e) {
181
187
  throw wrapSerializerError(e, CallErrorCode.CannotEncode, serializer.tag, value)
182
188
  }
183
- return isUnmarkedSerializer(serializer) ? payload! : markValue(serializer.tag, payload)
189
+ if (!isUnmarkedSerializer(serializer)) {
190
+ return markValue(serializer.tag, payload)
191
+ }
192
+ // written unmarked, so it must mark itself to be decoded by the same serializer
193
+ if (!isSnapshotJson(payload)) {
194
+ throw new CallError({
195
+ code: CallErrorCode.CannotEncode,
196
+ message: `the '${snapshotTag}' serializer must encode a snapshot (with $modelType or $frozen)`,
197
+ previewValue: value,
198
+ })
199
+ }
200
+ return payload!
184
201
  }
185
202
 
186
203
  const encodePlain = (value: unknown, plainKind: PlainKind): JsonValue => {
@@ -266,7 +283,11 @@ export function createEncodeContext(config: SerializerConfig, root: object): Enc
266
283
  * Creates the decoding context for one decoding: `ctx.decode` decodes any JSON value. It must run
267
284
  * inside an error diagnostics context.
268
285
  */
269
- export function createDecodeContext(config: SerializerConfig, root: object): DecodeContext {
286
+ export function createDecodeContext(
287
+ config: SerializerConfig,
288
+ root: object,
289
+ generateNewIds: boolean
290
+ ): DecodeContext {
270
291
  const decodeWithSerializer = (serializer: ValueSerializer, json: unknown): unknown => {
271
292
  try {
272
293
  return serializer.decode(json, ctx)
@@ -330,6 +351,7 @@ export function createDecodeContext(config: SerializerConfig, root: object): Dec
330
351
 
331
352
  const ctx: DecodeContext = {
332
353
  root,
354
+ generateNewIds,
333
355
  decode: (json: JsonValue, key?: PathElement): unknown =>
334
356
  key === undefined ? decodeAny(json) : withPathSegment(key, decodeAny, json),
335
357
  }