mobx-keystone 2.3.1 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/mobx-keystone.esm.js +5157 -3981
  3. package/dist/mobx-keystone.esm.mjs +5157 -3981
  4. package/dist/mobx-keystone.umd.js +5170 -3980
  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 +84 -0
  18. package/dist/types/serialization/ValueSerializer.d.ts +88 -0
  19. package/dist/types/serialization/applyCall.d.ts +54 -0
  20. package/dist/types/serialization/builtinSerializers.d.ts +49 -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 +321 -0
  43. package/src/serialization/ValueSerializer.ts +98 -0
  44. package/src/serialization/applyCall.ts +190 -0
  45. package/src/serialization/builtinSerializers.ts +261 -0
  46. package/src/serialization/codec.ts +338 -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
@@ -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
+ }
@@ -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,261 @@
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
+ * Marks a serializer whose encoded value is written as it is, without the marker
18
+ * (snapshots already mark themselves).
19
+ */
20
+ export const unmarkedEncodingSymbol = Symbol("unmarkedEncoding")
21
+
22
+ const snapshotSerializer: ValueSerializer<object, any> = {
23
+ tag: "snapshot",
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) {
34
+ return fromSnapshot(json)
35
+ },
36
+ }
37
+ ;(snapshotSerializer as unknown as Record<symbol, boolean>)[unmarkedEncodingSymbol] = true
38
+
39
+ const dataModelSerializer: ValueSerializer<AnyDataModel, { type: string; data: JsonValue }> = {
40
+ tag: "dataModel",
41
+
42
+ is(value): value is AnyDataModel {
43
+ return isDataModel(value)
44
+ },
45
+
46
+ encode(value, ctx) {
47
+ // data models can only be created from registered classes
48
+ const modelInfo = findModelInfoForPrototype(Object.getPrototypeOf(value))!
49
+ return { type: modelInfo.name, data: ctx.encode(value.$) }
50
+ },
51
+
52
+ decode(json, ctx) {
53
+ if (!isPlainObject(json) || typeof json.type !== "string" || !hasOwnProp(json, "data")) {
54
+ throw badPayloadError("dataModel", json)
55
+ }
56
+ const modelInfo = getModelInfoForName(json.type)
57
+ if (!modelInfo || !isDataModelClass(modelInfo.class)) {
58
+ throw new CallError({
59
+ code: CallErrorCode.BadArgument,
60
+ message: `no data model is registered as '${json.type}'`,
61
+ previewValue: json,
62
+ })
63
+ }
64
+ return new modelInfo.class(ctx.decode(json.data))
65
+ },
66
+ }
67
+
68
+ const dateSerializer: ValueSerializer<Date, string | number | null> = {
69
+ tag: "date",
70
+
71
+ is(value): value is Date {
72
+ return value instanceof Date
73
+ },
74
+
75
+ encode(value) {
76
+ return Number.isNaN(value.getTime()) ? null : value.toISOString()
77
+ },
78
+
79
+ decode(json) {
80
+ if (json === null) {
81
+ return new Date(Number.NaN)
82
+ }
83
+ const date = typeof json === "number" || typeof json === "string" ? new Date(json) : undefined
84
+ if (!date || Number.isNaN(date.getTime())) {
85
+ throw badPayloadError("date", json)
86
+ }
87
+ return date
88
+ },
89
+ }
90
+
91
+ const mapSerializer: ValueSerializer<Map<unknown, unknown>, [JsonValue, JsonValue][]> = {
92
+ tag: "map",
93
+
94
+ is(value): value is Map<unknown, unknown> {
95
+ return isMap(value)
96
+ },
97
+
98
+ encode(value, ctx) {
99
+ return Array.from(value.entries(), ([k, v], i): [JsonValue, JsonValue] => [
100
+ ctx.encode(k, i),
101
+ ctx.encode(v, i),
102
+ ])
103
+ },
104
+
105
+ decode(json, ctx) {
106
+ if (!isArray(json)) {
107
+ throw badPayloadError("map", json)
108
+ }
109
+ const map = new Map<unknown, unknown>()
110
+ json.forEach((entry, i) => {
111
+ if (!isArray(entry) || entry.length !== 2) {
112
+ throw badPayloadError("map", json)
113
+ }
114
+ map.set(ctx.decode(entry[0], i), ctx.decode(entry[1], i))
115
+ })
116
+ return map
117
+ },
118
+ }
119
+
120
+ const setSerializer: ValueSerializer<Set<unknown>, JsonValue[]> = {
121
+ tag: "set",
122
+
123
+ is(value): value is Set<unknown> {
124
+ return isSet(value)
125
+ },
126
+
127
+ encode(value, ctx) {
128
+ return Array.from(value.values(), (v, i) => ctx.encode(v, i))
129
+ },
130
+
131
+ decode(json, ctx) {
132
+ if (!isArray(json)) {
133
+ throw badPayloadError("set", json)
134
+ }
135
+ return new Set(json.map((v, i) => ctx.decode(v, i)))
136
+ },
137
+ }
138
+
139
+ const nonFiniteNumbers: Record<string, number> = {
140
+ NaN: Number.NaN,
141
+ Infinity: Number.POSITIVE_INFINITY,
142
+ "-Infinity": Number.NEGATIVE_INFINITY,
143
+ }
144
+
145
+ const numberSerializer: ValueSerializer<number, string> = {
146
+ tag: "number",
147
+
148
+ is(value): value is number {
149
+ return typeof value === "number" && !Number.isFinite(value)
150
+ },
151
+
152
+ encode(value) {
153
+ return String(value)
154
+ },
155
+
156
+ decode(json) {
157
+ if (typeof json !== "string" || !hasOwnProp(nonFiniteNumbers, json)) {
158
+ throw badPayloadError("number", json)
159
+ }
160
+ return nonFiniteNumbers[json]
161
+ },
162
+ }
163
+
164
+ const bigintSerializer: ValueSerializer<bigint, string> = {
165
+ tag: "bigint",
166
+
167
+ is(value): value is bigint {
168
+ return typeof value === "bigint"
169
+ },
170
+
171
+ encode(value) {
172
+ return value.toString()
173
+ },
174
+
175
+ decode(json) {
176
+ if (typeof json !== "string" || !/^-?\d+$/.test(json)) {
177
+ throw badPayloadError("bigint", json)
178
+ }
179
+ return BigInt(json)
180
+ },
181
+ }
182
+
183
+ const undefinedSerializer: ValueSerializer<undefined, undefined> = {
184
+ tag: "undefined",
185
+
186
+ is(value): value is undefined {
187
+ return value === undefined
188
+ },
189
+
190
+ encode() {
191
+ return undefined
192
+ },
193
+
194
+ decode() {
195
+ return undefined
196
+ },
197
+ }
198
+
199
+ /**
200
+ * 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.
202
+ * `createSerializer({ serializers: [builtinSerializers.date], builtins: false })`.
203
+ */
204
+ export const builtinSerializers = Object.freeze({
205
+ /**
206
+ * Models and frozen data that aren't references, as their snapshots (unmarked, since snapshots
207
+ * mark themselves with `$modelType` / `$frozen`), decoded with `fromSnapshot` into new
208
+ * instances. Tree arrays and plain objects that aren't references are walked like any other.
209
+ */
210
+ snapshot: snapshotSerializer,
211
+ /**
212
+ * Data model instances, as their registered name and their data (encoded, so data of the tree
213
+ * goes as a reference), decoded as a new instance over the decoded data.
214
+ */
215
+ dataModel: dataModelSerializer,
216
+ /**
217
+ * Dates, as `toISOString()` (an invalid date as `null`); decodes an ISO string, a timestamp or
218
+ * `null`.
219
+ */
220
+ date: dateSerializer,
221
+ /**
222
+ * Maps (plain or observable), as an array of `[key, value]` entries; decoded as plain maps.
223
+ */
224
+ map: mapSerializer,
225
+ /**
226
+ * Sets (plain or observable), as an array of their values; decoded as plain sets.
227
+ */
228
+ set: setSerializer,
229
+ /**
230
+ * The numbers JSON can't hold: `"NaN"`, `"Infinity"`, `"-Infinity"`.
231
+ */
232
+ number: numberSerializer,
233
+ /**
234
+ * Bigints, as a decimal string, with no loss of precision.
235
+ */
236
+ bigint: bigintSerializer,
237
+ /**
238
+ * `undefined` (as an array item, an argument or an object property, which keeps its key).
239
+ */
240
+ undefined: undefinedSerializer,
241
+ })
242
+
243
+ /**
244
+ * @internal
245
+ *
246
+ * The only `typeof` of the values each built-in encodes, so encoding can skip the built-ins that
247
+ * can't apply to a value.
248
+ */
249
+ export const builtinEncodedTypeof: ReadonlyMap<ValueSerializer, TypeofResult> = new Map<
250
+ ValueSerializer,
251
+ TypeofResult
252
+ >([
253
+ [snapshotSerializer, "object"],
254
+ [dataModelSerializer, "object"],
255
+ [dateSerializer, "object"],
256
+ [mapSerializer, "object"],
257
+ [setSerializer, "object"],
258
+ [numberSerializer, "number"],
259
+ [bigintSerializer, "bigint"],
260
+ [undefinedSerializer, "undefined"],
261
+ ])