@jbrowse/mobx-state-tree 5.12.0 → 5.13.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/dist/index.d.ts CHANGED
@@ -611,6 +611,7 @@ declare class ObjectNode<C, S, T> extends BaseNode<C, S, T> {
611
611
  private _cachedInitialSnapshot?;
612
612
  private _cachedInitialSnapshotCreated;
613
613
  private _snapshotComputed;
614
+ private _snapshotComputedEvaluated;
614
615
  constructor(complexType: ComplexType<C, S, T>, parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: C);
615
616
  createObservableInstanceIfNeeded(fireHooks?: boolean): void;
616
617
  createObservableInstance(fireHooks?: boolean): void;
@@ -620,6 +621,17 @@ declare class ObjectNode<C, S, T> extends BaseNode<C, S, T> {
620
621
  protected fireHook(name: Hook): void;
621
622
  private _snapshotUponDeath?;
622
623
  get snapshot(): S;
624
+ /**
625
+ * @internal
626
+ * @hidden
627
+ * Recompute+retrack the keepAlive snapshot computed if it has already run.
628
+ * Needed whenever something changes what the snapshot would produce without
629
+ * an observable mobx would track: flipping the (non-observable) observable-
630
+ * instance state, or SnapshotProcessor rewriting getSnapshot after
631
+ * construction (see snapshotProcessor._fixNode). If it never ran, there is
632
+ * nothing cached to go stale, so this is a no-op.
633
+ */
634
+ refreshMemoizedSnapshot(): void;
623
635
  getSnapshot(): S;
624
636
  private _getActualSnapshot;
625
637
  private _getCachedInitialSnapshot;
@@ -1725,6 +1737,31 @@ declare function compose<PA extends ModelProperties, OA, FCA, FSA, PB extends Mo
1725
1737
  * @returns
1726
1738
  */
1727
1739
  declare function isModelType<IT extends IAnyModelType = IAnyModelType>(type: IAnyType): type is IT;
1740
+ /**
1741
+ * `extendInstance` - attaches additional actions, views and volatile state to an
1742
+ * already-created model instance, using the same instantiation machinery as
1743
+ * `.extend()`. This enables lazily loading a model's views/actions chain: create
1744
+ * the model with only its base props (plus any critical-path members), then attach
1745
+ * the rest at runtime — e.g. from a dynamically imported module — without rebuilding
1746
+ * the type or re-hydrating the tree.
1747
+ *
1748
+ * The extension is applied inside an action context, so it works on protected trees.
1749
+ * Attached views are computeds (reactive, memoized) and attached actions get a full
1750
+ * MST action context, exactly like members declared in the original chain.
1751
+ *
1752
+ * Note: the attached members are NOT part of the instance's *static* type. If you
1753
+ * need them typed, declare the augmented shape separately (an `import type` of the
1754
+ * chain's return type is erased at build time, so it costs nothing in the bundle).
1755
+ *
1756
+ * @param instance a live model instance
1757
+ * @param fn receives the instance and returns `{ actions?, views?, state? }`
1758
+ * @returns the same instance
1759
+ */
1760
+ declare function extendInstance<T extends IAnyStateTreeNode>(instance: T, fn: (self: T) => {
1761
+ actions?: ModelActions;
1762
+ views?: object;
1763
+ state?: object;
1764
+ }): T;
1728
1765
 
1729
1766
  /**
1730
1767
  * `types.Date` - Creates a type that can only contain a javascript Date value.
@@ -2139,5 +2176,5 @@ declare const types: {
2139
2176
  resilient: typeof resilient;
2140
2177
  };
2141
2178
 
2142
- export { addDisposer, addMiddleware, applyAction, applyPatch, applySnapshot, cast, castFlowReturn, castToReferenceSnapshot, castToSnapshot, clone, createActionTrackingMiddleware, createActionTrackingMiddleware2, decorate, destroy, detach, escapeJsonPath, flow, getChildType, getEnv, getIdentifier, getLivelinessChecking, getMembers, getNodeId, getParent, getParentOfType, getPath, getPathParts, getPropertyMembers, getRelativePath, getRoot, getRunningActionContext, getSnapshot, getType, getUnionSubtypes, hasParent, hasParentOfType, isActionContextChildOf, isActionContextThisOrChildOf, isAlive, isArrayType, isFrozenType, isIdentifierType, isLateType, isLiteralType, isMapType, isModelType, isOptionalType, isPrimitiveType, isProtected, isReferenceType, isRefinementType, isRoot, isStateTreeNode, isType, isUnionType, isValidReference, joinJsonPath, onAction, onPatch, onSnapshot, protect, recordActions, recordPatches, resolveIdentifier, resolvePath, setDevMode, setLivelinessChecking, setLivelynessChecking, setTypeChecking, splitJsonPath, types as t, toGenerator, toGeneratorFunction, tryReference, tryResolve, typecheck, types, unescapeJsonPath, unprotect, walk };
2179
+ export { addDisposer, addMiddleware, applyAction, applyPatch, applySnapshot, cast, castFlowReturn, castToReferenceSnapshot, castToSnapshot, clone, createActionTrackingMiddleware, createActionTrackingMiddleware2, decorate, destroy, detach, escapeJsonPath, extendInstance, flow, getChildType, getEnv, getIdentifier, getLivelinessChecking, getMembers, getNodeId, getParent, getParentOfType, getPath, getPathParts, getPropertyMembers, getRelativePath, getRoot, getRunningActionContext, getSnapshot, getType, getUnionSubtypes, hasParent, hasParentOfType, isActionContextChildOf, isActionContextThisOrChildOf, isAlive, isArrayType, isFrozenType, isIdentifierType, isLateType, isLiteralType, isMapType, isModelType, isOptionalType, isPrimitiveType, isProtected, isReferenceType, isRefinementType, isRoot, isStateTreeNode, isType, isUnionType, isValidReference, joinJsonPath, onAction, onPatch, onSnapshot, protect, recordActions, recordPatches, resolveIdentifier, resolvePath, setDevMode, setLivelinessChecking, setLivelynessChecking, setTypeChecking, splitJsonPath, types as t, toGenerator, toGeneratorFunction, tryReference, tryResolve, typecheck, types, unescapeJsonPath, unprotect, walk };
2143
2180
  export type { $EmptyObjectBrand, CustomTypeOptions, IActionContext, IActionRecorder, IActionTrackingMiddleware2Call, IActionTrackingMiddleware2Hooks, IActionTrackingMiddlewareHooks, IAnyComplexType, IAnyModelType, IAnyStateTreeNode, IAnyType, IArrayType, IComplexType, IDisposer, IJsonPatch, IMSTArray, IMSTMap, IMapType, IMaybe, IMaybeIType, IMaybeNull, IMiddlewareEvent, IMiddlewareEventType, IMiddlewareHandler, IModelReflectionData, IModelReflectionPropertiesData, IModelType, IOptionalIType, IPatchRecorder, IReferenceType, IReversibleJsonPatch, ISerializedActionCall, ISimpleType, ISnapshotProcessor, ISnapshotProcessors, IStateTreeNode, IType, ITypeUnion, Instance, LivelinessMode, LivelynessMode, ModelActions, ModelCreationType, ModelCreationType2, ModelInstanceType, ModelInstanceTypeProps, ModelPrimitive, ModelProperties, ModelPropertiesDeclaration, ModelPropertiesDeclarationToProperties, ModelSnapshotType, ModelSnapshotType2, OnReferenceInvalidated, OnReferenceInvalidatedEvent, OptionalDefaultValueOrFunction, ReferenceIdentifier, ReferenceOptions, ReferenceOptionsGetSet, ReferenceOptionsOnInvalidated, SnapshotIn, SnapshotOrInstance, SnapshotOut, TypeOfValue, TypeOrStateTreeNodeToStateTreeNode, UnionOptions, UnionStringArray, ValidOptionalValue, ValidOptionalValues, _CustomCSProcessor, _CustomJoin, _CustomOrOther, _NotCustomized };
@@ -1139,6 +1139,10 @@ const snapshotReactionOptions = {
1139
1139
  throw e;
1140
1140
  }
1141
1141
  };
1142
+ // keepAlive lets the snapshot computed memoize its value even with no observer,
1143
+ // so getSnapshot() stays referentially stable without an eager snapshot
1144
+ // reaction serializing the whole tree at create time. Shared: mobx only reads it.
1145
+ const snapshotComputedOptions = { keepAlive: true };
1142
1146
  /**
1143
1147
  * @internal
1144
1148
  * @hidden
@@ -1171,9 +1175,18 @@ class ObjectNode extends BaseNode {
1171
1175
  _cachedInitialSnapshot;
1172
1176
  _cachedInitialSnapshotCreated = false;
1173
1177
  _snapshotComputed;
1178
+ // whether _snapshotComputed has ever run — see createObservableInstance
1179
+ _snapshotComputedEvaluated = false;
1174
1180
  constructor(complexType, parent, subpath, environment, initialValue) {
1175
1181
  super(complexType, parent, subpath, environment);
1176
- this._snapshotComputed = mobx.computed(() => freeze(this.getSnapshot()));
1182
+ // keepAlive: memoize the snapshot after the first read even with no
1183
+ // observer, so getSnapshot() is referentially stable (and reconcile-by-
1184
+ // snapshot works) without eagerly serializing the whole tree at create
1185
+ // time — the previous cost, driven by the eager root reaction below.
1186
+ this._snapshotComputed = mobx.computed(() => {
1187
+ this._snapshotComputedEvaluated = true;
1188
+ return freeze(this.getSnapshot());
1189
+ }, snapshotComputedOptions);
1177
1190
  this.unbox = this.unbox.bind(this);
1178
1191
  this._initialSnapshot = freeze(initialValue);
1179
1192
  this.identifierAttribute = complexType.identifierAttribute;
@@ -1257,10 +1270,17 @@ class ObjectNode extends BaseNode {
1257
1270
  this._isRunningAction = false;
1258
1271
  }
1259
1272
  this._observableInstanceState = ObservableInstanceLifecycle.CREATED;
1260
- this._snapshotComputed.trackAndCompute();
1261
- if (this.isRoot) {
1262
- this._addSnapshotReaction();
1263
- }
1273
+ // The snapshot computed branches on the non-observable
1274
+ // "_observableInstanceState" flag we just flipped, so a value already
1275
+ // memoized from the initial snapshot is now stale (mobx can't see the
1276
+ // change) — refresh it. When it was never read there is nothing to refresh,
1277
+ // and forcing it here would serialize the whole subtree (the dominant cost
1278
+ // of creating a root) for a value nobody has asked for yet, so we skip it.
1279
+ this.refreshMemoizedSnapshot();
1280
+ // The root's snapshot reaction only drives onSnapshot; set it up lazily
1281
+ // (onSnapshot already does) rather than eagerly here, so a root nobody
1282
+ // subscribes to never pays for a full-tree snapshot at creation. keepAlive
1283
+ // on _snapshotComputed preserves snapshot referential stability without it.
1264
1284
  this._childNodes = EMPTY_OBJECT;
1265
1285
  this.state = NodeLifeCycle.CREATED;
1266
1286
  if (fireHooks) {
@@ -1365,6 +1385,21 @@ class ObjectNode extends BaseNode {
1365
1385
  }
1366
1386
  return this._snapshotComputed.get();
1367
1387
  }
1388
+ /**
1389
+ * @internal
1390
+ * @hidden
1391
+ * Recompute+retrack the keepAlive snapshot computed if it has already run.
1392
+ * Needed whenever something changes what the snapshot would produce without
1393
+ * an observable mobx would track: flipping the (non-observable) observable-
1394
+ * instance state, or SnapshotProcessor rewriting getSnapshot after
1395
+ * construction (see snapshotProcessor._fixNode). If it never ran, there is
1396
+ * nothing cached to go stale, so this is a no-op.
1397
+ */
1398
+ refreshMemoizedSnapshot() {
1399
+ if (this._snapshotComputedEvaluated) {
1400
+ this._snapshotComputed.trackAndCompute();
1401
+ }
1402
+ }
1368
1403
  // NOTE: we use this method to get snapshot without creating @computed overhead
1369
1404
  getSnapshot() {
1370
1405
  if (!this.isAlive) {
@@ -3312,6 +3347,11 @@ let _typeChecking;
3312
3347
  function setTypeChecking(enabled) {
3313
3348
  _typeChecking = enabled;
3314
3349
  }
3350
+ // the ENABLE_TYPE_CHECK env var is fixed at process start, so read it once:
3351
+ // process.env access is a comparatively expensive lookup and isTypeCheckingEnabled
3352
+ // runs on every create() / typed write. The setTypeChecking() override and
3353
+ // dev-mode default stay live below.
3354
+ const _envTypeCheck = typeof process !== "undefined" && process.env?.ENABLE_TYPE_CHECK === "true";
3315
3355
  /**
3316
3356
  * @internal
3317
3357
  * @hidden
@@ -3319,10 +3359,7 @@ function setTypeChecking(enabled) {
3319
3359
  function isTypeCheckingEnabled() {
3320
3360
  // an explicit setTypeChecking() override (incl. `false`) wins over the
3321
3361
  // dev-mode / env-var default, hence ?? rather than ||
3322
- return (_typeChecking ??
3323
- (devMode() ||
3324
- (typeof process !== "undefined" &&
3325
- process.env?.ENABLE_TYPE_CHECK === "true")));
3362
+ return _typeChecking ?? (devMode() || _envTypeCheck);
3326
3363
  }
3327
3364
  let _devMode = process.env.NODE_ENV !== "production";
3328
3365
  /**
@@ -3725,6 +3762,11 @@ class SnapshotProcessor extends BaseType {
3725
3762
  }
3726
3763
  const oldGetSnapshot = node.getSnapshot;
3727
3764
  node.getSnapshot = () => this.postProcessSnapshot(oldGetSnapshot.call(node), node);
3765
+ // getSnapshot was just rewritten; refresh any snapshot already memoized for
3766
+ // this node (e.g. it was serialized before being moved under this processor)
3767
+ if (node instanceof ObjectNode) {
3768
+ node.refreshMemoizedSnapshot();
3769
+ }
3728
3770
  if (!isUnionType(this._subtype)) {
3729
3771
  node.getReconciliationType = () => {
3730
3772
  return this;
@@ -4647,11 +4689,27 @@ class ModelType extends ComplexType {
4647
4689
  constructor(opts) {
4648
4690
  super(opts.name || defaultObjectOptions.name);
4649
4691
  Object.assign(this, defaultObjectOptions, opts);
4650
- // ensures that any default value gets converted to its related type
4651
- this.properties = toPropertiesObject(this.properties);
4652
- freeze(this.properties); // make sure nobody messes with it
4653
- this.propertyNames = Object.keys(this.properties);
4654
- this.identifierAttribute = this._getIdentifierAttribute();
4692
+ if (opts.propertiesArePreProcessed) {
4693
+ // `properties` is a parent type's already-converted + frozen output
4694
+ // (chain step with no new props), so its derived propertyNames and
4695
+ // identifierAttribute are identical to the parent's — reuse them verbatim
4696
+ // (cloneAndEnhance passed them in) instead of re-running Object.keys and
4697
+ // the per-prop identifier scan, both O(props), on every step.
4698
+ this.propertyNames = opts.propertyNames;
4699
+ this.identifierAttribute = opts.identifierAttribute;
4700
+ }
4701
+ else {
4702
+ // `propertiesAreConverted` (set by cloneAndEnhance when props are added)
4703
+ // means every value is already a type — the parent's converted bag merged
4704
+ // with a freshly converted delta — so skip re-converting. Only raw entry
4705
+ // points (`model()`) still need the full toPropertiesObject pass.
4706
+ if (!opts.propertiesAreConverted) {
4707
+ this.properties = toPropertiesObject(this.properties);
4708
+ }
4709
+ freeze(this.properties); // make sure nobody messes with it
4710
+ this.propertyNames = Object.keys(this.properties);
4711
+ this.identifierAttribute = this._getIdentifierAttribute();
4712
+ }
4655
4713
  }
4656
4714
  _getIdentifierAttribute() {
4657
4715
  let identifierAttribute = undefined;
@@ -4666,9 +4724,35 @@ class ModelType extends ComplexType {
4666
4724
  return identifierAttribute;
4667
4725
  }
4668
4726
  cloneAndEnhance(opts) {
4727
+ // Fast path: a chain step that adds no new properties (.actions/.views/
4728
+ // .volatile/.named/pre-postProcessor) reuses this type's already-converted +
4729
+ // frozen properties (and their derived names/identifier) verbatim.
4730
+ const hasNewProps = opts.properties !== undefined && Object.keys(opts.properties).length > 0;
4731
+ if (!hasNewProps) {
4732
+ return new ModelType({
4733
+ name: opts.name || this.name,
4734
+ properties: this.properties,
4735
+ propertiesArePreProcessed: true,
4736
+ // safe to share: propertyNames is never mutated after construction
4737
+ propertyNames: this.propertyNames,
4738
+ identifierAttribute: this.identifierAttribute,
4739
+ initializers: this.initializers.concat(opts.initializers || []),
4740
+ preProcessor: opts.preProcessor || this.preProcessor,
4741
+ postProcessor: opts.postProcessor || this.postProcessor
4742
+ });
4743
+ }
4744
+ // Adding props: this type's `properties` is already converted+frozen, so only
4745
+ // the delta needs conversion. compose passes already-converted props
4746
+ // (cur.properties) and flags them; `.props()` passes a raw declaration. Either
4747
+ // way the merged bag is fully converted, so the constructor skips re-converting
4748
+ // the (potentially large) inherited set — the previous hot spot in compose.
4749
+ const convertedNewProps = opts.propertiesAreConverted
4750
+ ? opts.properties
4751
+ : toPropertiesObject(opts.properties);
4669
4752
  return new ModelType({
4670
4753
  name: opts.name || this.name,
4671
- properties: Object.assign({}, this.properties, opts.properties),
4754
+ properties: Object.assign({}, this.properties, convertedNewProps),
4755
+ propertiesAreConverted: true,
4672
4756
  initializers: this.initializers.concat(opts.initializers || []),
4673
4757
  preProcessor: opts.preProcessor || this.preProcessor,
4674
4758
  postProcessor: opts.postProcessor || this.postProcessor
@@ -4765,6 +4849,29 @@ class ModelType extends ComplexType {
4765
4849
  };
4766
4850
  return this.cloneAndEnhance({ initializers: [viewInitializer] });
4767
4851
  }
4852
+ /**
4853
+ * @internal
4854
+ * @hidden
4855
+ * Materializes an additional actions/views/state extension onto an
4856
+ * already-created instance, using the same machinery as `.extend()` runs at
4857
+ * creation time. Powers {@link extendInstance}; callers must ensure the writes
4858
+ * are permitted (see extendInstance, which runs this in an action context).
4859
+ */
4860
+ applyExtensionToInstance(self, extension) {
4861
+ const { actions, views, state, ...rest } = extension;
4862
+ for (const key in rest) {
4863
+ throw fail(`extendInstance should return an object with a subset of the fields 'actions', 'views' and 'state'. Found invalid key '${key}'`);
4864
+ }
4865
+ if (state) {
4866
+ this.instantiateVolatileState(self, state);
4867
+ }
4868
+ if (views) {
4869
+ this.instantiateViews(self, views);
4870
+ }
4871
+ if (actions) {
4872
+ this.instantiateActions(self, actions);
4873
+ }
4874
+ }
4768
4875
  instantiateViews(self, views) {
4769
4876
  // check views return
4770
4877
  if (!isPlainObject(views)) {
@@ -5025,6 +5132,8 @@ function compose(...args) {
5025
5132
  .reduce((prev, cur) => prev.cloneAndEnhance({
5026
5133
  name: `${prev.name}_${cur.name}`,
5027
5134
  properties: cur.properties,
5135
+ // cur.properties is another ModelType's already-converted+frozen bag
5136
+ propertiesAreConverted: true,
5028
5137
  initializers: cur.initializers,
5029
5138
  preProcessor: (snapshot) => cur.applySnapshotPreProcessor(prev.applySnapshotPreProcessor(snapshot)),
5030
5139
  postProcessor: (snapshot) => cur.applySnapshotPostProcessor(prev.applySnapshotPostProcessor(snapshot))
@@ -5040,6 +5149,46 @@ function compose(...args) {
5040
5149
  function isModelType(type) {
5041
5150
  return isType(type) && (type.flags & TypeFlags.Object) > 0;
5042
5151
  }
5152
+ /**
5153
+ * `extendInstance` - attaches additional actions, views and volatile state to an
5154
+ * already-created model instance, using the same instantiation machinery as
5155
+ * `.extend()`. This enables lazily loading a model's views/actions chain: create
5156
+ * the model with only its base props (plus any critical-path members), then attach
5157
+ * the rest at runtime — e.g. from a dynamically imported module — without rebuilding
5158
+ * the type or re-hydrating the tree.
5159
+ *
5160
+ * The extension is applied inside an action context, so it works on protected trees.
5161
+ * Attached views are computeds (reactive, memoized) and attached actions get a full
5162
+ * MST action context, exactly like members declared in the original chain.
5163
+ *
5164
+ * Note: the attached members are NOT part of the instance's *static* type. If you
5165
+ * need them typed, declare the augmented shape separately (an `import type` of the
5166
+ * chain's return type is erased at build time, so it costs nothing in the bundle).
5167
+ *
5168
+ * @param instance a live model instance
5169
+ * @param fn receives the instance and returns `{ actions?, views?, state? }`
5170
+ * @returns the same instance
5171
+ */
5172
+ function extendInstance(instance, fn) {
5173
+ if (!isStateTreeNode(instance)) {
5174
+ throw fail("extendInstance expects a mobx-state-tree node");
5175
+ }
5176
+ const type = getStateTreeNode(instance).type;
5177
+ if (!isModelType(type)) {
5178
+ throw fail("extendInstance can only be used on model instances");
5179
+ }
5180
+ // Views/actions/volatile are installed via defineProperty + makeObservable — the
5181
+ // same "add" operations the initializers perform at creation, before the
5182
+ // write-protection interceptor is attached (see finalizeNewInstance). On a live
5183
+ // instance that interceptor is already active, so run the attach inside an action
5184
+ // context: isRunningAction() then short-circuits assertWritable.
5185
+ const modelType = type;
5186
+ const attach = createActionInvoker(instance, "@@extendInstance", (() => {
5187
+ modelType.applyExtensionToInstance(instance, fn(instance));
5188
+ }));
5189
+ attach();
5190
+ return instance;
5191
+ }
5043
5192
 
5044
5193
  // TODO: implement CoreType using types.custom ?
5045
5194
  /**
@@ -5468,7 +5617,24 @@ class Union extends BaseType {
5468
5617
  }
5469
5618
  return `${baseWithDiscriminator}:\n ${formatValidationErrorLines(errors).join("\n ")}`;
5470
5619
  }
5620
+ // Memoizes the discriminator -> member scan below. Union membership is fixed
5621
+ // at construction, so the result for a given `type` string never changes.
5622
+ // Without this, validating a config with many elements drawn from a wide
5623
+ // pluggable union (e.g. jbrowse's 30+ track/adapter types) re-scans every
5624
+ // member — and calls resolveModelType + literal.is() on each — once per
5625
+ // element. With it, each distinct discriminator scans once; the rest are
5626
+ // O(1) map hits. `undefined` (no match OR ambiguous) is cached too.
5627
+ _discriminatorCache;
5471
5628
  _findCandidateByTypeDiscriminator(discriminator) {
5629
+ const cache = (this._discriminatorCache ??= new Map());
5630
+ if (cache.has(discriminator)) {
5631
+ return cache.get(discriminator);
5632
+ }
5633
+ const found = this._scanForTypeDiscriminator(discriminator);
5634
+ cache.set(discriminator, found);
5635
+ return found;
5636
+ }
5637
+ _scanForTypeDiscriminator(discriminator) {
5472
5638
  let found;
5473
5639
  for (const t of this._types) {
5474
5640
  const model = resolveModelType(t);
@@ -7005,6 +7171,7 @@ exports.decorate = decorate;
7005
7171
  exports.destroy = destroy;
7006
7172
  exports.detach = detach;
7007
7173
  exports.escapeJsonPath = escapeJsonPath;
7174
+ exports.extendInstance = extendInstance;
7008
7175
  exports.flow = flow;
7009
7176
  exports.getChildType = getChildType;
7010
7177
  exports.getEnv = getEnv;