@jbrowse/core 5.0.0-beta.10 → 5.0.0-beta.13

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 (145) hide show
  1. package/README.md +210 -80
  2. package/esm/BaseFeatureWidget/BaseFeatureDetail/ArrayValue.js +0 -2
  3. package/esm/BaseFeatureWidget/BaseFeatureDetail/Attributes.d.ts +3 -0
  4. package/esm/BaseFeatureWidget/BaseFeatureDetail/Attributes.js +14 -1
  5. package/esm/BaseFeatureWidget/BaseFeatureDetail/BasicValue.js +22 -6
  6. package/esm/BaseFeatureWidget/BaseFeatureDetail/jexlFilterActions.js +3 -4
  7. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeatureDetails.d.ts +2 -0
  8. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeatureDetails.js +5 -5
  9. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeaturePanel.js +1 -1
  10. package/esm/BaseFeatureWidget/SequenceFeatureDetails/dialogs/FeatureSequenceDialog.d.ts +3 -1
  11. package/esm/BaseFeatureWidget/SequenceFeatureDetails/dialogs/FeatureSequenceDialog.js +2 -2
  12. package/esm/BaseFeatureWidget/SequenceFeatureDetails/featureTypeUtil.d.ts +2 -1
  13. package/esm/BaseFeatureWidget/SequenceFeatureDetails/featureTypeUtil.js +7 -14
  14. package/esm/BaseFeatureWidget/SequenceFeatureDetails/seqtypes/CDNASequence.js +3 -3
  15. package/esm/BaseFeatureWidget/SequenceFeatureDetails/useSequenceData.js +8 -5
  16. package/esm/BaseFeatureWidget/stateModelFactory.d.ts +7 -0
  17. package/esm/BaseFeatureWidget/stateModelFactory.js +11 -0
  18. package/esm/BaseFeatureWidget/types.d.ts +2 -0
  19. package/esm/CorePlugin.js +32 -1
  20. package/esm/PluginManager.d.ts +6 -3
  21. package/esm/PluginManager.js +6 -3
  22. package/esm/ReExports/coreModules.generated.js +291 -283
  23. package/esm/ReExports/coreWorkerModules.generated.js +288 -281
  24. package/esm/ReExports/list.js +10 -2
  25. package/esm/TextSearch/TextSearchManager.d.ts +3 -3
  26. package/esm/TextSearch/TextSearchManager.js +6 -13
  27. package/esm/configuration/configurationSchema.d.ts +33 -95
  28. package/esm/configuration/configurationSchema.js +50 -218
  29. package/esm/configuration/configurationSlot.d.ts +18 -64
  30. package/esm/configuration/configurationSlot.js +31 -97
  31. package/esm/configuration/formatDetailsConfigSchema.js +2 -0
  32. package/esm/configuration/getConf.d.ts +7 -36
  33. package/esm/configuration/getConf.js +7 -37
  34. package/esm/configuration/index.d.ts +5 -2
  35. package/esm/configuration/index.js +3 -1
  36. package/esm/configuration/partitionAdvanced.d.ts +12 -0
  37. package/esm/configuration/partitionAdvanced.js +42 -0
  38. package/esm/configuration/plot.d.ts +67 -0
  39. package/esm/configuration/plot.js +175 -0
  40. package/esm/configuration/readConfObject.d.ts +15 -17
  41. package/esm/configuration/readConfObject.js +19 -50
  42. package/esm/configuration/schemaTypes.js +5 -1
  43. package/esm/configuration/snapshotPreprocess.js +1 -3
  44. package/esm/configuration/types.d.ts +60 -159
  45. package/esm/pluggableElementTypes/DisplayType.d.ts +14 -0
  46. package/esm/pluggableElementTypes/DisplayType.js +13 -0
  47. package/esm/pluggableElementTypes/RpcMethodType.d.ts +7 -0
  48. package/esm/pluggableElementTypes/RpcMethodType.js +9 -0
  49. package/esm/pluggableElementTypes/WidgetType.d.ts +6 -0
  50. package/esm/pluggableElementTypes/WidgetType.js +6 -0
  51. package/esm/pluggableElementTypes/addMenuItems.d.ts +2 -1
  52. package/esm/pluggableElementTypes/addMenuItems.js +2 -1
  53. package/esm/pluggableElementTypes/index.js +1 -8
  54. package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +58 -1
  55. package/esm/pluggableElementTypes/models/BaseDisplayModel.js +84 -2
  56. package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +2 -4
  57. package/esm/pluggableElementTypes/models/BaseTrackModel.js +2 -4
  58. package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +2 -2
  59. package/esm/pluggableElementTypes/models/BaseViewModel.js +1 -1
  60. package/esm/pluggableElementTypes/models/baseTrackConfig.js +3 -0
  61. package/esm/rpc/RpcRegistry.d.ts +2 -1
  62. package/esm/rpc/byteBudget.d.ts +7 -0
  63. package/esm/rpc/byteBudget.js +10 -0
  64. package/esm/rpc/coreRpcMethods.d.ts +1 -1
  65. package/esm/rpc/coreRpcMethods.js +1 -1
  66. package/esm/rpc/explainTransferError.js +3 -3
  67. package/esm/rpc/methods/CoreLoadTrackCode.d.ts +17 -0
  68. package/esm/rpc/methods/CoreLoadTrackCode.js +24 -0
  69. package/esm/ui/AppLogo.js +43 -3
  70. package/esm/ui/JexlFilterDialog.d.ts +1 -1
  71. package/esm/ui/JexlFilterDialog.js +2 -3
  72. package/esm/ui/buildExtraTrackMenuItems.d.ts +2 -2
  73. package/esm/ui/colors.d.ts +41 -30
  74. package/esm/ui/colors.js +81 -44
  75. package/esm/ui/multiTrackMenuItems.d.ts +2 -2
  76. package/esm/util/TimeTraveller.d.ts +3 -0
  77. package/esm/util/TimeTraveller.js +34 -2
  78. package/esm/util/alignedBytes.d.ts +19 -0
  79. package/esm/util/alignedBytes.js +37 -0
  80. package/esm/util/analytics.d.ts +5 -6
  81. package/esm/util/analytics.js +5 -3
  82. package/esm/util/baseDisplayConfig.d.ts +8 -0
  83. package/esm/util/baseDisplayConfig.js +27 -0
  84. package/esm/util/bezierConnector.js +5 -1
  85. package/esm/util/cachedSetup.d.ts +16 -6
  86. package/esm/util/cachedSetup.js +108 -38
  87. package/esm/util/categoricalField.d.ts +23 -3
  88. package/esm/util/categoricalField.js +36 -3
  89. package/esm/util/cellMatches.d.ts +23 -0
  90. package/esm/util/cellMatches.js +264 -0
  91. package/esm/util/cellsStep.d.ts +27 -0
  92. package/esm/util/cellsStep.js +439 -0
  93. package/esm/util/color-bits/parse.js +22 -11
  94. package/esm/util/colorRamp.js +6 -0
  95. package/esm/util/colorScale.d.ts +9 -2
  96. package/esm/util/colorScale.js +33 -2
  97. package/esm/util/colorSchemes.d.ts +4 -2
  98. package/esm/util/colorSchemes.js +4 -1
  99. package/esm/util/copyToClipboard.js +8 -1
  100. package/esm/util/createAbortRotation.js +3 -3
  101. package/esm/util/featureTransforms.d.ts +7 -6
  102. package/esm/util/featureTransforms.js +131 -479
  103. package/esm/util/fetchPhases.d.ts +1 -1
  104. package/esm/util/formatBytes.js +1 -1
  105. package/esm/util/formatGuessers.d.ts +25 -14
  106. package/esm/util/formatGuessers.js +42 -27
  107. package/esm/util/formatRelativeTime.js +1 -1
  108. package/esm/util/getLocationUri.d.ts +2 -1
  109. package/esm/util/getLocationUri.js +8 -2
  110. package/esm/util/groupKeys.d.ts +4 -5
  111. package/esm/util/groupKeys.js +7 -6
  112. package/esm/util/intervals.js +9 -4
  113. package/esm/util/io/index.js +3 -1
  114. package/esm/util/isoformRank.d.ts +39 -0
  115. package/esm/util/isoformRank.js +118 -0
  116. package/esm/util/jexlFilters.d.ts +31 -49
  117. package/esm/util/jexlFilters.js +29 -39
  118. package/esm/util/locString.js +5 -2
  119. package/esm/util/markEncoding.js +48 -8
  120. package/esm/util/markEncodingTypes.d.ts +11 -2
  121. package/esm/util/quantileExtent.d.ts +22 -0
  122. package/esm/util/quantileExtent.js +24 -1
  123. package/esm/util/springAnimate.js +1 -1
  124. package/esm/util/stepTables.d.ts +14 -0
  125. package/esm/util/stepTables.js +38 -4
  126. package/esm/util/systemColorScheme.d.ts +4 -0
  127. package/esm/util/systemColorScheme.js +21 -0
  128. package/esm/util/tabix.d.ts +7 -3
  129. package/esm/util/tabix.js +3 -3
  130. package/esm/util/temporaryAssembly.d.ts +2 -2
  131. package/esm/util/temporaryAssembly.js +2 -3
  132. package/esm/util/trackConfigDelta.d.ts +1 -1
  133. package/esm/util/tracks.d.ts +28 -45
  134. package/esm/util/tracks.js +71 -82
  135. package/esm/util/types/index.d.ts +15 -7
  136. package/esm/util/types/index.js +21 -2
  137. package/esm/util/unknownSnapshotKeys.d.ts +3 -1
  138. package/esm/util/unknownSnapshotKeys.js +5 -11
  139. package/esm/util/usePanZoom.js +10 -3
  140. package/esm/util/useRafCommit.js +1 -0
  141. package/esm/util/useRowVirtualScroll.d.ts +1 -0
  142. package/esm/util/withFeatureDetails.js +4 -0
  143. package/package.json +13 -9
  144. package/esm/rpc/methods/CoreLoadAdapterCode.d.ts +0 -13
  145. package/esm/rpc/methods/CoreLoadAdapterCode.js +0 -16
