@jbrowse/mobx-state-tree 5.10.8 → 5.11.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/README.md CHANGED
@@ -1,27 +1,155 @@
1
1
  # @jbrowse/mobx-state-tree
2
2
 
3
- Fork of mobx-state-tree v5.4.2 for use in jbrowse
4
-
5
- ## Current list of changes
6
-
7
- - updated typescript
8
- - updated to import actual filepaths instead of node module resolution
9
- - reduced rollup config
10
- - removed process.env.NODE_ENV from code, instead exporting a setDevMode function
11
- - converted tests to vitest
12
- - remove unused dependencies
13
- - updated tslint to eslint and typescript-eslint
14
- - dual CJS/ESM build using main and module fields (pure ESM causes trouble when both are imported in vite)
15
- - added `types.resilient` - wraps a type so that instantiation errors are caught
16
- and a fallback type is used instead of crashing the entire state tree. This is
17
- useful for arrays or maps that may contain unknown or invalid entries (e.g.
18
- plugin types that are not installed). It takes a primary type, a fallback type,
19
- and a `createFallbackSnapshot` callback that receives the error and original
20
- snapshot. Note: `isValidSnapshot` always returns success for resilient types,
21
- meaning any value is considered a valid snapshot. Validation is deferred to
22
- instantiation time, where failures are caught and routed to the fallback
23
- - removed NonEmptyObject annotation (also at upstream in post v5 versions)
24
- - replaced `unique symbol` phantom properties (`$emptyObject`, `$stateTreeNodeType`) with
3
+ Fork of [mobx-state-tree](https://mobx-state-tree.js.org) v5.4.2 maintained for use in
4
+ [JBrowse 2](https://jbrowse.org). All upstream types and APIs are available — this page
5
+ documents only the additions and changes made in this fork.
6
+
7
+ ## Infrastructure changes
8
+
9
+ - Updated TypeScript and import paths to use explicit file extensions
10
+ - Replaced `process.env.NODE_ENV` with an exported `setDevMode()` function
11
+ - Converted tests from Jest to Vitest
12
+ - Replaced tslint with eslint + typescript-eslint
13
+ - Dual CJS/ESM build (avoids issues when both are imported in the same Vite build)
14
+ - Removed unused dependencies and the `NonEmptyObject` annotation
15
+ - Replaced `unique symbol` phantom properties (`$emptyObject`, `$stateTreeNodeType`) with
25
16
  exported string-keyed brand interfaces (`$EmptyObjectBrand`, `$__mstStateTreeNodeType__`),
26
- fixing tsgo TS4058/TS4023 errors when MST types propagate into exported function return types
27
- - add reflection api getChildType and getUnionSubtypes and getDefaultInstanceOrSnapshot
17
+ fixing tsgo TS4058/TS4023 errors when MST types appear in exported function return types
18
+
19
+ ## Added APIs
20
+
21
+ ### `types.resilient`
22
+
23
+ Wraps a type so that if instantiation fails (e.g. because the snapshot is from an unknown
24
+ plugin that is not installed), the error is caught and a fallback type is used instead of
25
+ crashing the entire state tree.
26
+
27
+ ```ts
28
+ const PluginWidget = types.resilient(
29
+ KnownWidget,
30
+ UnknownWidget,
31
+ (error, originalSnapshot) => ({ kind: "unknown", raw: originalSnapshot }),
32
+ )
33
+
34
+ // Arrays of plugins stay alive even if one entry is unrecognized
35
+ const Store = types.model({
36
+ widgets: types.array(PluginWidget),
37
+ })
38
+ ```
39
+
40
+ `types.resilient` takes three arguments:
41
+
42
+ - `type` — the primary type to try first
43
+ - `fallbackType` — the type to use when the primary fails
44
+ - `createFallbackSnapshot(error, originalSnapshot)` — a callback that receives the caught
45
+ error and the original snapshot, and returns a valid snapshot for the fallback type
46
+
47
+ **Important caveat:** `isValidSnapshot` always returns success for a resilient type. Any
48
+ value is considered a valid snapshot — validation is deferred to instantiation time, where
49
+ failures are caught and routed to the fallback.
50
+
51
+ ---
52
+
53
+ ### `types.stripDefault`
54
+
55
+ Like `types.optional`, but omits the property key from the parent model's snapshot entirely
56
+ when the value equals the default.
57
+
58
+ #### Background: snapshots and `types.optional`
59
+
60
+ In MST, a **snapshot** is the plain JSON representation of your model — what you'd get from
61
+ `getSnapshot(model)` and what you'd pass to `Model.create(...)`. Snapshots are used for
62
+ serialization, persistence, and undo/redo.
63
+
64
+ `types.optional` lets you define a property with a default value so it can be omitted when
65
+ creating a model:
66
+
67
+ ```ts
68
+ const Settings = types.model({
69
+ theme: types.optional(types.string, "light"),
70
+ })
71
+
72
+ const s = Settings.create() // theme is "light"
73
+ getSnapshot(s) // { theme: "light" }
74
+ ```
75
+
76
+ Even though `"light"` is just the default, it always appears in the snapshot. If you store
77
+ or diff these snapshots, every instance carries that redundant data.
78
+
79
+ #### What `types.stripDefault` does
80
+
81
+ `types.stripDefault` works exactly like `types.optional` at runtime — the property gets its
82
+ default when missing — but the key is **left out of the snapshot entirely** when the value
83
+ equals the default:
84
+
85
+ ```ts
86
+ const Settings = types.model({
87
+ theme: types.stripDefault(types.string, "light"),
88
+ })
89
+
90
+ const s = Settings.create()
91
+ getSnapshot(s) // {} ← key omitted because value equals default
92
+
93
+ s.theme = "dark"
94
+ getSnapshot(s) // { theme: "dark" }
95
+
96
+ s.theme = "light"
97
+ getSnapshot(s) // {} ← omitted again
98
+ ```
99
+
100
+ #### Why this matters
101
+
102
+ Without `stripDefault`, keeping defaults out of snapshots requires a manual
103
+ `postProcessSnapshot` on every model. `stripDefault` handles this automatically per-property.
104
+
105
+ This is especially useful when:
106
+
107
+ - Snapshots are stored or synced and you want to minimize their size
108
+ - You diff snapshots (e.g. for undo/redo or change detection) and want default values to
109
+ appear as "no change" rather than an explicit write
110
+ - You have many optional fields and don't want boilerplate `postProcessSnapshot`
111
+
112
+ #### How the default comparison works
113
+
114
+ The comparison is done against the **normalized** default snapshot, not the raw value you
115
+ passed in. MST instantiates the subtype with the default once, reads back its snapshot
116
+ (which applies any nested model defaults and post-processing), and caches that as the
117
+ reference. This means a complex nested default that gains extra fields at instantiation time
118
+ will still compare correctly.
119
+
120
+ ---
121
+
122
+ ### Reflection API additions
123
+
124
+ These utilities expose type metadata that is useful for building generic tooling (e.g.
125
+ editors, inspectors, plugin systems) that need to introspect an MST type at runtime.
126
+
127
+ **`getChildType(node, propertyName?)`** — returns the declared MST type of a child
128
+ property on a model, array, or map instance. For arrays and maps the property name is
129
+ ignored (all children share one element type).
130
+
131
+ ```ts
132
+ const Box = types.model({ x: types.number, y: types.number })
133
+ const box = Box.create({ x: 1, y: 2 })
134
+
135
+ getChildType(box, "x").name // "number"
136
+ ```
137
+
138
+ **`getUnionSubtypes(type)`** — given a `types.union` type, returns the array of its member
139
+ types. Useful when you need to enumerate what a union can hold.
140
+
141
+ ```ts
142
+ const Shape = types.union(Circle, Square, Triangle)
143
+
144
+ getUnionSubtypes(Shape) // [Circle, Square, Triangle]
145
+ ```
146
+
147
+ **`getDefaultInstanceOrSnapshot(optionalType)`** — returns the default value (or snapshot)
148
+ declared on a `types.optional` or `types.stripDefault` type. Lets generic tooling read the
149
+ default without having to create a model instance.
150
+
151
+ ```ts
152
+ const t = types.optional(types.string, "hello")
153
+
154
+ t.getDefaultInstanceOrSnapshot() // "hello"
155
+ ```
package/dist/index.d.ts CHANGED
@@ -1921,6 +1921,17 @@ declare function optional<IT extends IAnyType, OptionalVals extends ValidOptiona
1921
1921
  * @returns
1922
1922
  */
1923
1923
  declare function isOptionalType<IT extends IAnyType>(type: IT): type is IT;
1924
+ /**
1925
+ * `types.stripDefault` - Like `types.optional`, but the property is omitted from
1926
+ * a parent model's snapshot entirely when its value equals the default (instead
1927
+ * of being serialized with the default value). Lets a model produce minimal
1928
+ * snapshots without a bespoke `postProcessSnapshot`.
1929
+ *
1930
+ * @param type
1931
+ * @param defaultValueOrFunction
1932
+ * @returns
1933
+ */
1934
+ declare function stripDefault<IT extends IAnyType>(type: IT, defaultValueOrFunction: OptionalDefaultValueOrFunction<IT>): IOptionalIType<IT, [undefined]>;
1924
1935
 
1925
1936
  /** @hidden */
1926
1937
  interface IMaybeIType<IT extends IAnyType, C, O> extends IType<IT["CreationType"] | C, IT["SnapshotType"] | O, IT["TypeWithoutSTN"] | O> {
@@ -2149,6 +2160,7 @@ declare const types: {
2149
2160
  safeReference: typeof safeReference;
2150
2161
  union: typeof union;
2151
2162
  optional: typeof optional;
2163
+ stripDefault: typeof stripDefault;
2152
2164
  literal: typeof literal;
2153
2165
  maybe: typeof maybe;
2154
2166
  maybeNull: typeof maybeNull;
@@ -4905,7 +4905,7 @@ class ModelType extends ComplexType {
4905
4905
  }
4906
4906
  getSnapshot(node, applyPostProcess = true) {
4907
4907
  const res = {};
4908
- this.forAllProps((name, _type) => {
4908
+ this.forAllProps((name, type) => {
4909
4909
  try {
4910
4910
  // TODO: FIXME, make sure the observable ref is used!
4911
4911
  const atom = mobx.getAtom(node.storedValue, name);
@@ -4914,7 +4914,11 @@ class ModelType extends ComplexType {
4914
4914
  catch (_e) {
4915
4915
  throw fail(`${name} property is declared twice`);
4916
4916
  }
4917
- res[name] = this.getChildNode(node, name).snapshot;
4917
+ const snapshot = this.getChildNode(node, name).snapshot;
4918
+ // strip-default optionals omit their key when equal to the default
4919
+ if (!shouldStripChildFromSnapshot(type, snapshot)) {
4920
+ res[name] = snapshot;
4921
+ }
4918
4922
  });
4919
4923
  if (applyPostProcess) {
4920
4924
  return this.applySnapshotPostProcessor(res);
@@ -4924,7 +4928,13 @@ class ModelType extends ComplexType {
4924
4928
  processInitialSnapshot(childNodes) {
4925
4929
  const processed = {};
4926
4930
  Object.keys(childNodes).forEach(key => {
4927
- processed[key] = childNodes[key].getSnapshot();
4931
+ const snapshot = childNodes[key].getSnapshot();
4932
+ // strip-default optionals omit their key when equal to the default; this
4933
+ // mirrors getSnapshot for nodes serialized before they become observable
4934
+ // instances (e.g. array/map children that were never accessed)
4935
+ if (!shouldStripChildFromSnapshot(this.properties[key], snapshot)) {
4936
+ processed[key] = snapshot;
4937
+ }
4928
4938
  });
4929
4939
  return this.applySnapshotPostProcessor(processed);
4930
4940
  }
@@ -5878,6 +5888,84 @@ const undefinedAsOptionalValues = [undefined];
5878
5888
  function isOptionalType(type) {
5879
5889
  return isType(type) && (type.flags & TypeFlags.Optional) > 0;
5880
5890
  }
5891
+ /**
5892
+ * Compare a child snapshot to a stripped-default's reference snapshot. Mirrors
5893
+ * the legacy hand-rolled comparison: identity for primitives, structural for
5894
+ * objects/arrays.
5895
+ */
5896
+ function defaultSnapshotEquals(a, b) {
5897
+ if (a === b) {
5898
+ return true;
5899
+ }
5900
+ if (typeof a === "object" &&
5901
+ a !== null &&
5902
+ typeof b === "object" &&
5903
+ b !== null) {
5904
+ // Cheap structural short-circuit before the full stringify compare: a
5905
+ // value of a different size can't equal the default, so the common strip
5906
+ // case (non-empty value vs an empty `[]`/`{}` default) avoids stringifying
5907
+ // a potentially large snapshot on every getSnapshot.
5908
+ if (Array.isArray(a) !== Array.isArray(b)) {
5909
+ return false;
5910
+ }
5911
+ const aSize = Array.isArray(a) ? a.length : Object.keys(a).length;
5912
+ const bSize = Array.isArray(b) ? b.length : Object.keys(b).length;
5913
+ if (aSize !== bSize) {
5914
+ return false;
5915
+ }
5916
+ return JSON.stringify(a) === JSON.stringify(b);
5917
+ }
5918
+ return false;
5919
+ }
5920
+ /**
5921
+ * An optional type that additionally omits its key from a parent model's
5922
+ * snapshot when the value equals the (snapshotted) default. The comparison is
5923
+ * against the default *snapshot* — i.e. the subtype is instantiated with the
5924
+ * default once and its post-processed snapshot is cached — so a default whose
5925
+ * normalized form gains fields (e.g. a fileLocation gaining `locationType`)
5926
+ * still strips correctly.
5927
+ *
5928
+ * @hidden
5929
+ * @internal
5930
+ */
5931
+ class StripDefaultValue extends OptionalValue {
5932
+ _defaultSnapshot;
5933
+ shouldStripFromSnapshot(snapshot) {
5934
+ if (!this._defaultSnapshot) {
5935
+ // instantiate the subtype detached with the default and read the node's
5936
+ // snapshot, which normalizes (fills model defaults, applies the subtype's
5937
+ // own postProcess). Cached on the (singleton) type after first use.
5938
+ const node = this.getSubTypes().instantiate(null, "", undefined, this.getDefaultInstanceOrSnapshot());
5939
+ this._defaultSnapshot = { value: node.snapshot };
5940
+ }
5941
+ return defaultSnapshotEquals(snapshot, this._defaultSnapshot.value);
5942
+ }
5943
+ }
5944
+ /**
5945
+ * Whether `type` is a strip-default optional whose current child `snapshot`
5946
+ * equals its default and should therefore be omitted from the parent model's
5947
+ * snapshot. Used by `ModelType.getSnapshot`.
5948
+ *
5949
+ * @hidden
5950
+ * @internal
5951
+ */
5952
+ function shouldStripChildFromSnapshot(type, snapshot) {
5953
+ return (type instanceof StripDefaultValue && type.shouldStripFromSnapshot(snapshot));
5954
+ }
5955
+ /**
5956
+ * `types.stripDefault` - Like `types.optional`, but the property is omitted from
5957
+ * a parent model's snapshot entirely when its value equals the default (instead
5958
+ * of being serialized with the default value). Lets a model produce minimal
5959
+ * snapshots without a bespoke `postProcessSnapshot`.
5960
+ *
5961
+ * @param type
5962
+ * @param defaultValueOrFunction
5963
+ * @returns
5964
+ */
5965
+ function stripDefault(type, defaultValueOrFunction) {
5966
+ checkOptionalPreconditions(type, defaultValueOrFunction);
5967
+ return new StripDefaultValue(type, defaultValueOrFunction, undefinedAsOptionalValues);
5968
+ }
5881
5969
 
5882
5970
  const optionalUndefinedType = optional(undefinedType, undefined);
5883
5971
  const optionalNullType = optional(nullType, null);
@@ -6895,6 +6983,7 @@ const types = {
6895
6983
  safeReference,
6896
6984
  union,
6897
6985
  optional,
6986
+ stripDefault,
6898
6987
  literal,
6899
6988
  maybe,
6900
6989
  maybeNull,