@getodk/xforms-engine 0.1.1 → 0.2.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 (101) hide show
  1. package/dist/body/BodyDefinition.d.ts +24 -7
  2. package/dist/body/RepeatElementDefinition.d.ts +19 -0
  3. package/dist/body/appearance/inputAppearanceParser.d.ts +4 -0
  4. package/dist/body/appearance/selectAppearanceParser.d.ts +4 -0
  5. package/dist/body/appearance/structureElementAppearanceParser.d.ts +4 -0
  6. package/dist/body/control/ControlDefinition.d.ts +2 -0
  7. package/dist/body/control/InputDefinition.d.ts +5 -0
  8. package/dist/body/control/select/SelectDefinition.d.ts +11 -1
  9. package/dist/body/group/BaseGroupDefinition.d.ts +3 -8
  10. package/dist/body/text/LabelDefinition.d.ts +2 -0
  11. package/dist/body/text/TextElementDefinition.d.ts +3 -3
  12. package/dist/client/BaseNode.d.ts +6 -1
  13. package/dist/client/GroupNode.d.ts +5 -2
  14. package/dist/client/NodeAppearances.d.ts +15 -0
  15. package/dist/client/RepeatInstanceNode.d.ts +3 -0
  16. package/dist/client/RepeatRangeNode.d.ts +5 -2
  17. package/dist/client/RootNode.d.ts +19 -0
  18. package/dist/client/SelectNode.d.ts +3 -0
  19. package/dist/client/StringNode.d.ts +3 -0
  20. package/dist/client/SubtreeNode.d.ts +1 -0
  21. package/dist/index.js +624 -368
  22. package/dist/index.js.map +1 -1
  23. package/dist/instance/Group.d.ts +3 -3
  24. package/dist/instance/RepeatInstance.d.ts +26 -2
  25. package/dist/instance/RepeatRange.d.ts +84 -5
  26. package/dist/instance/Root.d.ts +8 -23
  27. package/dist/instance/SelectField.d.ts +2 -2
  28. package/dist/instance/StringField.d.ts +2 -2
  29. package/dist/instance/Subtree.d.ts +1 -1
  30. package/dist/instance/abstract/DescendantNode.d.ts +12 -4
  31. package/dist/instance/abstract/InstanceNode.d.ts +26 -29
  32. package/dist/instance/internal-api/EvaluationContext.d.ts +5 -4
  33. package/dist/instance/internal-api/ValueContext.d.ts +2 -2
  34. package/dist/lib/TokenListParser.d.ts +84 -0
  35. package/dist/lib/dom/query.d.ts +5 -0
  36. package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +2 -1
  37. package/dist/model/DescendentNodeDefinition.d.ts +1 -2
  38. package/dist/model/NodeDefinition.d.ts +12 -12
  39. package/dist/model/RepeatInstanceDefinition.d.ts +5 -6
  40. package/dist/model/{RepeatSequenceDefinition.d.ts → RepeatRangeDefinition.d.ts} +4 -4
  41. package/dist/model/RepeatTemplateDefinition.d.ts +6 -7
  42. package/dist/model/RootDefinition.d.ts +3 -1
  43. package/dist/model/SubtreeDefinition.d.ts +2 -2
  44. package/dist/model/ValueNodeDefinition.d.ts +2 -3
  45. package/dist/solid.js +625 -369
  46. package/dist/solid.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/XFormDOM.ts +81 -8
  49. package/src/body/BodyDefinition.ts +38 -23
  50. package/src/body/RepeatElementDefinition.ts +70 -0
  51. package/src/body/appearance/inputAppearanceParser.ts +39 -0
  52. package/src/body/appearance/selectAppearanceParser.ts +38 -0
  53. package/src/body/appearance/structureElementAppearanceParser.ts +7 -0
  54. package/src/body/control/ControlDefinition.ts +4 -0
  55. package/src/body/control/InputDefinition.ts +13 -0
  56. package/src/body/control/select/SelectDefinition.ts +14 -5
  57. package/src/body/group/BaseGroupDefinition.ts +11 -49
  58. package/src/body/text/LabelDefinition.ts +15 -1
  59. package/src/body/text/TextElementDefinition.ts +5 -5
  60. package/src/client/BaseNode.ts +9 -1
  61. package/src/client/GroupNode.ts +6 -2
  62. package/src/client/NodeAppearances.ts +22 -0
  63. package/src/client/RepeatInstanceNode.ts +4 -0
  64. package/src/client/RepeatRangeNode.ts +6 -2
  65. package/src/client/RootNode.ts +22 -0
  66. package/src/client/SelectNode.ts +4 -0
  67. package/src/client/StringNode.ts +4 -0
  68. package/src/client/SubtreeNode.ts +1 -0
  69. package/src/instance/Group.ts +14 -9
  70. package/src/instance/RepeatInstance.ts +59 -15
  71. package/src/instance/RepeatRange.ts +133 -15
  72. package/src/instance/Root.ts +20 -64
  73. package/src/instance/SelectField.ts +7 -7
  74. package/src/instance/StringField.ts +8 -7
  75. package/src/instance/Subtree.ts +10 -7
  76. package/src/instance/abstract/DescendantNode.ts +45 -43
  77. package/src/instance/abstract/InstanceNode.ts +69 -86
  78. package/src/instance/children.ts +17 -7
  79. package/src/instance/index.ts +1 -1
  80. package/src/instance/internal-api/EvaluationContext.ts +5 -6
  81. package/src/instance/internal-api/ValueContext.ts +2 -2
  82. package/src/lib/TokenListParser.ts +156 -0
  83. package/src/lib/dom/query.ts +13 -0
  84. package/src/lib/reactivity/createChildrenState.ts +51 -6
  85. package/src/lib/reactivity/createComputedExpression.ts +1 -1
  86. package/src/lib/reactivity/createSelectItems.ts +4 -6
  87. package/src/lib/reactivity/createValueState.ts +6 -6
  88. package/src/lib/reactivity/materializeCurrentStateChildren.ts +3 -1
  89. package/src/model/DescendentNodeDefinition.ts +1 -2
  90. package/src/model/ModelDefinition.ts +1 -1
  91. package/src/model/NodeDefinition.ts +12 -12
  92. package/src/model/RepeatInstanceDefinition.ts +8 -13
  93. package/src/model/{RepeatSequenceDefinition.ts → RepeatRangeDefinition.ts} +6 -6
  94. package/src/model/RepeatTemplateDefinition.ts +10 -15
  95. package/src/model/RootDefinition.ts +6 -12
  96. package/src/model/SubtreeDefinition.ts +3 -3
  97. package/src/model/ValueNodeDefinition.ts +2 -3
  98. package/dist/body/RepeatDefinition.d.ts +0 -16
  99. package/dist/body/group/RepeatGroupDefinition.d.ts +0 -13
  100. package/src/body/RepeatDefinition.ts +0 -54
  101. package/src/body/group/RepeatGroupDefinition.ts +0 -91