@@ -0,0 +1,175 @@
1
+ import { getSnapshot, getType, isArrayType, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
2
+ import { compareStructural } from 'mobx';
3
+ import { colorProblems, colorSlotsOf, noticeLines, scaleEndProblems, scaleEndsOf, } from '../util/colorScale.js';
4
+ import { readConfObject } from './readConfObject.js';
5
+ import { getConfigurationSchemaMetadata } from './schemaRegistry.js';
6
+ import { bareFormOf, shorthandTargets } from './schemaTypes.js';
7
+ /**
8
+ * #api core/configuration
9
+ * The grammar's settings, by the slot name every display that has one gives
10
+ * it, and what each holds: what "Edit plot..." shows and an agent reads as a
11
+ * display's `plot`. A display's plot is the ones its config declares
12
+ * (`plotKeysOf`).
13
+ */
14
+ export const PLOT_VOCABULARY = {
15
+ marks: 'the marks drawn in order, each a mark and an encoding',
16
+ transform: 'the steps run over the features before any mark',
17
+ unit: 'what one row stands for: a read or a chain, a sample or a haplotype',
18
+ facet: 'one section per value of a field',
19
+ rows: 'the rows: their field or order, labels, focus and tree',
20
+ rowColor: 'the colour of each row, by a row attribute',
21
+ color: 'the colour',
22
+ baseColor: 'the per-base layer over the reads',
23
+ arcColor: 'the colour of the arcs between mates',
24
+ ribbonColor: 'the colour of the ribbons between lanes',
25
+ laneLayers: 'the layers drawn over each lane',
26
+ scales: 'the axes, scales.y the value axis',
27
+ y: "what each row's bar height carries",
28
+ filter: 'the jexl: expressions a feature has to pass',
29
+ filterBy: 'the read flags and tags a read has to pass',
30
+ };
31
+ const LIFT_ID = 'plotLift';
32
+ /**
33
+ * #api core/configuration
34
+ * The plot keys a display config declares.
35
+ */
36
+ export function plotKeysOf(conf) {
37
+ const definition = getConfigurationSchemaMetadata(conf)?.definition ?? {};
38
+ return Object.keys(PLOT_VOCABULARY).filter(key => Object.hasOwn(definition, key));
39
+ }
40
+ // A one-member object a shorthand would write as a bare value prints as that
41
+ // value, as a config file writes it: `color: "red"`, `facet: "strand"`.
42
+ function folded(member, value) {
43
+ if (!isStateTreeNode(member) ||
44
+ typeof value !== 'object' ||
45
+ value === null ||
46
+ Array.isArray(value)) {
47
+ return value;
48
+ }
49
+ const meta = getConfigurationSchemaMetadata(member);
50
+ const entries = Object.entries(value);
51
+ if (!meta || meta.options.shorthandWith || entries.length !== 1) {
52
+ return value;
53
+ }
54
+ const [slot, bare] = entries[0];
55
+ const form = bareFormOf(bare);
56
+ return form && shorthandTargets(meta)[form] === slot ? bare : value;
57
+ }
58
+ /**
59
+ * #api core/configuration
60
+ * A display's plot as declared, defaults left off. A list sitting at a
61
+ * default it shares with no other display, a default plot's marks, shows its
62
+ * entries, since they are what is drawn.
63
+ */
64
+ export function plotOf(conf) {
65
+ const snapshot = getSnapshot(conf);
66
+ const plot = {};
67
+ for (const key of plotKeysOf(conf)) {
68
+ const member = conf[key];
69
+ const value = snapshot[key] ??
70
+ (isStateTreeNode(member) &&
71
+ isArrayType(getType(member)) &&
72
+ member.length > 0
73
+ ? getSnapshot(member)
74
+ : undefined);
75
+ if (value !== undefined) {
76
+ plot[key] = folded(member, value);
77
+ }
78
+ }
79
+ return plot;
80
+ }
81
+ function checkKeys(draft, keys) {
82
+ const unknown = Object.keys(draft).filter(key => !keys.includes(key));
83
+ if (unknown.length > 0) {
84
+ throw new Error(`This display's plot is ${keys.join(', ')}, not ${unknown.join(', ')}`);
85
+ }
86
+ }
87
+ /**
88
+ * #api core/configuration
89
+ * The text as a plot, refusing a key the display's plot does not hold. The
90
+ * schema is the parser past this point.
91
+ */
92
+ export function parsePlot(text, keys) {
93
+ const parsed = JSON.parse(text);
94
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
95
+ throw new Error(`A plot is one JSON object of ${keys.join(', ')}`);
96
+ }
97
+ checkKeys(parsed, keys);
98
+ return parsed;
99
+ }
100
+ function merged(draft, current) {
101
+ const out = { ...current };
102
+ for (const [key, value] of Object.entries(draft)) {
103
+ if (value === null) {
104
+ delete out[key];
105
+ }
106
+ else if (value !== undefined) {
107
+ out[key] = value;
108
+ }
109
+ }
110
+ return out;
111
+ }
112
+ /**
113
+ * #api core/configuration
114
+ * A draft as the display's config would hold it, through the schema's own
115
+ * lift and checks: a shorthand becomes its object, a default falls off, and
116
+ * what a config file is refused for throws, a key outside the plot included.
117
+ * The node is never attached, so nothing on the display is touched; a display
118
+ * reads its typed members off it. The draft is copied first, since MST
119
+ * freezes what it creates from.
120
+ */
121
+ export function liftPlot(conf, draft) {
122
+ checkKeys(draft, plotKeysOf(conf));
123
+ return getType(conf).create({
124
+ displayId: LIFT_ID,
125
+ ...structuredClone(merged(draft, plotOf(conf))),
126
+ });
127
+ }
128
+ /**
129
+ * #api core/configuration
130
+ * What applying a draft writes: each setting whose lifted value differs from
131
+ * the plot, as the lifted value, and `null` for one it resets. A value
132
+ * spelled another way but lifting to the same setting writes nothing.
133
+ * Throws a refusal before anything is written.
134
+ */
135
+ export function plotWrites(conf, draft) {
136
+ const current = plotOf(conf);
137
+ const lifted = plotOf(liftPlot(conf, draft));
138
+ const writes = {};
139
+ for (const [key, value] of Object.entries(draft)) {
140
+ if (value === undefined || compareStructural(lifted[key], current[key])) {
141
+ continue;
142
+ }
143
+ writes[key] = lifted[key] ?? null;
144
+ }
145
+ return writes;
146
+ }
147
+ function subConfOf(conf, key) {
148
+ const member = conf[key];
149
+ return isStateTreeNode(member) ? member : undefined;
150
+ }
151
+ function membersOf(node) {
152
+ return (name) => readConfObject(node, name);
153
+ }
154
+ /**
155
+ * What a lifted plot's colour objects and `scales.y` say together that the
156
+ * display cannot draw as written, as corner-notice lines: each colour object
157
+ * under its schema's `fieldPresets`, as `jbrowse validate` judges it.
158
+ */
159
+ export function schemaPlotProblems(lifted) {
160
+ return plotKeysOf(lifted).flatMap(key => {
161
+ const node = subConfOf(lifted, key);
162
+ const presets = node
163
+ ? getConfigurationSchemaMetadata(node)?.options.fieldPresets
164
+ : undefined;
165
+ const y = key === 'scales' && node ? subConfOf(node, 'y') : undefined;
166
+ return [
167
+ ...(node && presets
168
+ ? noticeLines(key, colorProblems(colorSlotsOf(membersOf(node)), presets))
169
+ : []),
170
+ ...(y
171
+ ? noticeLines('scales.y', scaleEndProblems(scaleEndsOf(membersOf(y))))
172
+ : []),
173
+ ];
174
+ });
175
+ }
@@ -3,20 +3,13 @@ import type { JexlInstance } from '../util/jexlStrings.ts';
3
3
  import type { AnyConfigurationModel, AnyConfigurationSnapshot, ConfigurationSchemaForModel, ConfigurationSlotName, ConfigurationSlotPath, ConfigurationSlotPathValue, ConfigurationSlotValue } from './types.ts';
