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.
Files changed (51) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/mobx-keystone.esm.js +3604 -2414
  3. package/dist/mobx-keystone.esm.mjs +3604 -2414
  4. package/dist/mobx-keystone.umd.js +3685 -2481
  5. package/dist/types/action/CallKind.d.ts +29 -0
  6. package/dist/types/action/applyAction.d.ts +7 -1
  7. package/dist/types/action/index.d.ts +1 -0
  8. package/dist/types/action/resolveCallName.d.ts +1 -0
  9. package/dist/types/actionMiddlewares/actionSerialization/actionSerialization.d.ts +14 -0
  10. package/dist/types/actionMiddlewares/actionSerialization/applySerializedAction.d.ts +6 -0
  11. package/dist/types/actionMiddlewares/actionSerialization/core.d.ts +7 -0
  12. package/dist/types/index.d.ts +1 -0
  13. package/dist/types/patch/emitPatch.d.ts +6 -0
  14. package/dist/types/serialization/Call.d.ts +55 -0
  15. package/dist/types/serialization/CallError.d.ts +96 -0
  16. package/dist/types/serialization/JsonValue.d.ts +6 -0
  17. package/dist/types/serialization/Serializer.d.ts +98 -0
  18. package/dist/types/serialization/ValueSerializer.d.ts +93 -0
  19. package/dist/types/serialization/applyCall.d.ts +54 -0
  20. package/dist/types/serialization/builtinSerializers.d.ts +53 -0
  21. package/dist/types/serialization/codec.d.ts +1 -0
  22. package/dist/types/serialization/index.d.ts +8 -0
  23. package/dist/types/serialization/references.d.ts +74 -0
  24. package/dist/types/serialization/resolvedCall.d.ts +1 -0
  25. package/package.json +1 -1
  26. package/src/action/CallKind.ts +29 -0
  27. package/src/action/applyAction.ts +22 -49
  28. package/src/action/applySet.ts +21 -1
  29. package/src/action/index.ts +1 -0
  30. package/src/action/resolveCallName.ts +207 -0
  31. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +14 -0
  32. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +6 -0
  33. package/src/actionMiddlewares/actionSerialization/core.ts +7 -0
  34. package/src/actionMiddlewares/actionSerialization/index.ts +2 -0
  35. package/src/actionMiddlewares/actionSerialization/objectPathSerializer.ts +3 -2
  36. package/src/index.ts +1 -0
  37. package/src/parent/path.ts +45 -12
  38. package/src/patch/emitPatch.ts +6 -0
  39. package/src/serialization/Call.ts +58 -0
  40. package/src/serialization/CallError.ts +153 -0
  41. package/src/serialization/JsonValue.ts +12 -0
  42. package/src/serialization/Serializer.ts +342 -0
  43. package/src/serialization/ValueSerializer.ts +104 -0
  44. package/src/serialization/applyCall.ts +190 -0
  45. package/src/serialization/builtinSerializers.ts +264 -0
  46. package/src/serialization/codec.ts +360 -0
  47. package/src/serialization/index.ts +15 -0
  48. package/src/serialization/references.ts +225 -0
  49. package/src/serialization/resolvedCall.ts +169 -0
  50. package/src/standardActions/actions.ts +11 -3
  51. package/src/utils/errorDiagnostics.ts +9 -4