@@ -2,6 +2,7 @@ import type { XFormsXPathEvaluator } from '@getodk/xpath';
2
2
  import type { Accessor, Signal } from 'solid-js';
3
3
  import { createSignal } from 'solid-js';
4
4
  import type { XFormDOM } from '../XFormDOM.ts';
5
+ import type { BodyClassList } from '../body/BodyDefinition.ts';
5
6
  import type { ActiveLanguage, FormLanguage, FormLanguages } from '../client/FormLanguage.ts';
6
7
  import type { RootNode } from '../client/RootNode.ts';
7
8
  import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts';
@@ -98,27 +99,20 @@ export class Root
98
99
  SubscribableDependency,
99
100
  TranslationContext
100
101
  {
101
- static async initialize(
102
- xformDOM: XFormDOM,
103
- definition: RootDefinition,
104
- engineConfig: InstanceConfig
105
- ): Promise<Root> {
106
- const instance = new Root(xformDOM, definition, engineConfig);
107
-
108
- await instance.formStateInitialized();
109
-
110
- return instance;
111
- }
112
-
113
102
  private readonly childrenState: ChildrenState<GeneralChildNode>;
114
103
 
115
104
  // InstanceNode
105
+ readonly hasReadonlyAncestor = () => false;
106
+ readonly isReadonly = () => false;
107
+ readonly hasNonRelevantAncestor = () => false;
108
+ readonly isRelevant = () => true;
116
109
  protected readonly state: SharedNodeState<RootStateSpec>;
117
110
  protected readonly engineState: EngineState<RootStateSpec>;
118
111
 
119
112
  // RootNode
120
113
  readonly nodeType = 'root';
121
-
114
+ readonly appearances = null;
115
+ readonly classes: BodyClassList;
122
116
  readonly currentState: MaterializedChildren<CurrentState<RootStateSpec>, GeneralChildNode>;
123
117
 
124
118
  protected readonly instanceDOM: XFormDOM;
@@ -129,12 +123,6 @@ export class Root
129
123
  // EvaluationContext
130
124
  readonly evaluator: XFormsXPathEvaluator;
131
125
 
132
- private readonly rootReference: string;
133
-
134
- override get contextReference(): string {
135
- return this.rootReference;
136
- }
137
-
138
126
  readonly contextNode: Element;
139
127
 
140
128
  // RootNode
@@ -147,20 +135,18 @@ export class Root
147
135
  return this.engineState.activeLanguage;
148
136
  }