4
4
  /**
5
5
  * #api core/configuration
6
- * Given a configuration model (an instance of a ConfigurationSchema), read the
7
- * configuration value at the given path. Use this when you hold the
8
- * configuration model directly, e.g. an entry from `session.tracks`.
6
+ * Read the value at a path of a live config node, such as a track's
7
+ * `configuration`, evaluating a `jexl:` callback with `args`.
9
8
  *
10
- * Wants a **live config node**, not a snapshot of one, and passing a snapshot is
11
- * a type error. Slots are built with `types.stripDefault`, so a slot sitting at
12
- * its default is absent from a snapshot — "unset" and "at its default" are
13
- * indistinguishable there, and a read off one reports a default as missing.
14
- *
15
- * That is enforced in the types only, deliberately: it can't be a runtime check.
16
- * `generateHierarchy` reads slots straight off the **un-hydrated frozen** entries
17
- * of `jbrowse.tracks` on purpose, because hydrating every track to answer the
18
- * track selector is what `types.frozen` exists to avoid — and those reads are
19
- * indistinguishable at runtime from the broken spelling.
9
+ * A snapshot is refused by the types, since a slot at its default is absent
10
+ * from one. A `session.tracks` entry is one (`TrackConfigEntry`): read its
11
+ * `trackId` and `type` directly, and a slot through a helper that supplies the
12
+ * default, such as `getConfAssemblyNamesOrNone`.
20
13
  *
21
14
  * @param model - instance of ConfigurationSchema
22
15
  * @param slotPaths - array of paths to read
@@ -26,9 +19,14 @@ import type { AnyConfigurationModel, AnyConfigurationSnapshot, ConfigurationSche
26
19
  export declare function readConfObject(confObject: AnyConfigurationModel): AnyConfigurationSnapshot;
27
20
  export declare function readConfObject<CONFMODEL extends AnyConfigurationModel, const SLOT extends ConfigurationSlotName<ConfigurationSchemaForModel<CONFMODEL>> | ConfigurationSlotPath<ConfigurationSchemaForModel<CONFMODEL>> = ConfigurationSlotName<ConfigurationSchemaForModel<CONFMODEL>>>(confObject: CONFMODEL, slotPath?: SLOT, args?: Record<string, unknown>): SLOT extends string ? ConfigurationSlotValue<ConfigurationSchemaForModel<CONFMODEL>, SLOT> : ConfigurationSlotPathValue<ConfigurationSchemaForModel<CONFMODEL>, SLOT>;
28
21
  /**
29
- * Read a value from a plain config snapshot object. Automatically evaluates
30
- * "jexl:..." strings per-feature. Works without MST — intended for use in
31
- * rendering code (GPU, Canvas2D, workers). Pass the realm's `pluginManager.jexl`
32
- * so plugin-registered functions (e.g. in a custom `mouseover` slot) resolve.
22
+ * Read a value from a plain config snapshot, evaluating a `jexl:` callback
23
+ * against `feature`. For rendering code and workers; pass the realm's
24
+ * `pluginManager.jexl` so plugin-registered functions resolve.
33
25
  */