@@ -256,19 +256,20 @@ export function resolvePath<T = any>(
256
256
 
257
257
  const p = path[i]
258
258
 
259
- // check just to avoid mobx warnings about trying to access out of bounds index
260
- if (isArray(current) && +p >= current.length) {
261
- return unresolved
262
- }
263
-
264
- if (isModel(current)) {
259
+ if (isArray(current)) {
260
+ if (!isArrayIndexOf(current, p)) {
261
+ return unresolved
262
+ }
263
+ } else if (isModel(current)) {
265
264
  const dataNode = modelToDataNode(current)
266
- if (p in dataNode) {
265
+ if (hasOwnProp(dataNode, p)) {
267
266
  current = dataNode
267
+ } else if (!hasOwnProp(current, p) && !isInheritedGetter(current, p)) {
268
+ // the model's own fields and getters (e.g. computed trees) resolve,
269
+ // but not its methods or anything inherited from Object.prototype
270
+ return unresolved
268
271
  }
269
- }
270
-
271
- if (!(p in current)) {
272
+ } else if (!hasOwnProp(current, p)) {
272
273
  return unresolved
273
274
  }
274
275
 
@@ -278,6 +279,39 @@ export function resolvePath<T = any>(
278
279
  return { resolved: true, value: current }
279
280
  }
280
281
 
282
+ /**
283
+ * @internal
284
+ *
285
+ * Whether a value can be a path element (a property name or an array index).
286
+ */
287
+ export function isPathElement(value: unknown): value is PathElement {
288
+ return typeof value === "string" || typeof value === "number"
289
+ }
290
+
291
+ /**
292
+ * Whether a path element is an index of the array, so non-index props such as 'length' never
293
+ * resolve. It also avoids mobx warnings about trying to access out of bounds indexes.
294
+ */
295
+ function isArrayIndexOf(array: readonly unknown[], p: PathElement): boolean {
296
+ const index = +p
297
+ return Number.isInteger(index) && index >= 0 && index < array.length && p in array
298
+ }
299
+
300
+ /**
301
+ * Whether a getter is defined for a prop by the prototype chain of an object, below `Object.prototype`.
302
+ */
303
+ function isInheritedGetter(obj: object, p: PathElement): boolean {
304
+ let proto = Object.getPrototypeOf(obj)
305
+ while (proto && proto !== Object.prototype) {
306
+ const descriptor = Object.getOwnPropertyDescriptor(proto, p)
307
+ if (descriptor) {
308
+ return descriptor.get !== undefined
309
+ }
310
+ proto = Object.getPrototypeOf(proto)
311
+ }
312
+ return false
313
+ }
314
+
281
315
  /**
282
316
  * @internal
283
317
  */
@@ -321,9 +355,8 @@ export function resolvePathCheckingIds<T = any>(
321
355
 
322
356
  const p = path[i]
323
357
 
324
- // check just to avoid mobx warnings about trying to access out of bounds index
325
358
  if (isArray(current)) {
326
- if (+p >= current.length || !(p in current)) {
359
+ if (!isArrayIndexOf(current, p)) {
327
360
  return { resolved: false }
328
361
  }
329
362
  } else if (!hasOwnProp(current, p)) {
@@ -148,6 +148,9 @@ export function hasPatchListenersFor(target: object): boolean {
148
148
  /**
149
149
  * Adds a listener that will be called every time a patch is generated for the tree of the given target object.
150
150
  *
151
+ * The listener runs inside the action that made the change, so `getCurrentActionContext()` returns
152
+ * that action's context: its `rootContext` is the top-level action (for a flow step, the flow).
153
+ *
151
154
  * @param subtreeRoot Subtree root object of the patch listener.
152
155
  * @param listener The listener function that will be called everytime a patch is generated for the object or its children.
153
156
  * @returns A disposer to stop listening to patches.
@@ -190,6 +193,9 @@ export function internalOnPatches(
190
193
  * Adds a listener that will be called every time a patch is generated anywhere.
191
194
  * Usually prefer using `onPatches`.
192
195
  *
196
+ * The listener runs inside the action that made the change, so `getCurrentActionContext()` returns
197
+ * that action's context: its `rootContext` is the top-level action (for a flow step, the flow).
198
+ *
193
199
  * @param listener The listener function that will be called everytime a patch is generated anywhere.
194
200
  * @returns A disposer to stop listening to patches.
195
201
  */
@@ -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,12 @@
1
+ /**
2
+ * A JSON value: what `JSON.parse` can return.
3
+ */
4
+ export type JsonValue =
5
+ | null
6
+ | boolean
7
+ | number
8
+ | string
9
+ | JsonValue[]
10
+ | {
11
+ [key: string]: JsonValue
12
+ }
@@ -0,0 +1,342 @@
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
+ * @param options Options.
60
+ * @returns The decoded value.
61
+ */
62
+ decodeValue(root: object, json: JsonValue, options?: DecodeOptions): unknown
63
+
64
+ /**
65
+ * Encodes a call (an `ActionCall`, e.g. from `onActionMiddleware`'s `onStart`, or a `Call`)
66
+ * as JSON. The target always goes as a reference; for a standalone action, it isn't repeated
67
+ * in the arguments.
68
+ *
69
+ * Encode an `ActionCall` before the action runs (e.g. in `onStart`), so its arguments are
70
+ * encoded against the tree the receiver has.
71
+ *
72
+ * @param root Root of the tree.
73
+ * @param call The call.
74
+ * @returns The encoded call.
75
+ */
76
+ encodeCall(root: object, call: ActionCall | Call): EncodedCall
77
+
78
+ /**
79
+ * Decodes a call encoded with `encodeCall`: the target is resolved and the name resolved on
80
+ * it, nothing is run. The arguments are decoded when `args` is first read.
81
+ *
82
+ * @param root Root of the tree.
83
+ * @param json The encoded call.
84
+ * @param options Options, applied when the arguments are decoded.
85
+ * @returns The resolved call, to pass to `applyCall` or `applyCallAsync`.
86
+ */
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
101
+ }
102
+
103
+ /**
104
+ * Options of `createSerializer`.
105
+ */
106
+ export interface SerializerOptions {
107
+ /**
108
+ * Value serializers, tried in order before the built-ins when encoding.
109
+ */
110
+ serializers?: readonly ValueSerializer[]
111
+ /**
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`.
114
+ */
115
+ builtins?: boolean
116
+ }
117
+
118
+ function assertIsValueSerializer(serializer: unknown, index: number): void {
119
+ if (isObject(serializer) && typeof serializer.encodeValue === "function") {
120
+ throw failure(
121
+ `serializers[${index}] is a serializer; to extend one, spread its '.serializers' into the list`
122
+ )
123
+ }
124
+ if (
125
+ !isObject(serializer) ||
126
+ typeof serializer.tag !== "string" ||
127
+ serializer.tag === "" ||
128
+ typeof serializer.is !== "function" ||
129
+ typeof serializer.encode !== "function" ||
130
+ typeof serializer.decode !== "function" ||
131
+ (serializer.claimsTarget !== undefined && typeof serializer.claimsTarget !== "function")
132
+ ) {
133
+ throw failure(
134
+ `serializers[${index}] must be a value serializer ({ tag, is, encode, decode, claimsTarget? }) with a non-empty tag`
135
+ )
136
+ }
137
+ }
138
+
139
+ function isActionCall(call: ActionCall | Call): call is ActionCall {
140
+ return hasOwnProp(call, "actionName")
141
+ }
142
+
143
+ /**
144
+ * Creates a serializer from a list of value serializers. When encoding, a value goes through the
145
+ * first serializer whose `is` accepts it, then the built-ins (unless `builtins` is `false`),
146
+ * then as plain JSON; decoding picks the serializer by tag.
147
+ *
148
+ * @param options Options.
149
+ * @returns The serializer.
150
+ */
151
+ export function createSerializer(options: SerializerOptions = {}): Serializer {
152
+ const custom = options.serializers ? [...options.serializers] : []
153
+ custom.forEach(assertIsValueSerializer)
154
+
155
+ const customTags = new Set(custom.map((s) => s.tag))
156
+ const serializers: readonly ValueSerializer[] =
157
+ options.builtins === false
158
+ ? custom
159
+ : [
160
+ ...custom,
161
+ // a custom serializer replaces the built-in with its tag
162
+ ...Object.values<ValueSerializer>(builtinSerializers).filter(
163
+ (s) => !customTags.has(s.tag)
164
+ ),
165
+ ]
166
+
167
+ const byTag = new Map<string, ValueSerializer>()
168
+ for (const serializer of serializers) {
169
+ if (serializer.tag === escapedObjectTag) {
170
+ throw failure(`the tag '${escapedObjectTag}' is reserved`)
171
+ }
172
+ if (byTag.has(serializer.tag)) {
173
+ throw failure(`there is more than one serializer with tag '${serializer.tag}'`)
174
+ }
175
+ byTag.set(serializer.tag, serializer)
176
+ }
177
+
178
+ const config: SerializerConfig = {
179
+ serializers,
180
+ byTypeof: groupSerializersByTypeof(serializers),
181
+ byTag,
182
+ unmarked: serializers.find(isUnmarkedSerializer),
183
+ }
184
+
185
+ const encodeTarget = (target: unknown, ctx: EncodeContext): JsonValue => {
186
+ assertIsTargetInTree(target, ctx.root)
187
+ // only reference serializers are asked, through `claimsTarget`, which ignores `when` since
188
+ // the call must run on the node itself
189
+ for (const serializer of serializers) {
190
+ let payload: JsonValue | undefined
191
+ try {
192
+ if (!serializer.claimsTarget?.(target, ctx)) {
193
+ continue
194
+ }
195
+ payload = serializer.encode(target, ctx)
196
+ } catch (e) {
197
+ throw wrapSerializerError(e, CallErrorCode.CannotEncode, serializer.tag, target)
198
+ }
199
+ return markValue(serializer.tag, payload)
200
+ }
201
+ return markValue(pathRefTag, encodePathRef(target))
202
+ }
203
+
204
+ const decodeTarget = (json: unknown, ctx: DecodeContext): object => {
205
+ const tag = isPlainObject(json) ? json[serializationMarkerKey] : undefined
206
+ const serializer = typeof tag === "string" ? byTag.get(tag) : undefined
207
+
208
+ let target: unknown
209
+ if (tag === pathRefTag && !serializer) {
210
+ // the fallback every serializer decodes in a target position
211
+ target = decodePathRef((json as Record<string, unknown>).v, ctx.root)
212
+ } else if (serializer?.claimsTarget) {
213
+ target = ctx.decode(json as JsonValue)
214
+ } else {
215
+ throw new CallError({
216
+ code: CallErrorCode.BadArgument,
217
+ message: "the call target must be a reference",
218
+ previewValue: json,
219
+ })
220
+ }
221
+
222
+ assertIsTargetInTree(target, ctx.root)
223
+ return target
224
+ }
225
+
226
+ return {
227
+ serializers: Object.freeze(custom),
228
+
229
+ encodeValue(root, value) {
230
+ assertIsTreeRoot(root)
231
+ return runWithNewErrorDiagnosticsContext(() =>
232
+ createEncodeContext(config, root).encode(value)
233
+ )
234
+ },
235
+
236
+ decodeValue(root, json, options) {
237
+ assertIsTreeRoot(root)
238
+ return runWithNewErrorDiagnosticsContext(() =>
239
+ createDecodeContext(config, root, !!options?.generateNewIds).decode(json)
240
+ )
241
+ },
242
+
243
+ encodeCall(root, call) {
244
+ assertIsTreeRoot(root)
245
+ if (!isObject(call)) {
246
+ throw failure("call must be an object")
247
+ }
248
+
249
+ return runWithNewErrorDiagnosticsContext(() => {
250
+ let target: unknown
251
+ let name: string
252
+ let args: readonly unknown[] | undefined
253
+
254
+ if (isActionCall(call)) {
255
+ if (call.serialized) {
256
+ throw failure("cannot encode a serialized action call; deserialize it first")
257
+ }
258
+ const { value, resolved } = resolvePathCheckingIds(
259
+ root,
260
+ call.targetPath,
261
+ call.targetPathIds
262
+ )
263
+ if (!resolved) {
264
+ throw new CallError({
265
+ code: CallErrorCode.NotFound,
266
+ message: `object at path ${JSON.stringify(call.targetPath)} with ids ${JSON.stringify(
267
+ call.targetPathIds
268
+ )} could not be resolved`,
269
+ path: ["target"],
270
+ })
271
+ }
272
+ target = value
273
+ name = call.actionName
274
+ // the target of a standalone action is its first argument, sent once as the target
275
+ args = getStandaloneAction(name) ? call.args.slice(1) : call.args
276
+ } else {
277
+ assertIsCallShape(call)
278
+ target = call.target
279
+ name = call.name
280
+ args = call.args
281
+ }
282
+
283
+ const ctx = createEncodeContext(config, root)
284
+ const encoded: EncodedCall = {
285
+ target: withErrorPathSegment("target", () => encodeTarget(target, ctx)),
286
+ name,
287
+ }
288
+ if (args !== undefined) {
289
+ encoded.args = withErrorPathSegment("args", () =>
290
+ Array.from(args, (arg, i) => ctx.encode(arg, i))
291
+ )
292
+ }
293
+ return encoded
294
+ })
295
+ },
296
+
297
+ decodeCall(root, json, options) {
298
+ assertIsTreeRoot(root)
299
+
300
+ return runWithNewErrorDiagnosticsContext(() => {
301
+ if (
302
+ !isPlainObject(json) ||
303
+ !hasOwnProp(json, "target") ||
304
+ typeof json.name !== "string" ||
305
+ (json.args !== undefined && !isArray(json.args))
306
+ ) {
307
+ throw new CallError({
308
+ code: CallErrorCode.BadArgument,
309
+ message: "invalid call: expected { target, name, args? }",
310
+ previewValue: json,
311
+ })
312
+ }
313
+
314
+ const name = json.name
315
+ const rawArgs = json.args as JsonValue[] | undefined
316
+ const ctx = createDecodeContext(config, root, !!options?.generateNewIds)
317
+
318
+ const target = withErrorPathSegment("target", () => decodeTarget(json.target, ctx))
319
+ const resolution = resolveCallNameOrThrow(target, name)
320
+
321
+ return createResolvedCall(
322
+ target,
323
+ name,
324
+ resolution,
325
+ rawArgs && {
326
+ rawArgs,
327
+ decode: () =>
328
+ runWithNewErrorDiagnosticsContext(() =>
329
+ withErrorPathSegment("args", () => rawArgs.map((arg, i) => ctx.decode(arg, i)))
330
+ ),
331
+ }
332
+ )
333
+ })
334
+ },
335
+ }
336
+ }
337
+
338
+ /**
339
+ * The default serializer: nodes of the tree as path references (`pathRefs()`, value-type models
340
+ * excepted), plus the built-ins.
341
+ */
342
+ export const defaultSerializer: Serializer = createSerializer({ serializers: [pathRefs()] })