mobx-keystone 2.3.0 → 2.4.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.
- package/CHANGELOG.md +20 -0
- package/dist/mobx-keystone.esm.js +3737 -2722
- package/dist/mobx-keystone.esm.mjs +3737 -2722
- package/dist/mobx-keystone.umd.js +3750 -2721
- package/dist/types/action/CallKind.d.ts +29 -0
- package/dist/types/action/applyAction.d.ts +8 -2
- package/dist/types/action/applyMethodCall.d.ts +1 -1
- package/dist/types/action/index.d.ts +13 -12
- package/dist/types/action/isModelAction.d.ts +1 -1
- package/dist/types/action/middleware.d.ts +1 -1
- package/dist/types/action/resolveCallName.d.ts +1 -0
- package/dist/types/actionMiddlewares/actionSerialization/actionSerialization.d.ts +17 -3
- package/dist/types/actionMiddlewares/actionSerialization/applySerializedAction.d.ts +8 -2
- package/dist/types/actionMiddlewares/actionSerialization/arraySerializer.d.ts +2 -2
- package/dist/types/actionMiddlewares/actionSerialization/core.d.ts +7 -0
- package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
- package/dist/types/actionMiddlewares/actionSerialization/index.d.ts +3 -3
- package/dist/types/actionMiddlewares/actionSerialization/mapSerializer.d.ts +2 -2
- package/dist/types/actionMiddlewares/actionSerialization/objectPathSerializer.d.ts +2 -2
- package/dist/types/actionMiddlewares/actionSerialization/objectSnapshotSerializer.d.ts +1 -1
- package/dist/types/actionMiddlewares/actionSerialization/plainObjectSerializer.d.ts +1 -1
- package/dist/types/actionMiddlewares/actionSerialization/primitiveSerializer.d.ts +1 -1
- package/dist/types/actionMiddlewares/actionSerialization/setSerializer.d.ts +2 -2
- package/dist/types/actionMiddlewares/actionTrackingMiddleware.d.ts +3 -3
- package/dist/types/actionMiddlewares/index.d.ts +6 -6
- package/dist/types/actionMiddlewares/onActionMiddleware.d.ts +3 -3
- package/dist/types/actionMiddlewares/readonlyMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +2 -2
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +8 -8
- package/dist/types/computedTree/index.d.ts +1 -1
- package/dist/types/context/index.d.ts +1 -1
- package/dist/types/dataModel/BaseDataModel.d.ts +3 -3
- package/dist/types/dataModel/DataModel.d.ts +3 -3
- package/dist/types/dataModel/getDataModelMetadata.d.ts +3 -3
- package/dist/types/dataModel/index.d.ts +4 -4
- package/dist/types/dataModel/utils.d.ts +1 -1
- package/dist/types/deepChange/index.d.ts +1 -1
- package/dist/types/deepChange/onDeepChange.d.ts +1 -1
- package/dist/types/frozen/Frozen.d.ts +1 -1
- package/dist/types/frozen/index.d.ts +1 -1
- package/dist/types/globalConfig/index.d.ts +1 -1
- package/dist/types/index.d.ts +25 -24
- package/dist/types/model/BaseModel.d.ts +5 -5
- package/dist/types/model/Model.d.ts +3 -3
- package/dist/types/model/getModelMetadata.d.ts +3 -3
- package/dist/types/model/index.d.ts +6 -6
- package/dist/types/model/mixins.d.ts +4 -4
- package/dist/types/model/utils.d.ts +4 -4
- package/dist/types/modelShared/BaseModelShared.d.ts +5 -5
- package/dist/types/modelShared/index.d.ts +4 -4
- package/dist/types/modelShared/modelDecorator.d.ts +3 -3
- package/dist/types/modelShared/modelInfo.d.ts +3 -3
- package/dist/types/modelShared/newModel.d.ts +3 -3
- package/dist/types/modelShared/prop.d.ts +3 -3
- package/dist/types/modelShared/sharedInternalModel.d.ts +4 -4
- package/dist/types/parent/findParent.d.ts +1 -1
- package/dist/types/parent/index.d.ts +9 -9
- package/dist/types/parent/path.d.ts +1 -1
- package/dist/types/patch/Patch.d.ts +1 -1
- package/dist/types/patch/applyPatches.d.ts +1 -1
- package/dist/types/patch/emitPatch.d.ts +7 -1
- package/dist/types/patch/index.d.ts +5 -5
- package/dist/types/patch/jsonPatch.d.ts +2 -2
- package/dist/types/patch/patchRecorder.d.ts +2 -2
- package/dist/types/redux/index.d.ts +2 -2
- package/dist/types/redux/redux.d.ts +3 -3
- package/dist/types/ref/Ref.d.ts +3 -3
- package/dist/types/ref/core.d.ts +2 -2
- package/dist/types/ref/customRef.d.ts +2 -2
- package/dist/types/ref/index.d.ts +4 -4
- package/dist/types/ref/rootRef.d.ts +2 -2
- package/dist/types/rootStore/index.d.ts +1 -1
- 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 +84 -0
- package/dist/types/serialization/ValueSerializer.d.ts +88 -0
- package/dist/types/serialization/applyCall.d.ts +54 -0
- package/dist/types/serialization/builtinSerializers.d.ts +49 -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/dist/types/snapshot/SnapshotOf.d.ts +5 -5
- package/dist/types/snapshot/SnapshotProcessingError.d.ts +2 -2
- package/dist/types/snapshot/applySnapshot.d.ts +1 -1
- package/dist/types/snapshot/cloneTreeValue.d.ts +1 -1
- package/dist/types/snapshot/fromSnapshot.d.ts +2 -2
- package/dist/types/snapshot/getSnapshot.d.ts +2 -2
- package/dist/types/snapshot/index.d.ts +8 -8
- package/dist/types/snapshot/onSnapshot.d.ts +1 -1
- package/dist/types/standardActions/index.d.ts +3 -3
- package/dist/types/standardActions/objectActions.d.ts +1 -1
- package/dist/types/transforms/asMap.d.ts +1 -1
- package/dist/types/transforms/asSet.d.ts +2 -2
- package/dist/types/transforms/bigint.d.ts +1 -1
- package/dist/types/transforms/date.d.ts +1 -1
- package/dist/types/transforms/index.d.ts +4 -4
- package/dist/types/treeUtils/draft.d.ts +1 -1
- package/dist/types/treeUtils/index.d.ts +3 -3
- package/dist/types/tweaker/index.d.ts +2 -2
- package/dist/types/tweaker/tweak.d.ts +1 -1
- package/dist/types/types/SnapshotTypeMismatchError.d.ts +2 -2
- package/dist/types/types/TypeCheckError.d.ts +2 -2
- package/dist/types/types/TypeChecker.d.ts +3 -3
- package/dist/types/types/arrayBased/typesArray.d.ts +2 -2
- package/dist/types/types/arrayBased/typesTuple.d.ts +2 -2
- package/dist/types/types/getTypeInfo.d.ts +2 -2
- package/dist/types/types/index.d.ts +7 -7
- package/dist/types/types/objectBased/typesArraySet.d.ts +3 -3
- package/dist/types/types/objectBased/typesDataModelData.d.ts +4 -4
- package/dist/types/types/objectBased/typesModel.d.ts +4 -4
- package/dist/types/types/objectBased/typesObject.d.ts +3 -3
- package/dist/types/types/objectBased/typesObjectMap.d.ts +3 -3
- package/dist/types/types/objectBased/typesRecord.d.ts +2 -2
- package/dist/types/types/objectBased/typesRef.d.ts +3 -3
- package/dist/types/types/primitiveBased/typesEnum.d.ts +1 -1
- package/dist/types/types/primitiveBased/typesPrimitive.d.ts +3 -3
- package/dist/types/types/primitiveBased/typesRefinedPrimitive.d.ts +2 -2
- package/dist/types/types/schemas.d.ts +5 -5
- package/dist/types/types/tProp.d.ts +4 -4
- package/dist/types/types/typeCheck.d.ts +2 -2
- package/dist/types/types/types.d.ts +39 -39
- package/dist/types/types/utility/typeToStoredData.d.ts +2 -2
- package/dist/types/types/utility/typesBigInt.d.ts +1 -1
- package/dist/types/types/utility/typesCodec.d.ts +6 -6
- package/dist/types/types/utility/typesCodecCore.d.ts +4 -4
- package/dist/types/types/utility/typesCodecSupport.d.ts +4 -4
- package/dist/types/types/utility/typesDate.d.ts +2 -2
- package/dist/types/types/utility/typesMap.d.ts +2 -2
- package/dist/types/types/utility/typesMaybe.d.ts +1 -1
- package/dist/types/types/utility/typesOr.d.ts +2 -2
- package/dist/types/types/utility/typesRefinement.d.ts +3 -3
- package/dist/types/types/utility/typesSet.d.ts +3 -3
- package/dist/types/types/utility/typesSkipCheck.d.ts +2 -2
- package/dist/types/types/utility/typesTag.d.ts +2 -2
- package/dist/types/types/utility/typesUnchecked.d.ts +2 -2
- package/dist/types/utils/ModelPool.d.ts +1 -1
- package/dist/types/wrappers/ArraySet.d.ts +1 -1
- package/dist/types/wrappers/ObjectMap.d.ts +1 -1
- package/dist/types/wrappers/asMap.d.ts +1 -1
- package/dist/types/wrappers/asSet.d.ts +1 -1
- package/dist/types/wrappers/index.d.ts +4 -4
- package/package.json +5 -18
- 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/modelAction.ts +1 -1
- package/src/action/modelFlow.ts +1 -1
- 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/dataModel/BaseDataModel.ts +5 -5
- package/src/index.ts +1 -0
- package/src/model/BaseModel.ts +7 -6
- package/src/model/mixins.ts +1 -1
- package/src/model/newModel.ts +1 -1
- package/src/modelShared/modelDecorator.ts +4 -6
- package/src/modelShared/modelInfo.ts +2 -1
- package/src/modelShared/sharedInternalModel.ts +1 -1
- package/src/parent/path.ts +45 -12
- package/src/patch/emitPatch.ts +6 -0
- package/src/ref/Ref.ts +1 -1
- 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 +321 -0
- package/src/serialization/ValueSerializer.ts +98 -0
- package/src/serialization/applyCall.ts +190 -0
- package/src/serialization/builtinSerializers.ts +261 -0
- package/src/serialization/codec.ts +338 -0
- package/src/serialization/index.ts +15 -0
- package/src/serialization/references.ts +225 -0
- package/src/serialization/resolvedCall.ts +169 -0
- package/src/snapshot/SnapshotterAndReconcilerPriority.ts +2 -1
- package/src/snapshot/fromModelSnapshot.ts +1 -1
- package/src/snapshot/fromSnapshot.ts +1 -1
- package/src/standardActions/actions.ts +11 -3
- package/src/transforms/ImmutableDate.ts +19 -19
- package/src/tweaker/tweakArray.ts +1 -1
- package/src/tweaker/tweakFrozen.ts +1 -1
- package/src/tweaker/tweakModel.ts +1 -1
- package/src/tweaker/tweakPlainObject.ts +1 -1
- package/src/types/arrayBased/typesArray.ts +1 -1
- package/src/types/arrayBased/typesTuple.ts +1 -1
- package/src/types/objectBased/typesArraySet.ts +1 -1
- package/src/types/objectBased/typesModel.ts +2 -2
- package/src/types/objectBased/typesObject.ts +1 -1
- package/src/types/objectBased/typesObjectMap.ts +1 -1
- package/src/types/objectBased/typesRecord.ts +1 -1
- package/src/types/objectBased/typesRef.ts +2 -2
- package/src/types/primitiveBased/typesPrimitive.ts +1 -1
- package/src/types/schemas.ts +1 -1
- package/src/types/utility/typesCodecCore.ts +1 -1
- package/src/types/utility/typesCodecSupport.ts +2 -0
- package/src/types/utility/typesOr.ts +2 -2
- package/src/types/utility/typesRefinement.ts +1 -1
- package/src/utils/errorDiagnostics.ts +9 -4
- package/src/utils/index.ts +20 -13
- package/src/wrappers/ArraySet.ts +3 -5
- package/src/wrappers/ObjectMap.ts +3 -5
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CallKind } from "../action/CallKind"
|
|
2
|
+
import type { JsonValue } from "./JsonValue"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A call as JSON, as `Serializer.encodeCall` returns it and `Serializer.decodeCall` takes it.
|
|
6
|
+
*/
|
|
7
|
+
export interface EncodedCall {
|
|
8
|
+
/**
|
|
9
|
+
* A reference to the node the call runs on; for a standalone action, its first argument.
|
|
10
|
+
*/
|
|
11
|
+
target: JsonValue
|
|
12
|
+
/**
|
|
13
|
+
* Name of the action or view.
|
|
14
|
+
*/
|
|
15
|
+
name: string
|
|
16
|
+
/**
|
|
17
|
+
* Encoded arguments; left out to read a getter.
|
|
18
|
+
*/
|
|
19
|
+
args?: JsonValue[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A call to an action or a view of a node.
|
|
24
|
+
*/
|
|
25
|
+
export interface Call {
|
|
26
|
+
/**
|
|
27
|
+
* The node the call runs on; for a standalone action, its first argument.
|
|
28
|
+
*/
|
|
29
|
+
target: object
|
|
30
|
+
/**
|
|
31
|
+
* Name of the action or view.
|
|
32
|
+
*/
|
|
33
|
+
name: string
|
|
34
|
+
/**
|
|
35
|
+
* Arguments (for a standalone action, without the target); left out to read a getter.
|
|
36
|
+
*/
|
|
37
|
+
args?: unknown[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A call whose target and name are resolved, as `Serializer.decodeCall` returns it and `canCall`
|
|
42
|
+
* gets it.
|
|
43
|
+
*/
|
|
44
|
+
export interface ResolvedCall extends Readonly<Call> {
|
|
45
|
+
/**
|
|
46
|
+
* What `name` resolved to on the target.
|
|
47
|
+
*/
|
|
48
|
+
readonly kind: CallKind
|
|
49
|
+
/**
|
|
50
|
+
* Whether it is a `@modelFlow` or a `standaloneFlow`, which needs `applyCallAsync`.
|
|
51
|
+
*/
|
|
52
|
+
readonly isFlow: boolean
|
|
53
|
+
/**
|
|
54
|
+
* The arguments as JSON, only on a call returned by `decodeCall`. Reading `args` decodes them
|
|
55
|
+
* (once).
|
|
56
|
+
*/
|
|
57
|
+
readonly rawArgs?: JsonValue[]
|
|
58
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Path } from "../parent/pathTypes"
|
|
2
|
+
import { MobxKeystoneError } from "../utils"
|
|
3
|
+
import {
|
|
4
|
+
buildErrorMessageWithDiagnostics,
|
|
5
|
+
getErrorPathSnapshot,
|
|
6
|
+
noErrorValuePreview,
|
|
7
|
+
} from "../utils/errorDiagnostics"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The reason a call failed.
|
|
11
|
+
*/
|
|
12
|
+
export enum CallErrorCode {
|
|
13
|
+
/**
|
|
14
|
+
* The target, or a reference in the arguments, doesn't resolve.
|
|
15
|
+
*/
|
|
16
|
+
NotFound = "notFound",
|
|
17
|
+
/**
|
|
18
|
+
* Nothing by that name can be called on the target.
|
|
19
|
+
*/
|
|
20
|
+
UnknownName = "unknownName",
|
|
21
|
+
/**
|
|
22
|
+
* The name exists, but can't be used as asked (e.g. a method read without `args`).
|
|
23
|
+
*/
|
|
24
|
+
WrongKind = "wrongKind",
|
|
25
|
+
/**
|
|
26
|
+
* Refused by `canCall`.
|
|
27
|
+
*/
|
|
28
|
+
Forbidden = "forbidden",
|
|
29
|
+
/**
|
|
30
|
+
* An argument (or any other value being decoded) failed to decode.
|
|
31
|
+
*/
|
|
32
|
+
BadArgument = "badArgument",
|
|
33
|
+
/**
|
|
34
|
+
* A value (usually the result) can't be encoded.
|
|
35
|
+
*/
|
|
36
|
+
CannotEncode = "cannotEncode",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What a `CallError` is built from, e.g. by a custom `ValueSerializer` that wants to choose the
|
|
41
|
+
* code (a `CallError` it throws passes through as it is; any other error is wrapped).
|
|
42
|
+
*/
|
|
43
|
+
export interface CallErrorData {
|
|
44
|
+
/**
|
|
45
|
+
* The reason.
|
|
46
|
+
*/
|
|
47
|
+
code: CallErrorCode
|
|
48
|
+
/**
|
|
49
|
+
* The message, without the path and value preview the error adds to it.
|
|
50
|
+
*/
|
|
51
|
+
message: string
|
|
52
|
+
/**
|
|
53
|
+
* Path relative to the call or value, e.g. `["args", 1, "items", 3]`.
|
|
54
|
+
* Defaults to where the value being encoded or decoded is.
|
|
55
|
+
*/
|
|
56
|
+
path?: Path
|
|
57
|
+
/**
|
|
58
|
+
* The value the error is about, shown in the message.
|
|
59
|
+
*/
|
|
60
|
+
previewValue?: unknown
|
|
61
|
+
/**
|
|
62
|
+
* The error that caused it, if any. Its message is added to `message`, but left out of
|
|
63
|
+
* `toJSON()`, since it may hold the server's data.
|
|
64
|
+
*/
|
|
65
|
+
cause?: unknown
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The JSON form of a `CallError`, safe to send back to the caller.
|
|
70
|
+
*/
|
|
71
|
+
export interface CallErrorJson {
|
|
72
|
+
code: CallErrorCode
|
|
73
|
+
message: string
|
|
74
|
+
path: Path
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Thrown when a call or a value can't be decoded, resolved, allowed, run or encoded
|
|
79
|
+
* (extends `MobxKeystoneError`). Errors thrown by the action or view being called are not
|
|
80
|
+
* `CallError`s: they are rethrown as they are.
|
|
81
|
+
*
|
|
82
|
+
* Use `instanceof CallError` to distinguish call errors from other
|
|
83
|
+
* `MobxKeystoneError` instances.
|
|
84
|
+
*/
|
|
85
|
+
export class CallError extends MobxKeystoneError {
|
|
86
|
+
/**
|
|
87
|
+
* The reason.
|
|
88
|
+
*/
|
|
89
|
+
readonly code: CallErrorCode
|
|
90
|
+
/**
|
|
91
|
+
* Where in the call or value the error is, e.g. `["args", 1]`.
|
|
92
|
+
*/
|
|
93
|
+
readonly path: Path
|
|
94
|
+
/**
|
|
95
|
+
* The error that caused it, if any.
|
|
96
|
+
*/
|
|
97
|
+
readonly cause?: unknown
|
|
98
|
+
|
|
99
|
+
private readonly jsonMessage: string
|
|
100
|
+
|
|
101
|
+
constructor(data: CallErrorData) {
|
|
102
|
+
const path = data.path ?? getErrorPathSnapshot() ?? []
|
|
103
|
+
const previewValue = "previewValue" in data ? data.previewValue : noErrorValuePreview
|
|
104
|
+
const causeMessage =
|
|
105
|
+
data.cause === undefined
|
|
106
|
+
? undefined
|
|
107
|
+
: data.cause instanceof Error
|
|
108
|
+
? data.cause.message
|
|
109
|
+
: String(data.cause)
|
|
110
|
+
|
|
111
|
+
super(
|
|
112
|
+
buildErrorMessageWithDiagnostics({
|
|
113
|
+
message: causeMessage === undefined ? data.message : `${data.message}: ${causeMessage}`,
|
|
114
|
+
path,
|
|
115
|
+
previewValue,
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
this.code = data.code
|
|
120
|
+
this.path = path
|
|
121
|
+
this.cause = data.cause
|
|
122
|
+
// a CannotEncode value is the server's, and so may be a cause's message
|
|
123
|
+
this.jsonMessage =
|
|
124
|
+
data.code === CallErrorCode.CannotEncode
|
|
125
|
+
? data.message
|
|
126
|
+
: causeMessage === undefined
|
|
127
|
+
? this.message
|
|
128
|
+
: buildErrorMessageWithDiagnostics({ message: data.message, path, previewValue })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The error as JSON, for RPC and tool layers. It leaves out the cause's message, and the value
|
|
133
|
+
* preview of a `CannotEncode` error, since they may hold the server's data.
|
|
134
|
+
*/
|
|
135
|
+
toJSON(): CallErrorJson {
|
|
136
|
+
return {
|
|
137
|
+
code: this.code,
|
|
138
|
+
message: this.jsonMessage,
|
|
139
|
+
path: this.path,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
export function badPayloadError(tag: string, json: unknown): CallError {
|
|
148
|
+
return new CallError({
|
|
149
|
+
code: CallErrorCode.BadArgument,
|
|
150
|
+
message: `invalid '${tag}' value`,
|
|
151
|
+
previewValue: json,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import type { ActionCall } from "../action/applyAction"
|
|
2
|
+
import { resolvePathCheckingIds } from "../parent/path"
|
|
3
|
+
import { getStandaloneAction } from "../standardActions/actions"
|
|
4
|
+
import { failure, hasOwnProp, isArray, isObject, isPlainObject } from "../utils"
|
|
5
|
+
import { runWithNewErrorDiagnosticsContext, withErrorPathSegment } from "../utils/errorDiagnostics"
|
|
6
|
+
import { builtinSerializers } from "./builtinSerializers"
|
|
7
|
+
import type { Call, EncodedCall, ResolvedCall } from "./Call"
|
|
8
|
+
import { CallError, CallErrorCode } from "./CallError"
|
|
9
|
+
import {
|
|
10
|
+
createDecodeContext,
|
|
11
|
+
createEncodeContext,
|
|
12
|
+
escapedObjectTag,
|
|
13
|
+
groupSerializersByTypeof,
|
|
14
|
+
isUnmarkedSerializer,
|
|
15
|
+
markValue,
|
|
16
|
+
type SerializerConfig,
|
|
17
|
+
wrapSerializerError,
|
|
18
|
+
} from "./codec"
|
|
19
|
+
import type { JsonValue } from "./JsonValue"
|
|
20
|
+
import { decodePathRef, encodePathRef, pathRefs, pathRefTag } from "./references"
|
|
21
|
+
import {
|
|
22
|
+
assertIsCallShape,
|
|
23
|
+
assertIsTargetInTree,
|
|
24
|
+
assertIsTreeRoot,
|
|
25
|
+
createResolvedCall,
|
|
26
|
+
resolveCallNameOrThrow,
|
|
27
|
+
} from "./resolvedCall"
|
|
28
|
+
import type { DecodeContext, EncodeContext, ValueSerializer } from "./ValueSerializer"
|
|
29
|
+
import { serializationMarkerKey } from "./ValueSerializer"
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Encodes and decodes values and calls against a tree, with a list of value serializers.
|
|
33
|
+
* Created with `createSerializer`.
|
|
34
|
+
*
|
|
35
|
+
* Every operation takes the root of the tree as its first argument; it must be a tree root.
|
|
36
|
+
*/
|
|
37
|
+
export interface Serializer {
|
|
38
|
+
/**
|
|
39
|
+
* The list of value serializers it was created with, without the built-ins.
|
|
40
|
+
*/
|
|
41
|
+
readonly serializers: readonly ValueSerializer[]
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Encodes a value as JSON. Nodes of the tree go as references when a reference serializer in
|
|
45
|
+
* the list claims them.
|
|
46
|
+
*
|
|
47
|
+
* @param root Root of the tree.
|
|
48
|
+
* @param value Value to encode.
|
|
49
|
+
* @returns The encoded value.
|
|
50
|
+
*/
|
|
51
|
+
encodeValue(root: object, value: unknown): JsonValue
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Decodes a value encoded with `encodeValue`. References are resolved against the tree;
|
|
55
|
+
* snapshots become new instances.
|
|
56
|
+
*
|
|
57
|
+
* @param root Root of the tree.
|
|
58
|
+
* @param json Value to decode.
|
|
59
|
+
* @returns The decoded value.
|
|
60
|
+
*/
|
|
61
|
+
decodeValue(root: object, json: JsonValue): unknown
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Encodes a call (an `ActionCall`, e.g. from `onActionMiddleware`'s `onStart`, or a `Call`)
|
|
65
|
+
* as JSON. The target always goes as a reference; for a standalone action, it isn't repeated
|
|
66
|
+
* in the arguments.
|
|
67
|
+
*
|
|
68
|
+
* Encode an `ActionCall` before the action runs (e.g. in `onStart`), so its arguments are
|
|
69
|
+
* encoded against the tree the receiver has.
|
|
70
|
+
*
|
|
71
|
+
* @param root Root of the tree.
|
|
72
|
+
* @param call The call.
|
|
73
|
+
* @returns The encoded call.
|
|
74
|
+
*/
|
|
75
|
+
encodeCall(root: object, call: ActionCall | Call): EncodedCall
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Decodes a call encoded with `encodeCall`: the target is resolved and the name resolved on
|
|
79
|
+
* it, nothing is run. The arguments are decoded when `args` is first read.
|
|
80
|
+
*
|
|
81
|
+
* @param root Root of the tree.
|
|
82
|
+
* @param json The encoded call.
|
|
83
|
+
* @returns The resolved call, to pass to `applyCall` or `applyCallAsync`.
|
|
84
|
+
*/
|
|
85
|
+
decodeCall(root: object, json: EncodedCall | JsonValue): ResolvedCall
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Options of `createSerializer`.
|
|
90
|
+
*/
|
|
91
|
+
export interface SerializerOptions {
|
|
92
|
+
/**
|
|
93
|
+
* Value serializers, tried in order before the built-ins when encoding.
|
|
94
|
+
*/
|
|
95
|
+
serializers?: readonly ValueSerializer[]
|
|
96
|
+
/**
|
|
97
|
+
* Whether to append the built-in serializers (`builtinSerializers`) to the list.
|
|
98
|
+
* Defaults to `true`.
|
|
99
|
+
*/
|
|
100
|
+
builtins?: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertIsValueSerializer(serializer: unknown, index: number): void {
|
|
104
|
+
if (isObject(serializer) && typeof serializer.encodeValue === "function") {
|
|
105
|
+
throw failure(
|
|
106
|
+
`serializers[${index}] is a serializer; to extend one, spread its '.serializers' into the list`
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
!isObject(serializer) ||
|
|
111
|
+
typeof serializer.tag !== "string" ||
|
|
112
|
+
serializer.tag === "" ||
|
|
113
|
+
typeof serializer.is !== "function" ||
|
|
114
|
+
typeof serializer.encode !== "function" ||
|
|
115
|
+
typeof serializer.decode !== "function" ||
|
|
116
|
+
(serializer.claimsTarget !== undefined && typeof serializer.claimsTarget !== "function")
|
|
117
|
+
) {
|
|
118
|
+
throw failure(
|
|
119
|
+
`serializers[${index}] must be a value serializer ({ tag, is, encode, decode, claimsTarget? }) with a non-empty tag`
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isActionCall(call: ActionCall | Call): call is ActionCall {
|
|
125
|
+
return hasOwnProp(call, "actionName")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates a serializer from a list of value serializers. When encoding, a value goes through the
|
|
130
|
+
* first serializer whose `is` accepts it, then the built-ins (unless `builtins` is `false`),
|
|
131
|
+
* then as plain JSON; decoding picks the serializer by tag.
|
|
132
|
+
*
|
|
133
|
+
* @param options Options.
|
|
134
|
+
* @returns The serializer.
|
|
135
|
+
*/
|
|
136
|
+
export function createSerializer(options: SerializerOptions = {}): Serializer {
|
|
137
|
+
const custom = options.serializers ? [...options.serializers] : []
|
|
138
|
+
custom.forEach(assertIsValueSerializer)
|
|
139
|
+
|
|
140
|
+
const serializers: readonly ValueSerializer[] =
|
|
141
|
+
options.builtins === false
|
|
142
|
+
? custom
|
|
143
|
+
: [
|
|
144
|
+
...custom,
|
|
145
|
+
...Object.values<ValueSerializer>(builtinSerializers).filter((s) => !custom.includes(s)),
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
const byTag = new Map<string, ValueSerializer>()
|
|
149
|
+
for (const serializer of serializers) {
|
|
150
|
+
if (serializer.tag === escapedObjectTag) {
|
|
151
|
+
throw failure(`the tag '${escapedObjectTag}' is reserved`)
|
|
152
|
+
}
|
|
153
|
+
if (byTag.has(serializer.tag)) {
|
|
154
|
+
throw failure(`there is more than one serializer with tag '${serializer.tag}'`)
|
|
155
|
+
}
|
|
156
|
+
byTag.set(serializer.tag, serializer)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const config: SerializerConfig = {
|
|
160
|
+
serializers,
|
|
161
|
+
byTypeof: groupSerializersByTypeof(serializers),
|
|
162
|
+
byTag,
|
|
163
|
+
unmarked: serializers.find(isUnmarkedSerializer),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const encodeTarget = (target: unknown, ctx: EncodeContext): JsonValue => {
|
|
167
|
+
assertIsTargetInTree(target, ctx.root)
|
|
168
|
+
// only reference serializers are asked, through `claimsTarget`, which ignores `when` since
|
|
169
|
+
// the call must run on the node itself
|
|
170
|
+
for (const serializer of serializers) {
|
|
171
|
+
let payload: JsonValue | undefined
|
|
172
|
+
try {
|
|
173
|
+
if (!serializer.claimsTarget?.(target, ctx)) {
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
payload = serializer.encode(target, ctx)
|
|
177
|
+
} catch (e) {
|
|
178
|
+
throw wrapSerializerError(e, CallErrorCode.CannotEncode, serializer.tag, target)
|
|
179
|
+
}
|
|
180
|
+
return markValue(serializer.tag, payload)
|
|
181
|
+
}
|
|
182
|
+
return markValue(pathRefTag, encodePathRef(target))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const decodeTarget = (json: unknown, ctx: DecodeContext): object => {
|
|
186
|
+
const tag = isPlainObject(json) ? json[serializationMarkerKey] : undefined
|
|
187
|
+
const serializer = typeof tag === "string" ? byTag.get(tag) : undefined
|
|
188
|
+
|
|
189
|
+
let target: unknown
|
|
190
|
+
if (tag === pathRefTag && !serializer) {
|
|
191
|
+
// the fallback every serializer decodes in a target position
|
|
192
|
+
target = decodePathRef((json as Record<string, unknown>).v, ctx.root)
|
|
193
|
+
} else if (serializer?.claimsTarget) {
|
|
194
|
+
target = ctx.decode(json as JsonValue)
|
|
195
|
+
} else {
|
|
196
|
+
throw new CallError({
|
|
197
|
+
code: CallErrorCode.BadArgument,
|
|
198
|
+
message: "the call target must be a reference",
|
|
199
|
+
previewValue: json,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
assertIsTargetInTree(target, ctx.root)
|
|
204
|
+
return target
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
serializers: Object.freeze(custom),
|
|
209
|
+
|
|
210
|
+
encodeValue(root, value) {
|
|
211
|
+
assertIsTreeRoot(root)
|
|
212
|
+
return runWithNewErrorDiagnosticsContext(() =>
|
|
213
|
+
createEncodeContext(config, root).encode(value)
|
|
214
|
+
)
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
decodeValue(root, json) {
|
|
218
|
+
assertIsTreeRoot(root)
|
|
219
|
+
return runWithNewErrorDiagnosticsContext(() => createDecodeContext(config, root).decode(json))
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
encodeCall(root, call) {
|
|
223
|
+
assertIsTreeRoot(root)
|
|
224
|
+
if (!isObject(call)) {
|
|
225
|
+
throw failure("call must be an object")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return runWithNewErrorDiagnosticsContext(() => {
|
|
229
|
+
let target: unknown
|
|
230
|
+
let name: string
|
|
231
|
+
let args: readonly unknown[] | undefined
|
|
232
|
+
|
|
233
|
+
if (isActionCall(call)) {
|
|
234
|
+
if (call.serialized) {
|
|
235
|
+
throw failure("cannot encode a serialized action call; deserialize it first")
|
|
236
|
+
}
|
|
237
|
+
const { value, resolved } = resolvePathCheckingIds(
|
|
238
|
+
root,
|
|
239
|
+
call.targetPath,
|
|
240
|
+
call.targetPathIds
|
|
241
|
+
)
|
|
242
|
+
if (!resolved) {
|
|
243
|
+
throw new CallError({
|
|
244
|
+
code: CallErrorCode.NotFound,
|
|
245
|
+
message: `object at path ${JSON.stringify(call.targetPath)} with ids ${JSON.stringify(
|
|
246
|
+
call.targetPathIds
|
|
247
|
+
)} could not be resolved`,
|
|
248
|
+
path: ["target"],
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
target = value
|
|
252
|
+
name = call.actionName
|
|
253
|
+
// the target of a standalone action is its first argument, sent once as the target
|
|
254
|
+
args = getStandaloneAction(name) ? call.args.slice(1) : call.args
|
|
255
|
+
} else {
|
|
256
|
+
assertIsCallShape(call)
|
|
257
|
+
target = call.target
|
|
258
|
+
name = call.name
|
|
259
|
+
args = call.args
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const ctx = createEncodeContext(config, root)
|
|
263
|
+
const encoded: EncodedCall = {
|
|
264
|
+
target: withErrorPathSegment("target", () => encodeTarget(target, ctx)),
|
|
265
|
+
name,
|
|
266
|
+
}
|
|
267
|
+
if (args !== undefined) {
|
|
268
|
+
encoded.args = withErrorPathSegment("args", () =>
|
|
269
|
+
Array.from(args, (arg, i) => ctx.encode(arg, i))
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
return encoded
|
|
273
|
+
})
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
decodeCall(root, json) {
|
|
277
|
+
assertIsTreeRoot(root)
|
|
278
|
+
|
|
279
|
+
return runWithNewErrorDiagnosticsContext(() => {
|
|
280
|
+
if (
|
|
281
|
+
!isPlainObject(json) ||
|
|
282
|
+
!hasOwnProp(json, "target") ||
|
|
283
|
+
typeof json.name !== "string" ||
|
|
284
|
+
(json.args !== undefined && !isArray(json.args))
|
|
285
|
+
) {
|
|
286
|
+
throw new CallError({
|
|
287
|
+
code: CallErrorCode.BadArgument,
|
|
288
|
+
message: "invalid call: expected { target, name, args? }",
|
|
289
|
+
previewValue: json,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const name = json.name
|
|
294
|
+
const rawArgs = json.args as JsonValue[] | undefined
|
|
295
|
+
const ctx = createDecodeContext(config, root)
|
|
296
|
+
|
|
297
|
+
const target = withErrorPathSegment("target", () => decodeTarget(json.target, ctx))
|
|
298
|
+
const resolution = resolveCallNameOrThrow(target, name)
|
|
299
|
+
|
|
300
|
+
return createResolvedCall(
|
|
301
|
+
target,
|
|
302
|
+
name,
|
|
303
|
+
resolution,
|
|
304
|
+
rawArgs && {
|
|
305
|
+
rawArgs,
|
|
306
|
+
decode: () =>
|
|
307
|
+
runWithNewErrorDiagnosticsContext(() =>
|
|
308
|
+
withErrorPathSegment("args", () => rawArgs.map((arg, i) => ctx.decode(arg, i)))
|
|
309
|
+
),
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
})
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The default serializer: nodes of the tree as path references (`pathRefs()`, value-type models
|
|
319
|
+
* excepted), plus the built-ins.
|
|
320
|
+
*/
|
|
321
|
+
export const defaultSerializer: Serializer = createSerializer({ serializers: [pathRefs()] })
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
* Decodes a nested value with the same serializer.
|
|
40
|
+
*
|
|
41
|
+
* @param json Value to decode.
|
|
42
|
+
* @param key Where the value is inside its parent, for error paths.
|
|
43
|
+
* @returns The decoded value.
|
|
44
|
+
*/
|
|
45
|
+
decode(json: JsonValue, key?: PathElement): unknown
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Handles one kind of value: encodes it as `{ "$mobxKeystone": tag, "v": payload }` and decodes
|
|
50
|
+
* it back. `createSerializer` composes a list of them into a `Serializer`.
|
|
51
|
+
*
|
|
52
|
+
* @template T Type of the values it handles.
|
|
53
|
+
* @template J Type of the encoded payload (`undefined` to carry none).
|
|
54
|
+
*/
|
|
55
|
+
export interface ValueSerializer<T = any, J extends JsonValue | undefined = any> {
|
|
56
|
+
/**
|
|
57
|
+
* Unique, e.g. `"geom/Polygon"`; the `$mobxKeystone` of the encoded value.
|
|
58
|
+
*/
|
|
59
|
+
readonly tag: string
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether this serializer encodes `value`.
|
|
63
|
+
*
|
|
64
|
+
* @param value Value to check.
|
|
65
|
+
* @param ctx Encoding context; `ctx.root` is the tree the value is encoded against.
|
|
66
|
+
*/
|
|
67
|
+
is(value: unknown, ctx: EncodeContext): value is T
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Encodes a value `is` accepted. Use `ctx.encode(child)` for nested values.
|
|
71
|
+
*
|
|
72
|
+
* @param value Value to encode.
|
|
73
|
+
* @param ctx Encoding context.
|
|
74
|
+
* @returns The payload (`v`), or `undefined` to carry none.
|
|
75
|
+
*/
|
|
76
|
+
encode(value: T, ctx: EncodeContext): J
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Decodes a payload back into a value. Use `ctx.decode(child)` for nested values.
|
|
80
|
+
*
|
|
81
|
+
* @param json The payload (`v`).
|
|
82
|
+
* @param ctx Decoding context.
|
|
83
|
+
* @returns The decoded value.
|
|
84
|
+
*/
|
|
85
|
+
decode(json: J, ctx: DecodeContext): T
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Only for reference serializers: whether this serializer references `value`, a node of the
|
|
89
|
+
* tree, when it is a call's target. Unlike `is`, it ignores options such as `when`, since the
|
|
90
|
+
* call must run on the node itself. A call's target goes through the first serializer whose
|
|
91
|
+
* `claimsTarget` accepts it (else as a `pathRef`), and only serializers with `claimsTarget`
|
|
92
|
+
* decode it.
|
|
93
|
+
*
|
|
94
|
+
* @param value The call's target.
|
|
95
|
+
* @param ctx Encoding context.
|
|
96
|
+
*/
|
|
97
|
+
claimsTarget?(value: object, ctx: EncodeContext): boolean
|
|
98
|
+
}
|