34
26
  export declare function readConfigValue<T>(config: Record<string, unknown>, key: string | string[], feature: Feature, jexl: JexlInstance): T;
27
+ /**
28
+ * #api core/configuration
29
+ * A config value as `feature` reads it: a `jexl:` callback evaluated against
30
+ * the feature, anything else as written.
31
+ */
32
+ export declare function evaluateForFeature(value: unknown, feature: Feature, jexl: JexlInstance): unknown;
@@ -1,27 +1,14 @@
1
1
  /**
2
2
  * @module
3
- * The config **readers**: `readConfObject` off a live MST config node, and
4
- * `readConfigValue` off a plain snapshot object in a worker or a renderer. Both
5
- * exist to evaluate a slot's `jexl:...` callback on read; they differ only in
6
- * where the jexl instance comes from, the node's env or an explicit argument.
7
- * A `featureField` slot's `jexl:` expression is the display's to evaluate per
8
- * feature, so `readConfObject` hands it over as written.
3
+ * The config readers: `readConfObject` off a live config node, whose env
4
+ * supplies the jexl instance, and `readConfigValue` off a plain snapshot in a
5
+ * worker or renderer, which is handed one.
9
6
  */
10
7
  import { getEnv, getSnapshot, getType, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
11
8
  import { getConfigurationSchemaMetadata } from './schemaRegistry.js';
12
9
  import { evaluateJexl, isCallbackValue } from './slotValueUtils.js';
13
- // Evaluate a slot's `jexl:...` callback string against the realm's single jexl
14
- // instance (carrying plugin-registered functions), read from the config node's
15
- // env. readConfObject only ever operates on live MST configs — nested sub-config
16
- // reads stay MST (env resolves to the root), and frozen track configs hydrate to
17
- // MST before any callback is read — so the env instance is always present here.
18
10
  function evalConfigCallback(expr, args, confObject) {
19
11
  if (!isStateTreeNode(confObject)) {
20
- // A jexl slot needs the realm's jexl instance, which is read from a live
21
- // config node's env. A plain config snapshot (e.g. an un-hydrated
22
- // session.tracks entry) carries no env — read it through a hydrated model,
23
- // or use readConfigValue(config, key, feature, jexl) which takes jexl
24
- // explicitly.
25
12
  throw new Error(`cannot evaluate jexl config callback ${JSON.stringify(expr)}: config is a plain snapshot, not a live model (no env to resolve the jexl instance)`);
26
13
  }
27
14
  const jexl = getEnv(confObject)
@@ -35,11 +22,7 @@ function isFeatureField(confObject, slotName) {
35
22
  return (isStateTreeNode(confObject) &&
36
23
  !!getConfigurationSchemaMetadata(getType(confObject))?.featureFields.has(slotName));
37
24
  }
38
- // Read and resolve a single slot: raw value, jexl callback evaluation, then a
39
- // referentially-stable snapshot for sub-config nodes.
40
25
  function readSlot(confObject, slotName, args) {
41
- // strict undefined check, not truthiness — a slot value can legitimately be
42
- // falsy (0, '', false, null)
43
26
  const value = confObject[slotName];
44
27
  if (value === undefined) {
45
28
  return undefined;
@@ -47,42 +30,25 @@ function readSlot(confObject, slotName, args) {
47
30
  const val = isCallbackValue(value) && !isFeatureField(confObject, slotName)
48
31
  ? evalConfigCallback(value, args, confObject)
49
32
  : value;
50
- // Fast path for primitives (most common case)
51
33
  if (val === null || typeof val !== 'object') {
52
34
  return val;
53
35
  }
54
- // Return the live, referentially-stable snapshot (frozen in dev) rather than
55
- // a per-read clone: stable identity lets downstream computeds memoize, and
56
- // the old structuredClone was both a hot-path allocation and a source of
57
- // spurious recomputation. Treat as read-only.
36
+ // the live snapshot, stable across reads so downstream computeds memoize;
37
+ // read-only
58
38
  return isStateTreeNode(val) ? getSnapshot(val) : val;
59
39
  }
60
- // loose implementation signature: the body returns values that are `any` by
61
- // nature (raw slot values, snapshots); the typed overloads above are what
62
- // callers see. No looser overload admits the model: one that did was a
63
- // catch-all a slot-name typo fell through to, compiling clean as `any`.
40
+ // No looser overload admits the model: a catch-all lets a slot-name typo
41
+ // compile as `any`.
64
42
  export function readConfObject(confObject, slotPath, args = {}) {
65
- // the single-slot read, first and allocation-free: it is the shape of nearly
66
- // every one of the ~1300 call sites, so it doesn't get normalized into a
67
- // one-element array on the way past
68
43
  if (typeof slotPath === 'string') {
69
44
  return readSlot(confObject, slotPath, args);
70
45
  }
71
46
  if (slotPath !== undefined && !Array.isArray(slotPath)) {
72
47
  throw new TypeError('slotPath must be a string or array');
73
48
  }
74
- // No path, or an empty one — which means the same thing, and used not to: `[]`
75
- // is truthy, so it reached the walk below and read `confObject[undefined]`.
76
- //
77
- // Returns the whole config as a plain object: the live, referentially-stable
78
- // snapshot (frozen in dev), not a fresh clone — treat as read-only.
79
49
  if (!slotPath?.length) {
80
50
  return isStateTreeNode(confObject) ? getSnapshot(confObject) : confObject;
81
51
  }
82
- // every segment but the last names a sub-config, and only the last is a slot
83
- // read — which is where jexl evaluation and snapshotting happen, and nowhere
84
- // else. Iterative rather than a self-call per segment, which re-entered the
85
- // whole argument-shape preamble above at each level.
86
52
  let conf = confObject;
87
53
  for (let i = 0; i < slotPath.length - 1; i++) {
88
54
  const subConf = conf[slotPath[i]];
@@ -93,9 +59,6 @@ export function readConfObject(confObject, slotPath, args = {}) {
93
59
  }
94
60
  return readSlot(conf, slotPath[slotPath.length - 1], args);
95
61
  }
96
- // The plain-object half of the slot walk, for readConfigValue. The MST half is
97
- // readSlot/rawSlotValue above. A plain object has no map entries and no
98
- // sub-config nodes to snapshot, so it is just a path reduce.
99
62
  function resolveConfigValue(config, key) {
100
63
  if (Array.isArray(key)) {
101
64
  let val = config;
@@ -107,12 +70,18 @@ function resolveConfigValue(config, key) {
107
70
  return config[key];
108
71
  }
109
72
  /**
110
- * Read a value from a plain config snapshot object. Automatically evaluates
111
- * "jexl:..." strings per-feature. Works without MST — intended for use in
112
- * rendering code (GPU, Canvas2D, workers). Pass the realm's `pluginManager.jexl`
113
- * so plugin-registered functions (e.g. in a custom `mouseover` slot) resolve.
73
+ * Read a value from a plain config snapshot, evaluating a `jexl:` callback
74
+ * against `feature`. For rendering code and workers; pass the realm's
75
+ * `pluginManager.jexl` so plugin-registered functions resolve.
114
76
  */
115
77
  export function readConfigValue(config, key, feature, jexl) {
116
- const raw = resolveConfigValue(config, key);
117
- return (isCallbackValue(raw) ? evaluateJexl(raw, { feature }, jexl) : raw);
78
+ return evaluateForFeature(resolveConfigValue(config, key), feature, jexl);
79
+ }
80
+ /**
81
+ * #api core/configuration
82
+ * A config value as `feature` reads it: a `jexl:` callback evaluated against
83
+ * the feature, anything else as written.
84
+ */
85
+ export function evaluateForFeature(value, feature, jexl) {
86
+ return isCallbackValue(value) ? evaluateJexl(value, { feature }, jexl) : value;
118
87
  }
@@ -67,7 +67,11 @@ export function isSlotDefinitionEntry(def) {
67
67
  export function isConfigurationModel(thing) {
68
68
  return isStateTreeNode(thing) && isConfigurationSchemaType(getType(thing));
69
69
  }
70
- const NUMBER_SLOT_TYPES = new Set(['number', 'integer', 'maybeNumber']);
70
+ const NUMBER_SLOT_TYPES = new Set([
71
+ 'number',
72
+ 'integer',
73
+ 'maybeNumber',
74
+ ]);
71
75
  /**
72
76
  * The slot a bare value of each form lifts into, off a `shorthand` option:
73
77
  * each slot it names takes the form its own type holds, a number where it
@@ -1,4 +1,5 @@
1
1
  import { isJexl } from '../util/jexlStrings.js';
2
+ import { isPlainObject } from '../util/objectUtils.js';
2
3
  import { slotWriteRefusal } from './configurationSlot.js';
3
4
  import { getConfigurationSchemaMetadata } from './schemaRegistry.js';
4
5
  import { bareFormOf, isConstantEntry, shorthandTargets } from './schemaTypes.js';
@@ -60,9 +61,6 @@ function isOwnSnapshot({ name, options }, snapshot) {
60
61
  snapshot.type === undefined ||
61
62
  snapshot.type === name);
62
63
  }
63
- function isPlainObject(value) {
64
- return typeof value === 'object' && value !== null && !Array.isArray(value);
65
- }
66
64
  // `lifted` under `written`: what the snapshot spells wins, member by member
67
65
  // where both are objects.
68
66
  function underWritten(written, lifted) {