149
137
 
150
- protected constructor(
151
- xformDOM: XFormDOM,
152
- definition: RootDefinition,
153
- engineConfig: InstanceConfig
154
- ) {
155
- super(engineConfig, null, definition);
138
+ constructor(xformDOM: XFormDOM, definition: RootDefinition, engineConfig: InstanceConfig) {
139
+ const reference = definition.nodeset;
156
140
 
157
- const childrenState = createChildrenState<Root, GeneralChildNode>(this);
141
+ super(engineConfig, null, definition, {
142
+ computeReference: () => reference,
143
+ });
158
144
 
159
- this.childrenState = childrenState;
145
+ this.classes = definition.classes;
160
146
 
161
- const reference = definition.nodeset;
147
+ const childrenState = createChildrenState<Root, GeneralChildNode>(this);
162
148
 
163
- this.rootReference = reference;
149
+ this.childrenState = childrenState;
164
150
 
165
151
  const instanceDOM = xformDOM.createInstance();
166
152
  const evaluator = instanceDOM.primaryInstanceEvaluator;
@@ -187,7 +173,11 @@ export class Root
187
173
 
188
174
  this.state = state;
189
175
  this.engineState = state.engineState;
190
- this.currentState = materializeCurrentStateChildren(state.currentState, childrenState);
176
+ this.currentState = materializeCurrentStateChildren(
177
+ this.scope,
178
+ state.currentState,
179
+ childrenState
180
+ );
191
181
 
192
182
  const contextNode = instanceDOM.xformDocument.createElement(definition.nodeName);
193
183
 
@@ -201,40 +191,6 @@ export class Root
201
191
  childrenState.setChildren(buildChildren(this));
202
192
  }
203
193
 
204
- /**
205
- * Waits until form state is fully initialized.
206
- *
207
- * As much as possible, all instance state computations are implemented so
208
- * that they complete synchronously.
209
- *
210
- * There is currently one exception: because instance nodes may form
211
- * computation dependencies into their descendants as well as their ancestors,
212
- * there is an allowance **during form initialization only** to account for
213
- * this chicken/egg scenario. Note that this allowance is intentionally,
214
- * strictly limited: if form state initialization is not resolved within a
215
- * single microtask tick we throw/reject.
216
- *
217
- * All subsequent computations are always performed synchronously (and we will
218
- * use tests to validate this, by utilizing the synchronously returned `Root`
219
- * state from client-facing write interfaces).
220
- */
221
- async formStateInitialized(): Promise<void> {
222
- await new Promise<void>((resolve) => {
223
- queueMicrotask(resolve);
224
- });
225
-
226
- if (!this.isStateInitialized()) {
227
- throw new Error(
228
- 'Form state initialization failed to complete in a single frame. Has some aspect of reactive computation been made asynchronous by mistake?'
229
- );
230
- }
231
- }
232
-
233
- // InstanceNode
234
- protected computeReference(_parent: null, definition: RootDefinition): string {
235
- return definition.nodeset;
236
- }
237
-
238
194
  getChildren(): readonly GeneralChildNode[] {
239
195
  return this.childrenState.getChildren();
240
196
  }
@@ -2,7 +2,7 @@ import { xmlXPathWhitespaceSeparatedList } from '@getodk/common/lib/string/white
2
2
  import type { Accessor } from 'solid-js';
3
3
  import { untrack } from 'solid-js';
4
4
  import type { AnySelectDefinition } from '../body/control/select/SelectDefinition.ts';
5
- import type { SelectItem, SelectNode } from '../client/SelectNode.ts';
5
+ import type { SelectItem, SelectNode, SelectNodeAppearances } from '../client/SelectNode.ts';
6
6
  import type { TextRange } from '../index.ts';
7
7
  import { createSelectItems } from '../lib/reactivity/createSelectItems.ts';
8
8
  import { createValueState } from '../lib/reactivity/createValueState.ts';
@@ -50,7 +50,7 @@ export class SelectField
50
50
 
51
51
  // SelectNode
52
52
  readonly nodeType = 'select';
53
-
53
+ readonly appearances: SelectNodeAppearances;
54
54
  readonly currentState: CurrentState<SelectFieldStateSpec>;
55
55
 
56
56
  // ValueContext
@@ -83,6 +83,7 @@ export class SelectField
83
83
  constructor(parent: GeneralParentNode, definition: SelectFieldDefinition) {
84
84
  super(parent, definition);
85
85
 
86
+ this.appearances = definition.bodyElement.appearances;
86
87
  this.selectExclusive = definition.bodyElement.type === 'select1';
87
88
 
88
89
  const valueOptions = createSelectItems(this);
@@ -92,7 +93,10 @@ export class SelectField
92
93
  const state = createSharedNodeState(
93
94
  this.scope,
94
95
  {
95
- ...this.buildSharedStateSpec(parent, definition),
96
+ reference: this.contextReference,
97
+ readonly: this.isReadonly,
98
+ relevant: this.isRelevant,
99
+ required: this.isRequired,
96
100
 
97
101
  label: createNodeLabel(this, definition),
98
102
  hint: createFieldHint(this, definition),
@@ -120,10 +124,6 @@ export class SelectField
120
124
  );
121
125
  }
122
126
 
123
- protected computeReference(parent: GeneralParentNode): string {
124
- return this.computeChildStepReference(parent);
125
- }
126
-
127
127
  protected updateSelectedItemValues(values: readonly string[]) {
128
128
  const itemsByValue = untrack(() => this.getSelectItemsByValue());
129
129
 
@@ -1,7 +1,7 @@
1
1
  import { identity } from '@getodk/common/lib/identity.ts';
2
2
  import type { Accessor } from 'solid-js';
3
3
  import type { InputDefinition } from '../body/control/InputDefinition.ts';
4
- import type { StringNode } from '../client/StringNode.ts';
4
+ import type { StringNode, StringNodeAppearances } from '../client/StringNode.ts';
5
5
  import type { TextRange } from '../index.ts';
6
6
  import { createValueState } from '../lib/reactivity/createValueState.ts';
7
7
  import type { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts';
@@ -43,7 +43,7 @@ export class StringField
43
43
 
44
44
  // StringNode
45
45
  readonly nodeType = 'string';
46
-
46
+ readonly appearances: StringNodeAppearances;
47
47
  readonly currentState: CurrentState<StringFieldStateSpec>;
48
48
 
49
49
  // ValueContext
@@ -54,10 +54,15 @@ export class StringField
54
54
  constructor(parent: GeneralParentNode, definition: StringFieldDefinition) {
55
55
  super(parent, definition);
56
56
 
57
+ this.appearances = (definition.bodyElement?.appearances ?? null) as StringNodeAppearances;
58
+
57
59
  const state = createSharedNodeState(
58
60
  this.scope,
59
61
  {
60
- ...this.buildSharedStateSpec(parent, definition),
62
+ reference: this.contextReference,
63
+ readonly: this.isReadonly,
64
+ relevant: this.isRelevant,
65
+ required: this.isRequired,
61
66
 
62
67
  label: createNodeLabel(this, definition),
63
68
  hint: createFieldHint(this, definition),
@@ -75,10 +80,6 @@ export class StringField
75
80
  this.currentState = state.currentState;
76
81
  }
77
82
 
78
- protected computeReference(parent: GeneralParentNode): string {
79
- return this.computeChildStepReference(parent);
80
- }
81
-
82
83
  // InstanceNode
83
84
  getChildren(): readonly [] {
84
85
  return [];
@@ -36,7 +36,7 @@ export class Subtree
36
36
 
37
37
  // SubtreeNode
38
38
  readonly nodeType = 'subtree';
39
-
39
+ readonly appearances = null;
40
40
  readonly currentState: MaterializedChildren<CurrentState<SubtreeStateSpec>, GeneralChildNode>;
41
41
 
42
42
  constructor(parent: GeneralParentNode, definition: SubtreeDefinition) {
@@ -49,7 +49,10 @@ export class Subtree
49
49
  const state = createSharedNodeState(
50
50
  this.scope,
51
51
  {
52
- ...this.buildSharedStateSpec(parent, definition),
52
+ reference: this.contextReference,
53
+ readonly: this.isReadonly,
54
+ relevant: this.isRelevant,
55
+ required: this.isRequired,
53
56
 
54
57
  label: null,
55
58
  hint: null,
@@ -64,15 +67,15 @@ export class Subtree
64
67
 
65
68
  this.state = state;
66
69
  this.engineState = state.engineState;
67
- this.currentState = materializeCurrentStateChildren(state.currentState, childrenState);
70
+ this.currentState = materializeCurrentStateChildren(
71
+ this.scope,
72
+ state.currentState,
73
+ childrenState
74
+ );
68
75
 
69
76
  childrenState.setChildren(buildChildren(this));
70
77
  }
71
78
 
72
- protected computeReference(parent: GeneralParentNode): string {
73
- return this.computeChildStepReference(parent);
74
- }
75
-
76
79
  getChildren(): readonly GeneralChildNode[] {
77
80
  return this.childrenState.getChildren();
78
81
  }
@@ -1,6 +1,5 @@
1
1
  import type { XFormsXPathEvaluator } from '@getodk/xpath';
2
2
  import type { Accessor } from 'solid-js';
3
- import { createMemo } from 'solid-js';
4
3
  import type { BaseNode } from '../../client/BaseNode.ts';
5
4
  import { createComputedExpression } from '../../lib/reactivity/createComputedExpression.ts';
6
5
  import type { ReactiveScope } from '../../lib/reactivity/scope.ts';
@@ -52,6 +51,10 @@ export type AnyDescendantNode = DescendantNode<
52
51
  any
53
52
  >;
54
53
 
54
+ interface DescendantNodeOptions {
55
+ readonly computeReference?: Accessor<string>;
56
+ }
57
+
55
58
  export abstract class DescendantNode<
56
59
  Definition extends DescendantNodeDefinition,
57
60
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -61,63 +64,62 @@ export abstract class DescendantNode<
61
64
  extends InstanceNode<Definition, Spec, Child>
62
65
  implements BaseNode, EvaluationContext, SubscribableDependency
63
66
  {
67
+ readonly hasReadonlyAncestor: Accessor<boolean> = () => {
68
+ const { parent } = this;
69
+
70
+ return parent.hasReadonlyAncestor() || parent.isReadonly();
71
+ };
72
+
73
+ readonly isSelfReadonly: Accessor<boolean>;
74
+
75
+ readonly isReadonly: Accessor<boolean> = () => {
76
+ if (this.hasReadonlyAncestor()) {
77
+ return true;
78
+ }
79
+
80
+ return this.isSelfReadonly();
81
+ };
82
+
83
+ readonly hasNonRelevantAncestor: Accessor<boolean> = () => {
84
+ const { parent } = this;
85
+
86
+ return parent.hasNonRelevantAncestor() || !parent.isRelevant();
87
+ };
88
+
89
+ readonly isSelfRelevant: Accessor<boolean>;
90
+
91
+ readonly isRelevant: Accessor<boolean> = () => {
92
+ if (this.hasNonRelevantAncestor()) {
93
+ return false;
94
+ }
95
+
96
+ return this.isSelfRelevant();
97
+ };
98
+
99
+ protected readonly isRequired: Accessor<boolean>;
100
+
64
101
  readonly root: Root;
65
102
  readonly evaluator: XFormsXPathEvaluator;
66
103
  readonly contextNode: Element;
67
104
 
68
105
  constructor(
69
106
  override readonly parent: DescendantNodeParent<Definition>,
70
- override readonly definition: Definition
107
+ override readonly definition: Definition,
108
+ options?: DescendantNodeOptions
71
109
  ) {
72
- super(parent.engineConfig, parent, definition);
110
+ super(parent.engineConfig, parent, definition, options);
73
111
 
74
112
  const { evaluator, root } = parent;
75
113
 
76
114
  this.root = root;
77
115
  this.evaluator = evaluator;
78
116
  this.contextNode = this.initializeContextNode(parent.contextNode, definition.nodeName);
79
- }
80
-
81
- protected computeChildStepReference(parent: DescendantNodeParent<Definition>): string {
82
- return `${parent.contextReference}/${this.definition.nodeName}`;
83
- }
84
117
 
85
- protected abstract override computeReference(
86
- parent: DescendantNodeParent<Definition>,
87
- definition: Definition
88
- ): string;
89
-
90
- protected buildSharedStateSpec(
91
- parent: DescendantNodeParent<Definition>,
92
- definition: Definition
93
- ): DescendantNodeSharedStateSpec {
94
- return this.scope.runTask(() => {
95
- const reference = createMemo(() => this.contextReference);
96
- const { bind } = definition;
97
-
98
- // TODO: we can likely short-circuit `readonly` computation when a node
99
- // is non-relevant.
100
- const selfReadonly = createComputedExpression(this, bind.readonly);
101
- const readonly = createMemo(() => {
102
- return parent.isReadonly || selfReadonly();
103
- });
104
-
105
- const selfRelevant = createComputedExpression(this, bind.relevant);
106
- const relevant = createMemo(() => {
107
- return parent.isRelevant && selfRelevant();
108
- });
118
+ const { readonly, relevant, required } = definition.bind;
109
119
 
110
- // TODO: we can likely short-circuit `required` computation when a node
111
- // is non-relevant.
112
- const required = createComputedExpression(this, bind.required);
113
-
114
- return {
115
- reference,
116
- readonly,
117
- relevant,
118
- required,
119
- };
120
- });
120
+ this.isSelfReadonly = createComputedExpression(this, readonly);
121
+ this.isSelfRelevant = createComputedExpression(this, relevant);
122
+ this.isRequired = createComputedExpression(this, required);
121
123
  }
122
124
 
123
125
  protected createContextNode(parentContextNode: Element, nodeName: string): Element {
@@ -1,7 +1,7 @@
1
1
  import type { XFormsXPathEvaluator } from '@getodk/xpath';
2
2
  import type { Accessor, Signal } from 'solid-js';
3
- import { createSignal } from 'solid-js';
4
3
  import type { BaseNode } from '../../client/BaseNode.ts';
4
+ import type { NodeAppearances } from '../../client/NodeAppearances.ts';
5
5
  import type { InstanceNodeType } from '../../client/node-types.ts';
6
6
  import type { TextRange } from '../../index.ts';
7
7
  import type { MaterializedChildren } from '../../lib/reactivity/materializeCurrentStateChildren.ts';
@@ -40,10 +40,6 @@ type AnyInstanceNode = InstanceNode<
40
40
  any
41
41
  >;
42
42
 
43
- interface InitializedStateOptions<T, K extends keyof T> {
44
- readonly uninitializedFallback: T[K];
45
- }
46
-
47
43
  /**
48
44
  * This type has the same effect as {@link MaterializedChildren}, but abstractly
49
45
  * handles leaf node types as well.
@@ -61,6 +57,21 @@ export type InstanceNodeCurrentState<
61
57
  : null;
62
58
  };
63
59
 
60
+ interface ComputableReferenceNode {
61
+ readonly parent: AnyParentNode | null;
62
+ readonly definition: AnyNodeDefinition;
63
+ }
64
+
65
+ type ComputeInstanceNodeReference = <This extends ComputableReferenceNode>(
66
+ this: This,
67
+ parent: This['parent'],
68
+ definition: This['definition']
69
+ ) => string;
70
+
71
+ export interface InstanceNodeOptions {
72
+ readonly computeReference?: () => string;
73
+ }
74
+
64
75
  export abstract class InstanceNode<
65
76
  Definition extends AnyNodeDefinition,
66
77
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -69,60 +80,32 @@ export abstract class InstanceNode<
69
80
  >
70
81
  implements BaseNode, EvaluationContext, SubscribableDependency
71
82
  {
72
- protected readonly isStateInitialized: Accessor<boolean>;
73
-
74
83
  protected abstract readonly state: SharedNodeState<Spec>;
75
84
  protected abstract readonly engineState: EngineState<Spec>;
76
85
 
77
86
  /**
78
- * Provides a generalized mechanism for accessing a reactive state value
79
- * during a node's construction, while {@link engineState} is still being
80
- * defined and thus isn't assigned.
81
- *
82
- * The fallback value specified in {@link options} will be returned on access
83
- * until {@link isStateInitialized} returns true. This ensures:
84
- *
85
- * - a value of the expected type will be available
86
- * - any read access will become reactive to the actual state, once it has
87
- * been initialized and {@link engineState} is assigned
88
- *
89
- * @todo This is one among several chicken/egg problems encountered trying to
90
- * support state initialization in which some aspects of the state derive from
91
- * other aspects of it. It would be nice to dispense with this entirely. But
92
- * if it must persist, we should also consider replacing the method with a
93
- * direct accessor once state initialization completes, so the initialized
94
- * check is only called until it becomes impertinent.
87
+ * @package Exposed on every node type to facilitate inheritance, as well as
88
+ * conditional behavior for value nodes.
95
89
  */
96
- protected getInitializedState<K extends keyof EngineState<Spec>>(
97
- key: K,
98
- options: InitializedStateOptions<EngineState<Spec>, K>
99
- ): EngineState<Spec>[K] {
100
- if (this.isStateInitialized()) {
101
- return this.engineState[key];
102
- }
90
+ abstract readonly hasReadonlyAncestor: Accessor<boolean>;
103
91
 
104
- return options.uninitializedFallback;
105
- }
92
+ /**
93
+ * @package Exposed on every node type to facilitate inheritance, as well as
94
+ * conditional behavior for value nodes.
95
+ */
96
+ abstract readonly isReadonly: Accessor<boolean>;
106
97
 
107
98
  /**
108
99
  * @package Exposed on every node type to facilitate inheritance, as well as
109
100
  * conditional behavior for value nodes.
110
101
  */
111
- get isReadonly(): boolean {
112
- return (this as AnyInstanceNode).getInitializedState('readonly', {
113
- uninitializedFallback: false,
114
- });
115
- }
102
+ abstract readonly hasNonRelevantAncestor: Accessor<boolean>;
116
103
 
117
104
  /**
118
105
  * @package Exposed on every node type to facilitate inheritance, as well as
119
106
  * conditional behavior for value nodes.
120
107
  */
121
- get isRelevant(): boolean {
122
- return (this as AnyInstanceNode).getInitializedState('relevant', {
123
- uninitializedFallback: true,
124
- });
125
- }
108
+ abstract readonly isRelevant: Accessor<boolean>;
126
109
 
127
110
  // BaseNode: identity
128
111
  readonly nodeId: NodeID;
@@ -130,6 +113,8 @@ export abstract class InstanceNode<
130
113
  // BaseNode: node types and variants (e.g. for narrowing)
131
114
  abstract readonly nodeType: InstanceNodeType;
132
115
 
116
+ abstract readonly appearances: NodeAppearances<Definition>;
117
+
133
118
  abstract readonly currentState: InstanceNodeCurrentState<Spec, Child>;
134
119
 
135
120
  // BaseNode: structural
@@ -141,35 +126,40 @@ export abstract class InstanceNode<
141
126
  // EvaluationContext *and* Subscribable: node-specific
142
127
  readonly scope: ReactiveScope;
143
128
 
129
+ readonly computeReference: ComputeInstanceNodeReference;
130
+
131
+ protected readonly computeChildStepReference: ComputeInstanceNodeReference = (
132
+ parent,
133
+ definition
134
+ ): string => {
135
+ if (parent == null) {
136
+ throw new Error(
137
+ 'Cannot compute child step reference of node without parent (was this called from `Root`?)'
138
+ );
139
+ }
140
+
141
+ return `${parent.contextReference()}/${definition.nodeName}`;
142
+ };
143
+
144
144
  // EvaluationContext: node-specific
145
- get contextReference(): string {
145
+ readonly contextReference = (): string => {
146
146
  return this.computeReference(this.parent, this.definition);
147
- }
147
+ };
148
148
 
149
149
  abstract readonly contextNode: Element;
150
150
 
151
151
  constructor(
152
152
  readonly engineConfig: InstanceConfig,
153
153
  readonly parent: AnyParentNode | null,
154
- readonly definition: Definition
154
+ readonly definition: Definition,
155
+ options?: InstanceNodeOptions
155
156
  ) {
157
+ this.computeReference = options?.computeReference ?? this.computeChildStepReference;
158
+
156
159
  this.scope = createReactiveScope();
157
160
  this.engineConfig = engineConfig;
158
161
  this.nodeId = declareNodeID(engineConfig.createUniqueId());
159
162
  this.definition = definition;
160
-
161
- const checkStateInitialized = () => this.engineState != null;
162
- const [isStateInitialized, setStateInitialized] = createSignal(checkStateInitialized());
163
-
164
- this.isStateInitialized = isStateInitialized;
165
-
166
- queueMicrotask(() => {
167
- if (checkStateInitialized()) {
168
- setStateInitialized(true);
169
- } else {
170
- throw new Error('Node state was never initialized');
171
- }
172
- });
173
163
  }
174
164
 
175
165
  /**
@@ -184,18 +174,13 @@ export abstract class InstanceNode<
184
174
  */
185
175
  abstract getChildren(this: AnyInstanceNode): readonly AnyChildNode[];
186
176
 
187
- protected abstract computeReference(
188
- parent: AnyInstanceNode | null,
189
- definition: Definition
190
- ): string;
191
-
192
- getNodeByReference(
177
+ getNodesByReference(
193
178
  this: AnyNode,
194
179
  visited: WeakSet<AnyNode>,
195
180
  dependencyReference: string
196
- ): SubscribableDependency | null {
181
+ ): readonly SubscribableDependency[] {
197
182
  if (visited.has(this)) {
198
- return null;
183
+ return [];
199
184
  }
200
185
 
201
186
  visited.add(this);
@@ -203,39 +188,37 @@ export abstract class InstanceNode<
203
188
  const { nodeset } = this.definition;
204
189
 
205
190
  if (dependencyReference === nodeset) {
206
- return this;
191
+ if (this.nodeType === 'repeat-instance') {
192
+ return [this.parent];
193
+ }
194
+
195
+ return [this];
207
196
  }
208
197
 
209
198
  if (
210
199
  dependencyReference.startsWith(`${nodeset}/`) ||
211
200
  dependencyReference.startsWith(`${nodeset}[`)
212
201
  ) {
213
- const children = this.getChildren();
214
-
215
- if (children == null) {
216
- return null;
217
- }
218
-
219
- for (const child of children) {
220
- const dependency = child.getNodeByReference(visited, dependencyReference);
221
-
222
- if (dependency != null) {
223
- return dependency;
224
- }
225
- }
202
+ return this.getChildren().flatMap((child) => {
203
+ return child.getNodesByReference(visited, dependencyReference);
204
+ });
226
205
  }
227
206
 
228
- return this.parent?.getNodeByReference(visited, dependencyReference) ?? null;
207
+ return this.parent?.getNodesByReference(visited, dependencyReference) ?? [];
229
208
  }
230
209
 
231
210
  // EvaluationContext: node-relative
232
- getSubscribableDependencyByReference(
211
+ getSubscribableDependenciesByReference(
233
212
  this: AnyNode,
234
213
  reference: string
235
- ): SubscribableDependency | null {
236
- const visited = new WeakSet<SubscribableDependency>();
214
+ ): readonly SubscribableDependency[] {
215
+ if (this.nodeType === 'root') {
216
+ const visited = new WeakSet<AnyNode>();
217
+
218
+ return this.getNodesByReference(visited, reference);
219
+ }
237
220
 
238
- return this.getNodeByReference(visited, reference);
221
+ return this.root.getSubscribableDependenciesByReference(reference);
239
222
  }
240
223
 
241
224
  // SubscribableDependency