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.
- package/CHANGELOG.md +6 -0
- package/dist/mobx-keystone.esm.js +32 -18
- package/dist/mobx-keystone.esm.mjs +32 -18
- package/dist/mobx-keystone.umd.js +32 -18
- package/dist/types/serialization/Serializer.d.ts +18 -4
- package/dist/types/serialization/ValueSerializer.d.ts +5 -0
- package/dist/types/serialization/builtinSerializers.d.ts +6 -2
- package/package.json +1 -1
- package/src/serialization/Serializer.ts +30 -9
- package/src/serialization/ValueSerializer.ts +6 -0
- package/src/serialization/builtinSerializers.ts +12 -9
- package/src/serialization/codec.ts +28 -6
|
@@ -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
|
|
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
|
@@ -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
|
-
|
|
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(() =>
|
|
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
|
-
*
|
|
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
|
|
20
|
+
export const snapshotTag = "snapshot"
|
|
21
21
|
|
|
22
22
|
const snapshotSerializer: ValueSerializer<object, any> = {
|
|
23
|
-
tag:
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
}
|