mobx-keystone 2.3.1 → 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 +22 -0
- package/dist/mobx-keystone.esm.js +3604 -2414
- package/dist/mobx-keystone.esm.mjs +3604 -2414
- package/dist/mobx-keystone.umd.js +3685 -2481
- package/dist/types/action/CallKind.d.ts +29 -0
- package/dist/types/action/applyAction.d.ts +7 -1
- package/dist/types/action/index.d.ts +1 -0
- package/dist/types/action/resolveCallName.d.ts +1 -0
- package/dist/types/actionMiddlewares/actionSerialization/actionSerialization.d.ts +14 -0
- package/dist/types/actionMiddlewares/actionSerialization/applySerializedAction.d.ts +6 -0
- package/dist/types/actionMiddlewares/actionSerialization/core.d.ts +7 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/patch/emitPatch.d.ts +6 -0
- package/dist/types/serialization/Call.d.ts +55 -0
- package/dist/types/serialization/CallError.d.ts +96 -0
- package/dist/types/serialization/JsonValue.d.ts +6 -0
- package/dist/types/serialization/Serializer.d.ts +98 -0
- package/dist/types/serialization/ValueSerializer.d.ts +93 -0
- package/dist/types/serialization/applyCall.d.ts +54 -0
- package/dist/types/serialization/builtinSerializers.d.ts +53 -0
- package/dist/types/serialization/codec.d.ts +1 -0
- package/dist/types/serialization/index.d.ts +8 -0
- package/dist/types/serialization/references.d.ts +74 -0
- package/dist/types/serialization/resolvedCall.d.ts +1 -0
- package/package.json +1 -1
- package/src/action/CallKind.ts +29 -0
- package/src/action/applyAction.ts +22 -49
- package/src/action/applySet.ts +21 -1
- package/src/action/index.ts +1 -0
- package/src/action/resolveCallName.ts +207 -0
- package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +14 -0
- package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +6 -0
- package/src/actionMiddlewares/actionSerialization/core.ts +7 -0
- package/src/actionMiddlewares/actionSerialization/index.ts +2 -0
- package/src/actionMiddlewares/actionSerialization/objectPathSerializer.ts +3 -2
- package/src/index.ts +1 -0
- package/src/parent/path.ts +45 -12
- package/src/patch/emitPatch.ts +6 -0
- package/src/serialization/Call.ts +58 -0
- package/src/serialization/CallError.ts +153 -0
- package/src/serialization/JsonValue.ts +12 -0
- package/src/serialization/Serializer.ts +342 -0
- package/src/serialization/ValueSerializer.ts +104 -0
- package/src/serialization/applyCall.ts +190 -0
- package/src/serialization/builtinSerializers.ts +264 -0
- package/src/serialization/codec.ts +360 -0
- package/src/serialization/index.ts +15 -0
- package/src/serialization/references.ts +225 -0
- package/src/serialization/resolvedCall.ts +169 -0
- package/src/standardActions/actions.ts +11 -3
- package/src/utils/errorDiagnostics.ts +9 -4
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { PathElement } from "../parent/pathTypes"
|
|
2
|
+
import type { JsonValue } from "./JsonValue"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The key that marks a special value in the encoded JSON:
|
|
6
|
+
* `{ "$mobxKeystone": <tag>, "v": <payload> }`.
|
|
7
|
+
*/
|
|
8
|
+
export const serializationMarkerKey = "$mobxKeystone"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Context passed to `ValueSerializer.is` and `ValueSerializer.encode`.
|
|
12
|
+
*/
|
|
13
|
+
export interface EncodeContext {
|
|
14
|
+
/**
|
|
15
|
+
* Root of the tree the value is encoded against.
|
|
16
|
+
*/
|
|
17
|
+
readonly root: object
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Encodes a nested value with the same serializer.
|
|
21
|
+
*
|
|
22
|
+
* @param value Value to encode.
|
|
23
|
+
* @param key Where the value is inside its parent, for error paths.
|
|
24
|
+
* @returns The encoded value.
|
|
25
|
+
*/
|
|
26
|
+
encode(value: unknown, key?: PathElement): JsonValue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Context passed to `ValueSerializer.decode`.
|
|
31
|
+
*/
|
|
32
|
+
export interface DecodeContext {
|
|
33
|
+
/**
|
|
34
|
+
* Root of the tree the value is decoded against.
|
|
35
|
+
*/
|
|
36
|
+
readonly root: object
|
|
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
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Decodes a nested value with the same serializer.
|
|
46
|
+
*
|
|
47
|
+
* @param json Value to decode.
|
|
48
|
+
* @param key Where the value is inside its parent, for error paths.
|
|
49
|
+
* @returns The decoded value.
|
|
50
|
+
*/
|
|
51
|
+
decode(json: JsonValue, key?: PathElement): unknown
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Handles one kind of value: encodes it as `{ "$mobxKeystone": tag, "v": payload }` and decodes
|
|
56
|
+
* it back. `createSerializer` composes a list of them into a `Serializer`.
|
|
57
|
+
*
|
|
58
|
+
* @template T Type of the values it handles.
|
|
59
|
+
* @template J Type of the encoded payload (`undefined` to carry none).
|
|
60
|
+
*/
|
|
61
|
+
export interface ValueSerializer<T = any, J extends JsonValue | undefined = any> {
|
|
62
|
+
/**
|
|
63
|
+
* Unique, e.g. `"geom/Polygon"`; the `$mobxKeystone` of the encoded value.
|
|
64
|
+
*/
|
|
65
|
+
readonly tag: string
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether this serializer encodes `value`.
|
|
69
|
+
*
|
|
70
|
+
* @param value Value to check.
|
|
71
|
+
* @param ctx Encoding context; `ctx.root` is the tree the value is encoded against.
|
|
72
|
+
*/
|
|
73
|
+
is(value: unknown, ctx: EncodeContext): value is T
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Encodes a value `is` accepted. Use `ctx.encode(child)` for nested values.
|
|
77
|
+
*
|
|
78
|
+
* @param value Value to encode.
|
|
79
|
+
* @param ctx Encoding context.
|
|
80
|
+
* @returns The payload (`v`), or `undefined` to carry none.
|
|
81
|
+
*/
|
|
82
|
+
encode(value: T, ctx: EncodeContext): J
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Decodes a payload back into a value. Use `ctx.decode(child)` for nested values.
|
|
86
|
+
*
|
|
87
|
+
* @param json The payload (`v`).
|
|
88
|
+
* @param ctx Decoding context.
|
|
89
|
+
* @returns The decoded value.
|
|
90
|
+
*/
|
|
91
|
+
decode(json: J, ctx: DecodeContext): T
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Only for reference serializers: whether this serializer references `value`, a node of the
|
|
95
|
+
* tree, when it is a call's target. Unlike `is`, it ignores options such as `when`, since the
|
|
96
|
+
* call must run on the node itself. A call's target goes through the first serializer whose
|
|
97
|
+
* `claimsTarget` accepts it (else as a `pathRef`), and only serializers with `claimsTarget`
|
|
98
|
+
* decode it.
|
|
99
|
+
*
|
|
100
|
+
* @param value The call's target.
|
|
101
|
+
* @param ctx Encoding context.
|
|
102
|
+
*/
|
|
103
|
+
claimsTarget?(value: object, ctx: EncodeContext): boolean
|
|
104
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { transaction } from "mobx"
|
|
2
|
+
import { CallKind } from "../action/CallKind"
|
|
3
|
+
import { isTreeNode } from "../tweaker/core"
|
|
4
|
+
import { failure, hasOwnProp, isObject, isPlainObject } from "../utils"
|
|
5
|
+
import type { Call, ResolvedCall } from "./Call"
|
|
6
|
+
import { CallError, CallErrorCode } from "./CallError"
|
|
7
|
+
import {
|
|
8
|
+
assertIsCallShape,
|
|
9
|
+
assertIsTargetInTree,
|
|
10
|
+
assertIsTreeRoot,
|
|
11
|
+
getResolvedCallData,
|
|
12
|
+
toResolvedCall,
|
|
13
|
+
} from "./resolvedCall"
|
|
14
|
+
import { serializationMarkerKey } from "./ValueSerializer"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options of `applyCall` and `applyCallAsync`.
|
|
18
|
+
*/
|
|
19
|
+
export interface ApplyCallOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Decides whether a call may run, after its target and name are resolved and before anything
|
|
22
|
+
* runs. Returning false throws a `CallError` with code `Forbidden`; an error it throws passes
|
|
23
|
+
* through as it is. Reading `call.args` decodes the arguments (once).
|
|
24
|
+
*
|
|
25
|
+
* Defaults to `allowModelActions`.
|
|
26
|
+
*/
|
|
27
|
+
canCall?(call: ResolvedCall): boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The default `canCall`: allows model actions (`CallKind.ModelAction`) only.
|
|
32
|
+
*
|
|
33
|
+
* @param call The call.
|
|
34
|
+
* @returns Whether it is a model action.
|
|
35
|
+
*/
|
|
36
|
+
export function allowModelActions(call: ResolvedCall): boolean {
|
|
37
|
+
return call.kind === CallKind.ModelAction
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A `canCall` that allows every call, for calls from a source that is trusted.
|
|
42
|
+
*
|
|
43
|
+
* @param _call The call.
|
|
44
|
+
* @returns `true`.
|
|
45
|
+
*/
|
|
46
|
+
export function allowAnyCall(_call: ResolvedCall): boolean {
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const kindDescriptions: Record<CallKind, string> = {
|
|
51
|
+
[CallKind.ModelAction]: "a model action",
|
|
52
|
+
[CallKind.StandaloneAction]: "a standalone action",
|
|
53
|
+
[CallKind.StandardAction]: "a standard action",
|
|
54
|
+
[CallKind.BuiltInAction]: "a built-in action",
|
|
55
|
+
[CallKind.View]: "a view",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function looksLikeEncodedCall(call: Call): boolean {
|
|
59
|
+
const target: unknown = call.target
|
|
60
|
+
return isPlainObject(target) && hasOwnProp(target, serializationMarkerKey) && !isTreeNode(target)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function wrongKind(call: Call, message: string): CallError {
|
|
64
|
+
return new CallError({
|
|
65
|
+
code: CallErrorCode.WrongKind,
|
|
66
|
+
message: `'${call.name}' ${message}`,
|
|
67
|
+
path: ["name"],
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPromiseLike(value: unknown): boolean {
|
|
72
|
+
return (
|
|
73
|
+
(isObject(value) || typeof value === "function") &&
|
|
74
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolves and checks a call, and returns the function that runs it.
|
|
80
|
+
*/
|
|
81
|
+
function prepareCall(
|
|
82
|
+
root: object,
|
|
83
|
+
call: Call | ResolvedCall,
|
|
84
|
+
options: ApplyCallOptions | undefined,
|
|
85
|
+
allowFlows: boolean
|
|
86
|
+
): () => unknown {
|
|
87
|
+
assertIsTreeRoot(root)
|
|
88
|
+
|
|
89
|
+
if (!isObject(call)) {
|
|
90
|
+
throw failure("call must be an object")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const isResolved = getResolvedCallData(call) !== undefined
|
|
94
|
+
if (!isResolved) {
|
|
95
|
+
if (looksLikeEncodedCall(call)) {
|
|
96
|
+
throw failure("this looks like an EncodedCall; decode it first")
|
|
97
|
+
}
|
|
98
|
+
assertIsCallShape(call)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// also checked for decoded calls, since they could be applied to another tree
|
|
102
|
+
assertIsTargetInTree(call.target, root)
|
|
103
|
+
|
|
104
|
+
const resolvedCall = isResolved ? (call as ResolvedCall) : toResolvedCall(call)
|
|
105
|
+
const { resolution, hasArgs } = getResolvedCallData(resolvedCall)!
|
|
106
|
+
|
|
107
|
+
if (!allowFlows && resolution.isFlow) {
|
|
108
|
+
throw wrongKind(resolvedCall, "is a flow; use applyCallAsync")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (resolution.kind !== CallKind.View) {
|
|
112
|
+
if (!hasArgs) {
|
|
113
|
+
throw wrongKind(resolvedCall, "is an action; pass args")
|
|
114
|
+
}
|
|
115
|
+
} else if (resolution.viewForm === "getter") {
|
|
116
|
+
if (hasArgs) {
|
|
117
|
+
throw wrongKind(resolvedCall, "is a getter; leave args out")
|
|
118
|
+
}
|
|
119
|
+
} else if (!hasArgs) {
|
|
120
|
+
throw wrongKind(resolvedCall, "is a method; pass args")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const canCall = options?.canCall ?? allowModelActions
|
|
124
|
+
if (!canCall(resolvedCall)) {
|
|
125
|
+
const kindDescription = kindDescriptions[resolution.kind]
|
|
126
|
+
throw new CallError({
|
|
127
|
+
code: CallErrorCode.Forbidden,
|
|
128
|
+
message: options?.canCall
|
|
129
|
+
? `'${resolvedCall.name}' (${kindDescription}) was refused by canCall`
|
|
130
|
+
: `'${resolvedCall.name}' is ${kindDescription}, which the default canCall refuses; pass canCall to allow it`,
|
|
131
|
+
path: [],
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return () => {
|
|
136
|
+
const args = resolvedCall.args ?? []
|
|
137
|
+
|
|
138
|
+
if (resolution.kind !== CallKind.View) {
|
|
139
|
+
return resolution.run(args)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// one batch, so computeds read by the view stay cached while it runs
|
|
143
|
+
const result = transaction(() => resolution.run(args))
|
|
144
|
+
if (isPromiseLike(result)) {
|
|
145
|
+
throw wrongKind(resolvedCall, "returned a promise; async reads belong in a flow")
|
|
146
|
+
}
|
|
147
|
+
return result
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Runs a call (as returned by `Serializer.decodeCall`, or built in process) on a tree and returns
|
|
153
|
+
* its result as is. Flows are refused (`call.isFlow`); use `applyCallAsync` for them.
|
|
154
|
+
*
|
|
155
|
+
* The call's name is resolved on its target (see `CallKind`), then `canCall` decides whether it
|
|
156
|
+
* may run (by default only model actions may). Errors about the call are `CallError`s; errors
|
|
157
|
+
* thrown by the action or view are rethrown as they are.
|
|
158
|
+
*
|
|
159
|
+
* @template TRet Return type.
|
|
160
|
+
* @param root Root of the tree.
|
|
161
|
+
* @param call The call.
|
|
162
|
+
* @param options Options.
|
|
163
|
+
* @returns The result of the call.
|
|
164
|
+
*/
|
|
165
|
+
export function applyCall<TRet = unknown>(
|
|
166
|
+
root: object,
|
|
167
|
+
call: Call | ResolvedCall,
|
|
168
|
+
options?: ApplyCallOptions
|
|
169
|
+
): TRet {
|
|
170
|
+
return prepareCall(root, call, options, false)() as TRet
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Like `applyCall`, but accepts flows too and always returns a promise, which resolves to the
|
|
175
|
+
* result once the call (or the flow) ends. Every error is a rejection.
|
|
176
|
+
*
|
|
177
|
+
* @template TRet Return type.
|
|
178
|
+
* @param root Root of the tree.
|
|
179
|
+
* @param call The call.
|
|
180
|
+
* @param options Options.
|
|
181
|
+
* @returns A promise of the result of the call.
|
|
182
|
+
*/
|
|
183
|
+
export async function applyCallAsync<TRet = unknown>(
|
|
184
|
+
root: object,
|
|
185
|
+
call: Call | ResolvedCall,
|
|
186
|
+
options?: ApplyCallOptions
|
|
187
|
+
): Promise<TRet> {
|
|
188
|
+
const run = prepareCall(root, call, options, true)
|
|
189
|
+
return (await run()) as TRet
|
|
190
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { AnyDataModel } from "../dataModel/BaseDataModel"
|
|
2
|
+
import { isDataModel, isDataModelClass } from "../dataModel/utils"
|
|
3
|
+
import { Frozen } from "../frozen/Frozen"
|
|
4
|
+
import { isModel } from "../model/utils"
|
|
5
|
+
import { findModelInfoForPrototype, getModelInfoForName } from "../modelShared/modelInfo"
|
|
6
|
+
import { fromSnapshot } from "../snapshot/fromSnapshot"
|
|
7
|
+
import { getSnapshot } from "../snapshot/getSnapshot"
|
|
8
|
+
import { hasOwnProp, isArray, isMap, isPlainObject, isSet } from "../utils"
|
|
9
|
+
import { badPayloadError, CallError, CallErrorCode } from "./CallError"
|
|
10
|
+
import type { TypeofResult } from "./codec"
|
|
11
|
+
import type { JsonValue } from "./JsonValue"
|
|
12
|
+
import type { ValueSerializer } from "./ValueSerializer"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*
|
|
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
|
+
*/
|
|
20
|
+
export const snapshotTag = "snapshot"
|
|
21
|
+
|
|
22
|
+
const snapshotSerializer: ValueSerializer<object, any> = {
|
|
23
|
+
tag: snapshotTag,
|
|
24
|
+
|
|
25
|
+
is(value): value is object {
|
|
26
|
+
return isModel(value) || value instanceof Frozen
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
encode(value) {
|
|
30
|
+
return getSnapshot(value)
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
decode(json, ctx) {
|
|
34
|
+
return fromSnapshot<object>(json, { generateNewIds: ctx.generateNewIds })
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const dataModelSerializer: ValueSerializer<AnyDataModel, { type: string; data: JsonValue }> = {
|
|
39
|
+
tag: "dataModel",
|
|
40
|
+
|
|
41
|
+
is(value): value is AnyDataModel {
|
|
42
|
+
return isDataModel(value)
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
encode(value, ctx) {
|
|
46
|
+
// data models can only be created from registered classes
|
|
47
|
+
const modelInfo = findModelInfoForPrototype(Object.getPrototypeOf(value))!
|
|
48
|
+
return { type: modelInfo.name, data: ctx.encode(value.$) }
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
decode(json, ctx) {
|
|
52
|
+
if (!isPlainObject(json) || typeof json.type !== "string" || !hasOwnProp(json, "data")) {
|
|
53
|
+
throw badPayloadError("dataModel", json)
|
|
54
|
+
}
|
|
55
|
+
const modelInfo = getModelInfoForName(json.type)
|
|
56
|
+
if (!modelInfo || !isDataModelClass(modelInfo.class)) {
|
|
57
|
+
throw new CallError({
|
|
58
|
+
code: CallErrorCode.BadArgument,
|
|
59
|
+
message: `no data model is registered as '${json.type}'`,
|
|
60
|
+
previewValue: json,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
return new modelInfo.class(ctx.decode(json.data))
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const dateSerializer: ValueSerializer<Date, string | number | null> = {
|
|
68
|
+
tag: "date",
|
|
69
|
+
|
|
70
|
+
is(value): value is Date {
|
|
71
|
+
return value instanceof Date
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
encode(value) {
|
|
75
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString()
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
decode(json) {
|
|
79
|
+
if (json === null) {
|
|
80
|
+
return new Date(Number.NaN)
|
|
81
|
+
}
|
|
82
|
+
const date = typeof json === "number" || typeof json === "string" ? new Date(json) : undefined
|
|
83
|
+
if (!date || Number.isNaN(date.getTime())) {
|
|
84
|
+
throw badPayloadError("date", json)
|
|
85
|
+
}
|
|
86
|
+
return date
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const mapSerializer: ValueSerializer<Map<unknown, unknown>, [JsonValue, JsonValue][]> = {
|
|
91
|
+
tag: "map",
|
|
92
|
+
|
|
93
|
+
is(value): value is Map<unknown, unknown> {
|
|
94
|
+
return isMap(value)
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
encode(value, ctx) {
|
|
98
|
+
return Array.from(value.entries(), ([k, v], i): [JsonValue, JsonValue] => [
|
|
99
|
+
ctx.encode(k, i),
|
|
100
|
+
ctx.encode(v, i),
|
|
101
|
+
])
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
decode(json, ctx) {
|
|
105
|
+
if (!isArray(json)) {
|
|
106
|
+
throw badPayloadError("map", json)
|
|
107
|
+
}
|
|
108
|
+
const map = new Map<unknown, unknown>()
|
|
109
|
+
json.forEach((entry, i) => {
|
|
110
|
+
if (!isArray(entry) || entry.length !== 2) {
|
|
111
|
+
throw badPayloadError("map", json)
|
|
112
|
+
}
|
|
113
|
+
map.set(ctx.decode(entry[0], i), ctx.decode(entry[1], i))
|
|
114
|
+
})
|
|
115
|
+
return map
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const setSerializer: ValueSerializer<Set<unknown>, JsonValue[]> = {
|
|
120
|
+
tag: "set",
|
|
121
|
+
|
|
122
|
+
is(value): value is Set<unknown> {
|
|
123
|
+
return isSet(value)
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
encode(value, ctx) {
|
|
127
|
+
return Array.from(value.values(), (v, i) => ctx.encode(v, i))
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
decode(json, ctx) {
|
|
131
|
+
if (!isArray(json)) {
|
|
132
|
+
throw badPayloadError("set", json)
|
|
133
|
+
}
|
|
134
|
+
return new Set(json.map((v, i) => ctx.decode(v, i)))
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const nonFiniteNumbers: Record<string, number> = {
|
|
139
|
+
NaN: Number.NaN,
|
|
140
|
+
Infinity: Number.POSITIVE_INFINITY,
|
|
141
|
+
"-Infinity": Number.NEGATIVE_INFINITY,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const numberSerializer: ValueSerializer<number, string> = {
|
|
145
|
+
tag: "number",
|
|
146
|
+
|
|
147
|
+
is(value): value is number {
|
|
148
|
+
return typeof value === "number" && !Number.isFinite(value)
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
encode(value) {
|
|
152
|
+
return String(value)
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
decode(json) {
|
|
156
|
+
if (typeof json !== "string" || !hasOwnProp(nonFiniteNumbers, json)) {
|
|
157
|
+
throw badPayloadError("number", json)
|
|
158
|
+
}
|
|
159
|
+
return nonFiniteNumbers[json]
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const bigintSerializer: ValueSerializer<bigint, string> = {
|
|
164
|
+
tag: "bigint",
|
|
165
|
+
|
|
166
|
+
is(value): value is bigint {
|
|
167
|
+
return typeof value === "bigint"
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
encode(value) {
|
|
171
|
+
return value.toString()
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
decode(json) {
|
|
175
|
+
if (typeof json !== "string" || !/^-?\d+$/.test(json)) {
|
|
176
|
+
throw badPayloadError("bigint", json)
|
|
177
|
+
}
|
|
178
|
+
return BigInt(json)
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const undefinedSerializer: ValueSerializer<undefined, undefined> = {
|
|
183
|
+
tag: "undefined",
|
|
184
|
+
|
|
185
|
+
is(value): value is undefined {
|
|
186
|
+
return value === undefined
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
encode() {
|
|
190
|
+
return undefined
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
decode() {
|
|
194
|
+
return undefined
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The built-in value serializers, appended in this order to every serializer's list (unless it is
|
|
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.
|
|
201
|
+
* `createSerializer({ serializers: [builtinSerializers.date], builtins: false })`.
|
|
202
|
+
*/
|
|
203
|
+
export const builtinSerializers = Object.freeze({
|
|
204
|
+
/**
|
|
205
|
+
* Models and frozen data that aren't references, as their snapshots (unmarked, since snapshots
|
|
206
|
+
* mark themselves with `$modelType` / `$frozen`), decoded with `fromSnapshot` into new
|
|
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`.
|
|
212
|
+
*/
|
|
213
|
+
snapshot: snapshotSerializer,
|
|
214
|
+
/**
|
|
215
|
+
* Data model instances, as their registered name and their data (encoded, so data of the tree
|
|
216
|
+
* goes as a reference), decoded as a new instance over the decoded data.
|
|
217
|
+
*/
|
|
218
|
+
dataModel: dataModelSerializer,
|
|
219
|
+
/**
|
|
220
|
+
* Dates, as `toISOString()` (an invalid date as `null`); decodes an ISO string, a timestamp or
|
|
221
|
+
* `null`.
|
|
222
|
+
*/
|
|
223
|
+
date: dateSerializer,
|
|
224
|
+
/**
|
|
225
|
+
* Maps (plain or observable), as an array of `[key, value]` entries; decoded as plain maps.
|
|
226
|
+
*/
|
|
227
|
+
map: mapSerializer,
|
|
228
|
+
/**
|
|
229
|
+
* Sets (plain or observable), as an array of their values; decoded as plain sets.
|
|
230
|
+
*/
|
|
231
|
+
set: setSerializer,
|
|
232
|
+
/**
|
|
233
|
+
* The numbers JSON can't hold: `"NaN"`, `"Infinity"`, `"-Infinity"`.
|
|
234
|
+
*/
|
|
235
|
+
number: numberSerializer,
|
|
236
|
+
/**
|
|
237
|
+
* Bigints, as a decimal string, with no loss of precision.
|
|
238
|
+
*/
|
|
239
|
+
bigint: bigintSerializer,
|
|
240
|
+
/**
|
|
241
|
+
* `undefined` (as an array item, an argument or an object property, which keeps its key).
|
|
242
|
+
*/
|
|
243
|
+
undefined: undefinedSerializer,
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @internal
|
|
248
|
+
*
|
|
249
|
+
* The only `typeof` of the values each built-in encodes, so encoding can skip the built-ins that
|
|
250
|
+
* can't apply to a value.
|
|
251
|
+
*/
|
|
252
|
+
export const builtinEncodedTypeof: ReadonlyMap<ValueSerializer, TypeofResult> = new Map<
|
|
253
|
+
ValueSerializer,
|
|
254
|
+
TypeofResult
|
|
255
|
+
>([
|
|
256
|
+
[snapshotSerializer, "object"],
|
|
257
|
+
[dataModelSerializer, "object"],
|
|
258
|
+
[dateSerializer, "object"],
|
|
259
|
+
[mapSerializer, "object"],
|
|
260
|
+
[setSerializer, "object"],
|
|
261
|
+
[numberSerializer, "number"],
|
|
262
|
+
[bigintSerializer, "bigint"],
|
|
263
|
+
[undefinedSerializer, "undefined"],
|
|
264
|
+
])
|