@getodk/xforms-engine 0.1.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/README.md +44 -0
- package/dist/.vite/manifest.json +7 -0
- package/dist/XFormDOM.d.ts +31 -0
- package/dist/XFormDataType.d.ts +26 -0
- package/dist/XFormDefinition.d.ts +14 -0
- package/dist/body/BodyDefinition.d.ts +52 -0
- package/dist/body/BodyElementDefinition.d.ts +32 -0
- package/dist/body/RepeatDefinition.d.ts +15 -0
- package/dist/body/UnsupportedBodyElementDefinition.d.ts +10 -0
- package/dist/body/control/ControlDefinition.d.ts +16 -0
- package/dist/body/control/InputDefinition.d.ts +5 -0
- package/dist/body/control/select/ItemDefinition.d.ts +13 -0
- package/dist/body/control/select/ItemsetDefinition.d.ts +16 -0
- package/dist/body/control/select/ItemsetNodesetContext.d.ts +11 -0
- package/dist/body/control/select/ItemsetNodesetExpression.d.ts +5 -0
- package/dist/body/control/select/ItemsetValueExpression.d.ts +6 -0
- package/dist/body/control/select/SelectDefinition.d.ts +23 -0
- package/dist/body/group/BaseGroupDefinition.d.ts +46 -0
- package/dist/body/group/LogicalGroupDefinition.d.ts +6 -0
- package/dist/body/group/PresentationGroupDefinition.d.ts +11 -0
- package/dist/body/group/RepeatGroupDefinition.d.ts +12 -0
- package/dist/body/group/StructuralGroupDefinition.d.ts +6 -0
- package/dist/body/text/HintDefinition.d.ts +11 -0
- package/dist/body/text/LabelDefinition.d.ts +20 -0
- package/dist/body/text/TextElementDefinition.d.ts +32 -0
- package/dist/body/text/TextElementOutputPart.d.ts +12 -0
- package/dist/body/text/TextElementPart.d.ts +12 -0
- package/dist/body/text/TextElementReferencePart.d.ts +6 -0
- package/dist/body/text/TextElementStaticPart.d.ts +6 -0
- package/dist/client/BaseNode.d.ts +138 -0
- package/dist/client/EngineConfig.d.ts +78 -0
- package/dist/client/FormLanguage.d.ts +63 -0
- package/dist/client/GroupNode.d.ts +24 -0
- package/dist/client/OpaqueReactiveObjectFactory.d.ts +70 -0
- package/dist/client/RepeatInstanceNode.d.ts +28 -0
- package/dist/client/RepeatRangeNode.d.ts +94 -0
- package/dist/client/RootNode.d.ts +31 -0
- package/dist/client/SelectNode.d.ts +60 -0
- package/dist/client/StringNode.d.ts +41 -0
- package/dist/client/SubtreeNode.d.ts +52 -0
- package/dist/client/TextRange.d.ts +55 -0
- package/dist/client/hierarchy.d.ts +48 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/node-types.d.ts +1 -0
- package/dist/expression/DependencyContext.d.ts +12 -0
- package/dist/expression/DependentExpression.d.ts +43 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +37622 -0
- package/dist/index.js.map +1 -0
- package/dist/instance/Group.d.ts +31 -0
- package/dist/instance/RepeatInstance.d.ts +60 -0
- package/dist/instance/RepeatRange.d.ts +81 -0
- package/dist/instance/Root.d.ts +70 -0
- package/dist/instance/SelectField.d.ts +45 -0
- package/dist/instance/StringField.d.ts +39 -0
- package/dist/instance/Subtree.d.ts +30 -0
- package/dist/instance/abstract/DescendantNode.d.ts +76 -0
- package/dist/instance/abstract/InstanceNode.d.ts +107 -0
- package/dist/instance/children.d.ts +2 -0
- package/dist/instance/hierarchy.d.ts +12 -0
- package/dist/instance/identity.d.ts +7 -0
- package/dist/instance/index.d.ts +8 -0
- package/dist/instance/internal-api/EvaluationContext.d.ts +34 -0
- package/dist/instance/internal-api/InstanceConfig.d.ts +8 -0
- package/dist/instance/internal-api/SubscribableDependency.d.ts +59 -0
- package/dist/instance/internal-api/TranslationContext.d.ts +4 -0
- package/dist/instance/internal-api/ValueContext.d.ts +22 -0
- package/dist/instance/resource.d.ts +10 -0
- package/dist/instance/text/FormattedTextStub.d.ts +1 -0
- package/dist/instance/text/TextChunk.d.ts +11 -0
- package/dist/instance/text/TextRange.d.ts +10 -0
- package/dist/lib/dom/query.d.ts +20 -0
- package/dist/lib/reactivity/createChildrenState.d.ts +36 -0
- package/dist/lib/reactivity/createComputedExpression.d.ts +12 -0
- package/dist/lib/reactivity/createSelectItems.d.ts +16 -0
- package/dist/lib/reactivity/createValueState.d.ts +44 -0
- package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +18 -0
- package/dist/lib/reactivity/node-state/createClientState.d.ts +9 -0
- package/dist/lib/reactivity/node-state/createCurrentState.d.ts +6 -0
- package/dist/lib/reactivity/node-state/createEngineState.d.ts +5 -0
- package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +22 -0
- package/dist/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.d.ts +6 -0
- package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +139 -0
- package/dist/lib/reactivity/node-state/representations.d.ts +25 -0
- package/dist/lib/reactivity/scope.d.ts +23 -0
- package/dist/lib/reactivity/text/createFieldHint.d.ts +5 -0
- package/dist/lib/reactivity/text/createNodeLabel.d.ts +5 -0
- package/dist/lib/reactivity/text/createTextRange.d.ts +19 -0
- package/dist/lib/reactivity/types.d.ts +21 -0
- package/dist/lib/unique-id.d.ts +27 -0
- package/dist/lib/xpath/analysis.d.ts +22 -0
- package/dist/model/BindComputation.d.ts +30 -0
- package/dist/model/BindDefinition.d.ts +31 -0
- package/dist/model/BindElement.d.ts +6 -0
- package/dist/model/DescendentNodeDefinition.d.ts +25 -0
- package/dist/model/ModelBindMap.d.ts +15 -0
- package/dist/model/ModelDefinition.d.ts +10 -0
- package/dist/model/NodeDefinition.d.ts +74 -0
- package/dist/model/RepeatInstanceDefinition.d.ts +15 -0
- package/dist/model/RepeatSequenceDefinition.d.ts +19 -0
- package/dist/model/RepeatTemplateDefinition.d.ts +29 -0
- package/dist/model/RootDefinition.d.ts +24 -0
- package/dist/model/SubtreeDefinition.d.ts +14 -0
- package/dist/model/ValueNodeDefinition.d.ts +15 -0
- package/dist/solid.js +37273 -0
- package/dist/solid.js.map +1 -0
- package/package.json +87 -0
- package/src/XFormDOM.ts +224 -0
- package/src/XFormDataType.ts +64 -0
- package/src/XFormDefinition.ts +40 -0
- package/src/body/BodyDefinition.ts +202 -0
- package/src/body/BodyElementDefinition.ts +62 -0
- package/src/body/RepeatDefinition.ts +54 -0
- package/src/body/UnsupportedBodyElementDefinition.ts +17 -0
- package/src/body/control/ControlDefinition.ts +42 -0
- package/src/body/control/InputDefinition.ts +9 -0
- package/src/body/control/select/ItemDefinition.ts +31 -0
- package/src/body/control/select/ItemsetDefinition.ts +36 -0
- package/src/body/control/select/ItemsetNodesetContext.ts +26 -0
- package/src/body/control/select/ItemsetNodesetExpression.ts +8 -0
- package/src/body/control/select/ItemsetValueExpression.ts +11 -0
- package/src/body/control/select/SelectDefinition.ts +74 -0
- package/src/body/group/BaseGroupDefinition.ts +137 -0
- package/src/body/group/LogicalGroupDefinition.ts +11 -0
- package/src/body/group/PresentationGroupDefinition.ts +28 -0
- package/src/body/group/RepeatGroupDefinition.ts +91 -0
- package/src/body/group/StructuralGroupDefinition.ts +11 -0
- package/src/body/text/HintDefinition.ts +26 -0
- package/src/body/text/LabelDefinition.ts +54 -0
- package/src/body/text/TextElementDefinition.ts +97 -0
- package/src/body/text/TextElementOutputPart.ts +27 -0
- package/src/body/text/TextElementPart.ts +31 -0
- package/src/body/text/TextElementReferencePart.ts +21 -0
- package/src/body/text/TextElementStaticPart.ts +26 -0
- package/src/client/BaseNode.ts +180 -0
- package/src/client/EngineConfig.ts +83 -0
- package/src/client/FormLanguage.ts +77 -0
- package/src/client/GroupNode.ts +33 -0
- package/src/client/OpaqueReactiveObjectFactory.ts +100 -0
- package/src/client/README.md +39 -0
- package/src/client/RepeatInstanceNode.ts +41 -0
- package/src/client/RepeatRangeNode.ts +100 -0
- package/src/client/RootNode.ts +36 -0
- package/src/client/SelectNode.ts +69 -0
- package/src/client/StringNode.ts +46 -0
- package/src/client/SubtreeNode.ts +57 -0
- package/src/client/TextRange.ts +63 -0
- package/src/client/hierarchy.ts +63 -0
- package/src/client/index.ts +29 -0
- package/src/client/node-types.ts +10 -0
- package/src/expression/DependencyContext.ts +53 -0
- package/src/expression/DependentExpression.ts +102 -0
- package/src/index.ts +35 -0
- package/src/instance/Group.ts +82 -0
- package/src/instance/RepeatInstance.ts +164 -0
- package/src/instance/RepeatRange.ts +214 -0
- package/src/instance/Root.ts +264 -0
- package/src/instance/SelectField.ts +204 -0
- package/src/instance/StringField.ts +93 -0
- package/src/instance/Subtree.ts +79 -0
- package/src/instance/abstract/DescendantNode.ts +182 -0
- package/src/instance/abstract/InstanceNode.ts +257 -0
- package/src/instance/children.ts +52 -0
- package/src/instance/hierarchy.ts +54 -0
- package/src/instance/identity.ts +11 -0
- package/src/instance/index.ts +37 -0
- package/src/instance/internal-api/EvaluationContext.ts +41 -0
- package/src/instance/internal-api/InstanceConfig.ts +9 -0
- package/src/instance/internal-api/SubscribableDependency.ts +61 -0
- package/src/instance/internal-api/TranslationContext.ts +5 -0
- package/src/instance/internal-api/ValueContext.ts +27 -0
- package/src/instance/resource.ts +75 -0
- package/src/instance/text/FormattedTextStub.ts +8 -0
- package/src/instance/text/TextChunk.ts +20 -0
- package/src/instance/text/TextRange.ts +23 -0
- package/src/lib/dom/query.ts +49 -0
- package/src/lib/reactivity/createChildrenState.ts +60 -0
- package/src/lib/reactivity/createComputedExpression.ts +114 -0
- package/src/lib/reactivity/createSelectItems.ts +163 -0
- package/src/lib/reactivity/createValueState.ts +258 -0
- package/src/lib/reactivity/materializeCurrentStateChildren.ts +121 -0
- package/src/lib/reactivity/node-state/createClientState.ts +51 -0
- package/src/lib/reactivity/node-state/createCurrentState.ts +27 -0
- package/src/lib/reactivity/node-state/createEngineState.ts +18 -0
- package/src/lib/reactivity/node-state/createSharedNodeState.ts +79 -0
- package/src/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.ts +85 -0
- package/src/lib/reactivity/node-state/createSpecifiedState.ts +229 -0
- package/src/lib/reactivity/node-state/representations.ts +64 -0
- package/src/lib/reactivity/scope.ts +106 -0
- package/src/lib/reactivity/text/createFieldHint.ts +16 -0
- package/src/lib/reactivity/text/createNodeLabel.ts +16 -0
- package/src/lib/reactivity/text/createTextRange.ts +155 -0
- package/src/lib/reactivity/types.ts +27 -0
- package/src/lib/unique-id.ts +34 -0
- package/src/lib/xpath/analysis.ts +241 -0
- package/src/model/BindComputation.ts +88 -0
- package/src/model/BindDefinition.ts +104 -0
- package/src/model/BindElement.ts +8 -0
- package/src/model/DescendentNodeDefinition.ts +56 -0
- package/src/model/ModelBindMap.ts +71 -0
- package/src/model/ModelDefinition.ts +19 -0
- package/src/model/NodeDefinition.ts +146 -0
- package/src/model/RepeatInstanceDefinition.ts +39 -0
- package/src/model/RepeatSequenceDefinition.ts +53 -0
- package/src/model/RepeatTemplateDefinition.ts +150 -0
- package/src/model/RootDefinition.ts +121 -0
- package/src/model/SubtreeDefinition.ts +50 -0
- package/src/model/ValueNodeDefinition.ts +39 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { createComputed, createMemo, createSignal, untrack } from 'solid-js';
|
|
2
|
+
import type { DependentExpression } from '../../expression/DependentExpression.ts';
|
|
3
|
+
import type { SubscribableDependency } from '../../instance/internal-api/SubscribableDependency.ts';
|
|
4
|
+
import type { ValueContext } from '../../instance/internal-api/ValueContext.ts';
|
|
5
|
+
import type { BindComputation } from '../../model/BindComputation.ts';
|
|
6
|
+
import { createComputedExpression } from './createComputedExpression.ts';
|
|
7
|
+
import type { SimpleAtomicState, SimpleAtomicStateSetter } from './types.ts';
|
|
8
|
+
|
|
9
|
+
type InitialValueSource = 'FORM_DEFAULT' | 'PRIMARY_INSTANCE';
|
|
10
|
+
|
|
11
|
+
export interface ValueStateOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Specifies the source of a {@link createValueState} signal's initial
|
|
14
|
+
* value state, where:
|
|
15
|
+
*
|
|
16
|
+
* - 'FORM_DEFAULT': Derives the initial state from the form's
|
|
17
|
+
* definition of the node itself. This is the default option, appropriate
|
|
18
|
+
* when initializing a form without additional primary instance data. In
|
|
19
|
+
* other words, this value should not be used for edits.
|
|
20
|
+
*
|
|
21
|
+
* - 'PRIMARY_INSTANCE': Derives the initial state from the current text
|
|
22
|
+
* content of the {@link ValueNode.contextNode} (currently an XML DOM
|
|
23
|
+
* backing store/source of thruth for primary instance state). This option
|
|
24
|
+
* should be specified when initializing a form with existing primary
|
|
25
|
+
* instance data, such as when editing a previous submission.
|
|
26
|
+
*
|
|
27
|
+
* @default 'FORM_DEFAULT'
|
|
28
|
+
*
|
|
29
|
+
* Specifies whether a {@link createV} signal's initial state
|
|
30
|
+
* should be derived from the current text content of the
|
|
31
|
+
* {@link ValueNode.contextNode | primary instance DOM state}.
|
|
32
|
+
*/
|
|
33
|
+
readonly initialValueSource?: InitialValueSource;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PersistedValueState {
|
|
37
|
+
readonly isRelevant: boolean;
|
|
38
|
+
readonly value: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type PrimaryInstanceValueState = SimpleAtomicState<string>;
|
|
42
|
+
|
|
43
|
+
type ValueState<RuntimeValue> = SimpleAtomicState<RuntimeValue>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a signal which:
|
|
47
|
+
*
|
|
48
|
+
* 1. Persists its state to the primary instance's
|
|
49
|
+
* {@link ValueContext.contextNode | contextNode} on state changes.
|
|
50
|
+
* 2. Propagates downstream reactivity only after that write is persisted.
|
|
51
|
+
*
|
|
52
|
+
* This ensures that reactive subscriptions get a consistent view of a node's
|
|
53
|
+
* current state, regardless of whether they derive state from values in the
|
|
54
|
+
* primary instance (currently: computed {@link DependentExpression}
|
|
55
|
+
* evaluations) or from other aspects of reactive runtime state (generally,
|
|
56
|
+
* everything besides computed {@link DependentExpression}s).
|
|
57
|
+
*/
|
|
58
|
+
const createPrimaryInstanceValueState = <RuntimeValue>(
|
|
59
|
+
context: ValueContext<RuntimeValue>,
|
|
60
|
+
options: ValueStateOptions
|
|
61
|
+
): PrimaryInstanceValueState => {
|
|
62
|
+
const { contextNode, definition, scope } = context;
|
|
63
|
+
const { initialValueSource } = options;
|
|
64
|
+
const { defaultValue } = definition;
|
|
65
|
+
|
|
66
|
+
return scope.runTask(() => {
|
|
67
|
+
// prettier-ignore
|
|
68
|
+
const initialValue =
|
|
69
|
+
initialValueSource === 'PRIMARY_INSTANCE'
|
|
70
|
+
? contextNode.textContent ?? defaultValue
|
|
71
|
+
: defaultValue;
|
|
72
|
+
|
|
73
|
+
const persistedValueState = createSignal<PersistedValueState>(
|
|
74
|
+
{
|
|
75
|
+
isRelevant: context.isRelevant,
|
|
76
|
+
value: initialValue,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
// This could return a single boolean expression, but checking each
|
|
80
|
+
// equality condition separately feels like it more clearly expresses
|
|
81
|
+
// the intent.
|
|
82
|
+
equals: (previous, updated) => {
|
|
83
|
+
if (updated.isRelevant !== previous.isRelevant) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return updated.value === previous.value;
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
const [persistedValue, setValueForPersistence] = persistedValueState;
|
|
92
|
+
|
|
93
|
+
createComputed(() => {
|
|
94
|
+
const isRelevant = context.isRelevant;
|
|
95
|
+
|
|
96
|
+
setValueForPersistence((persisted) => {
|
|
97
|
+
return {
|
|
98
|
+
isRelevant,
|
|
99
|
+
value: persisted.value,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const toSignalValue = (current: PersistedValueState): string => {
|
|
105
|
+
if (current.isRelevant) {
|
|
106
|
+
return current.value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return '';
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const [signalValue, setSignalValue] = createSignal(toSignalValue(persistedValue()));
|
|
113
|
+
|
|
114
|
+
createComputed(() => {
|
|
115
|
+
const { isRelevant, value } = persistedValue();
|
|
116
|
+
const preparedTextContent = isRelevant ? value : '';
|
|
117
|
+
const assignedTextContent = (contextNode.textContent = preparedTextContent);
|
|
118
|
+
|
|
119
|
+
setSignalValue(assignedTextContent);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const setPrimaryInstanceValue: SimpleAtomicStateSetter<string> = (value) => {
|
|
123
|
+
// TODO: Check (error?) for non-relevant value change?
|
|
124
|
+
const persisted = setValueForPersistence({
|
|
125
|
+
isRelevant: context.isRelevant,
|
|
126
|
+
value,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return persisted.value;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return [signalValue, setPrimaryInstanceValue];
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Wraps a node's {@link PrimaryInstanceValueState} in a signal-like interface
|
|
138
|
+
* which automatically encodes and decodes a node's runtime value
|
|
139
|
+
* representation:
|
|
140
|
+
*
|
|
141
|
+
* - Values read by a node will be read from the current state as persisted in
|
|
142
|
+
* the primary instance, then {@link ValueContext.decodeValue | decoded} (as
|
|
143
|
+
* implemented by that node) into a format suitable for the rest of the
|
|
144
|
+
* functionality provided by that node.
|
|
145
|
+
*
|
|
146
|
+
* - Values written by a node will be {@link ValueContext.encodeValue | encoded}
|
|
147
|
+
* (also as implemented by that node) into a string appropriate to persist to
|
|
148
|
+
* the primary instance, and written to it as such.
|
|
149
|
+
*
|
|
150
|
+
* - Downstream reactive computations should subscribe to updates to this
|
|
151
|
+
* runtime state (suggesting the value node itself should access this state in
|
|
152
|
+
* its {@link SubscribableDependency.subscribe} implementation).
|
|
153
|
+
*/
|
|
154
|
+
const createRuntimeValueState = <RuntimeValue>(
|
|
155
|
+
context: ValueContext<RuntimeValue>,
|
|
156
|
+
primaryInstanceState: PrimaryInstanceValueState
|
|
157
|
+
): ValueState<RuntimeValue> => {
|
|
158
|
+
const { decodeValue, encodeValue } = context;
|
|
159
|
+
|
|
160
|
+
return context.scope.runTask(() => {
|
|
161
|
+
const [primaryInstanceValue, setPrimaryInstanceValue] = primaryInstanceState;
|
|
162
|
+
const getRuntimeValue = createMemo(() => {
|
|
163
|
+
return decodeValue(primaryInstanceValue());
|
|
164
|
+
});
|
|
165
|
+
const setRuntimeValue: SimpleAtomicStateSetter<RuntimeValue> = (value) => {
|
|
166
|
+
const encodedValue = encodeValue(value);
|
|
167
|
+
|
|
168
|
+
return decodeValue(setPrimaryInstanceValue(encodedValue));
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return [getRuntimeValue, setRuntimeValue];
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* For fields with a `readonly` bind expression, prevent downstream
|
|
177
|
+
* (client/user) writes when the field is in a `readonly` state.
|
|
178
|
+
*/
|
|
179
|
+
const guardDownstreamReadonlyWrites = <RuntimeValue>(
|
|
180
|
+
context: ValueContext<RuntimeValue>,
|
|
181
|
+
baseState: SimpleAtomicState<RuntimeValue>
|
|
182
|
+
): SimpleAtomicState<RuntimeValue> => {
|
|
183
|
+
const { readonly } = context.definition.bind;
|
|
184
|
+
|
|
185
|
+
if (readonly.isDefaultExpression) {
|
|
186
|
+
return baseState;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const [getValue, baseSetValue] = baseState;
|
|
190
|
+
|
|
191
|
+
const setValue: SimpleAtomicStateSetter<RuntimeValue> = (value) => {
|
|
192
|
+
if (context.isReadonly) {
|
|
193
|
+
const reference = untrack(() => context.contextReference);
|
|
194
|
+
|
|
195
|
+
throw new Error(`Cannot write to readonly field: ${reference}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return baseSetValue(value);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return [getValue, setValue];
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Defines a reactive effect which writes the result of `calculate` bind
|
|
206
|
+
* computations to the provided value setter, on initialization and any
|
|
207
|
+
* subsequent reactive update.
|
|
208
|
+
*/
|
|
209
|
+
const createCalculation = <RuntimeValue>(
|
|
210
|
+
context: ValueContext<RuntimeValue>,
|
|
211
|
+
setValue: SimpleAtomicStateSetter<RuntimeValue>,
|
|
212
|
+
calculateDefinition: BindComputation<'calculate'>
|
|
213
|
+
): void => {
|
|
214
|
+
context.scope.runTask(() => {
|
|
215
|
+
const calculate = createComputedExpression(context, calculateDefinition);
|
|
216
|
+
|
|
217
|
+
createComputed(() => {
|
|
218
|
+
if (context.isRelevant) {
|
|
219
|
+
const calculated = calculate();
|
|
220
|
+
const value = context.decodeValue(calculated);
|
|
221
|
+
|
|
222
|
+
setValue(value);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Provides a consistent interface for value nodes of any type which:
|
|
230
|
+
*
|
|
231
|
+
* - derives initial state from either the existing primary instance state (e.g.
|
|
232
|
+
* for edits) or the node's definition (e.g. initializing a new submission)
|
|
233
|
+
* - decodes primary instance state into the value node's runtime type
|
|
234
|
+
* - encodes and persists updated values to the primary instance
|
|
235
|
+
* - initializes reactive computation of `calculate` bind expressions for those
|
|
236
|
+
* nodes defined with one
|
|
237
|
+
* - ensures any downstream reactive dependencies are updated only after writes
|
|
238
|
+
* (whether performed by a client, or by a reactive `calculate` computation)
|
|
239
|
+
* are persisted, ensuring a consistent view of state when downstream
|
|
240
|
+
* computations perform XPath evaluations against that primary instance state
|
|
241
|
+
*/
|
|
242
|
+
export const createValueState = <RuntimeValue>(
|
|
243
|
+
context: ValueContext<RuntimeValue>,
|
|
244
|
+
options: ValueStateOptions = {}
|
|
245
|
+
): ValueState<RuntimeValue> => {
|
|
246
|
+
const primaryInstanceState = createPrimaryInstanceValueState(context, options);
|
|
247
|
+
const runtimeState = createRuntimeValueState(context, primaryInstanceState);
|
|
248
|
+
|
|
249
|
+
const { calculate } = context.definition.bind;
|
|
250
|
+
|
|
251
|
+
if (calculate != null) {
|
|
252
|
+
const [, setValue] = runtimeState;
|
|
253
|
+
|
|
254
|
+
createCalculation(context, setValue, calculate);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return guardDownstreamReadonlyWrites(context, runtimeState);
|
|
258
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { AnyChildNode } from '../../instance/hierarchy.ts';
|
|
2
|
+
import type { NodeID } from '../../instance/identity.ts';
|
|
3
|
+
import type { ChildrenState, createChildrenState } from './createChildrenState.ts';
|
|
4
|
+
import type { ClientState } from './node-state/createClientState.ts';
|
|
5
|
+
import type { CurrentState } from './node-state/createCurrentState.ts';
|
|
6
|
+
|
|
7
|
+
interface InconsistentChildrenStateDetails {
|
|
8
|
+
readonly missingIds: readonly NodeID[];
|
|
9
|
+
readonly unexpectedIds: readonly NodeID[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class InconsistentChildrenStateError extends Error {
|
|
13
|
+
constructor(details: InconsistentChildrenStateDetails) {
|
|
14
|
+
const { missingIds, unexpectedIds } = details;
|
|
15
|
+
|
|
16
|
+
const messageLines = ['Detected inconsistent engine/client child state.'];
|
|
17
|
+
|
|
18
|
+
if (missingIds.length > 0) {
|
|
19
|
+
const missingIdLines = missingIds.map((missingId) => {
|
|
20
|
+
return `- ${missingId}`;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
messageLines.push('\nMissing child nodes for ids:\n', ...missingIdLines);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (unexpectedIds.length > 0) {
|
|
27
|
+
const unexpectedIdLines = unexpectedIds.map((unexpectedId) => {
|
|
28
|
+
return `- ${unexpectedId}`;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
messageLines.push('\nUnexpected child nodes with ids:\n', ...unexpectedIdLines);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
super(messageLines.join('\n'));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface EncodedParentState {
|
|
39
|
+
readonly children: readonly NodeID[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* For potential use when debugging in dev mode. The assumption is that we do
|
|
44
|
+
* not believe the implementation **should** produce inconsistencies between:
|
|
45
|
+
*
|
|
46
|
+
* 1. The internal `children` state of a node
|
|
47
|
+
* 2. The corresponding, internally computed `engineState.children` node ID list
|
|
48
|
+
* 3. The reactively written `clientState.children` node ID list
|
|
49
|
+
* 4. The reactively read `currentState.children` node ID list
|
|
50
|
+
* 5. The read state of #1 as produced when a client reads #4
|
|
51
|
+
*
|
|
52
|
+
* For now we can check for this in dev mode and warn if any aspect of this
|
|
53
|
+
* assumption deviates from reality. We should aim to confirm this so that we
|
|
54
|
+
* can confidently skip this check in production (as it would effectively be
|
|
55
|
+
* wasted CPU cycles).
|
|
56
|
+
*
|
|
57
|
+
* @todo should we throw rather than warn until we have this confidence?
|
|
58
|
+
*/
|
|
59
|
+
const reportInconsistentChildrenState = (
|
|
60
|
+
expectedClientIds: readonly NodeID[],
|
|
61
|
+
actualNodes: readonly AnyChildNode[]
|
|
62
|
+
): void => {
|
|
63
|
+
const actualIds = actualNodes.map((node) => node.nodeId);
|
|
64
|
+
const missingIds = expectedClientIds.filter((expectedId) => {
|
|
65
|
+
return !actualIds.includes(expectedId);
|
|
66
|
+
});
|
|
67
|
+
const unexpectedIds = actualIds.filter((actualId) => {
|
|
68
|
+
return !expectedClientIds.includes(actualId);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (missingIds.length > 0 || unexpectedIds.length > 0) {
|
|
72
|
+
throw new InconsistentChildrenStateError({
|
|
73
|
+
missingIds,
|
|
74
|
+
unexpectedIds,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// prettier-ignore
|
|
80
|
+
export type MaterializedChildren<
|
|
81
|
+
BaseState extends EncodedParentState,
|
|
82
|
+
Child extends AnyChildNode | null
|
|
83
|
+
> =
|
|
84
|
+
& Omit<BaseState, 'children'>
|
|
85
|
+
& { readonly children: readonly Child[] };
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a wrapper proxy around a parent node's {@link CurrentState} to map
|
|
89
|
+
* `children` state, which is written to the node's (internal, synchronized)
|
|
90
|
+
* {@link ClientState} as an array of {@link NodeID}s, back to the node objects
|
|
91
|
+
* corresponding to those IDs.
|
|
92
|
+
*
|
|
93
|
+
* @see {@link createChildrenState} for further detail.
|
|
94
|
+
*/
|
|
95
|
+
export const materializeCurrentStateChildren = <
|
|
96
|
+
Child extends AnyChildNode,
|
|
97
|
+
ParentState extends EncodedParentState,
|
|
98
|
+
>(
|
|
99
|
+
currentState: ParentState,
|
|
100
|
+
childrenState: ChildrenState<Child>
|
|
101
|
+
): MaterializedChildren<ParentState, Child> => {
|
|
102
|
+
const baseState: Omit<ParentState, 'children'> = currentState;
|
|
103
|
+
const proxyTarget = baseState as MaterializedChildren<ParentState, Child>;
|
|
104
|
+
|
|
105
|
+
return new Proxy(proxyTarget, {
|
|
106
|
+
get(_, key) {
|
|
107
|
+
if (key === 'children') {
|
|
108
|
+
const expectedChildIDs = currentState.children;
|
|
109
|
+
const children = childrenState.getChildren();
|
|
110
|
+
|
|
111
|
+
if (import.meta.env.DEV) {
|
|
112
|
+
reportInconsistentChildrenState(expectedChildIDs, children);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return children;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return baseState[key as Exclude<keyof ParentState, 'children'>];
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getPropertyKeys } from '@getodk/common/lib/objects/structure.ts';
|
|
2
|
+
import type { ShallowMutable } from '@getodk/common/types/helpers.js';
|
|
3
|
+
import { createComputed, untrack } from 'solid-js';
|
|
4
|
+
import type { OpaqueReactiveObjectFactory } from '../../../index.ts';
|
|
5
|
+
import type { ReactiveScope } from '../scope.ts';
|
|
6
|
+
import type { EngineState } from './createEngineState.ts';
|
|
7
|
+
import type { SpecifiedState, StateSpec } from './createSpecifiedState.ts';
|
|
8
|
+
import type { InternalClientRepresentation } from './representations.ts';
|
|
9
|
+
import { declareInternalClientRepresentation } from './representations.ts';
|
|
10
|
+
|
|
11
|
+
const deriveInitialState = <Spec extends StateSpec>(
|
|
12
|
+
scope: ReactiveScope,
|
|
13
|
+
engineState: EngineState<Spec>
|
|
14
|
+
): ShallowMutable<SpecifiedState<Spec>> => {
|
|
15
|
+
return scope.runTask(() => {
|
|
16
|
+
return untrack(() => {
|
|
17
|
+
return { ...engineState };
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SpecifiedClientStateFactory<
|
|
23
|
+
Factory extends OpaqueReactiveObjectFactory,
|
|
24
|
+
Spec extends StateSpec,
|
|
25
|
+
> = ShallowMutable<SpecifiedState<Spec>> extends Parameters<Factory>[0] ? Factory : never;
|
|
26
|
+
|
|
27
|
+
export type ClientState<Spec extends StateSpec> = InternalClientRepresentation<
|
|
28
|
+
SpecifiedState<Spec>
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
export const createClientState = <
|
|
32
|
+
Factory extends OpaqueReactiveObjectFactory,
|
|
33
|
+
Spec extends StateSpec,
|
|
34
|
+
>(
|
|
35
|
+
scope: ReactiveScope,
|
|
36
|
+
engineState: EngineState<Spec>,
|
|
37
|
+
clientStateFactory: SpecifiedClientStateFactory<Factory, Spec>
|
|
38
|
+
): ClientState<Spec> => {
|
|
39
|
+
const initialState = deriveInitialState(scope, engineState);
|
|
40
|
+
const clientState = clientStateFactory(initialState);
|
|
41
|
+
|
|
42
|
+
scope.runTask(() => {
|
|
43
|
+
getPropertyKeys(initialState).forEach((key) => {
|
|
44
|
+
createComputed(() => {
|
|
45
|
+
clientState[key] = engineState[key];
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return declareInternalClientRepresentation(clientState);
|
|
51
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactiveScope } from '../scope.ts';
|
|
2
|
+
import type { ClientState } from './createClientState.ts';
|
|
3
|
+
import type { SpecifiedState, StateSpec } from './createSpecifiedState.ts';
|
|
4
|
+
import type { ReadonlyClientRepresentation } from './representations.ts';
|
|
5
|
+
import { declareReadonlyClientRepresentation } from './representations.ts';
|
|
6
|
+
|
|
7
|
+
export type CurrentState<Spec extends StateSpec> = ReadonlyClientRepresentation<
|
|
8
|
+
SpecifiedState<Spec>
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
export const createCurrentState = <Spec extends StateSpec>(
|
|
12
|
+
scope: ReactiveScope,
|
|
13
|
+
clientState: ClientState<Spec>
|
|
14
|
+
): CurrentState<Spec> => {
|
|
15
|
+
return scope.runTask(() => {
|
|
16
|
+
const currentStateProxy = new Proxy<Readonly<SpecifiedState<Spec>>>(clientState, {
|
|
17
|
+
get: (_, key) => {
|
|
18
|
+
return clientState[key as keyof SpecifiedState<Spec>];
|
|
19
|
+
},
|
|
20
|
+
set: () => {
|
|
21
|
+
throw new TypeError('Cannot write directly to client-facing currentState');
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return declareReadonlyClientRepresentation(currentStateProxy);
|
|
26
|
+
});
|
|
27
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactiveScope } from '../scope.ts';
|
|
2
|
+
import type { SpecifiedState, StateSpec } from './createSpecifiedState.ts';
|
|
3
|
+
import { createSpecifiedState } from './createSpecifiedState.ts';
|
|
4
|
+
import type { EngineRepresentation } from './representations.ts';
|
|
5
|
+
import { declareEngineRepresentation } from './representations.ts';
|
|
6
|
+
|
|
7
|
+
export type EngineState<Spec extends StateSpec> = EngineRepresentation<SpecifiedState<Spec>>;
|
|
8
|
+
|
|
9
|
+
export const createEngineState = <Spec extends StateSpec>(
|
|
10
|
+
scope: ReactiveScope,
|
|
11
|
+
spec: Spec
|
|
12
|
+
): EngineState<Spec> => {
|
|
13
|
+
return scope.runTask(() => {
|
|
14
|
+
const state = createSpecifiedState(spec);
|
|
15
|
+
|
|
16
|
+
return declareEngineRepresentation(state);
|
|
17
|
+
});
|
|
18
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getPropertyKeys } from '@getodk/common/lib/objects/structure.ts';
|
|
2
|
+
import type { OpaqueReactiveObjectFactory } from '../../../index.ts';
|
|
3
|
+
import type { ReactiveScope } from '../scope.ts';
|
|
4
|
+
import type { ClientState, SpecifiedClientStateFactory } from './createClientState.ts';
|
|
5
|
+
import { createClientState } from './createClientState.ts';
|
|
6
|
+
import type { CurrentState } from './createCurrentState.ts';
|
|
7
|
+
import { createCurrentState } from './createCurrentState.ts';
|
|
8
|
+
import type { EngineState } from './createEngineState.ts';
|
|
9
|
+
import { createEngineState } from './createEngineState.ts';
|
|
10
|
+
import type { MutablePropertySpec, SpecifiedState, StateSpec } from './createSpecifiedState.ts';
|
|
11
|
+
import { isComputedPropertySpec, isMutablePropertySpec } from './createSpecifiedState.ts';
|
|
12
|
+
|
|
13
|
+
// prettier-ignore
|
|
14
|
+
type MutableKeyOf<Spec extends StateSpec> = {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
[K in string & keyof Spec]: Spec[K] extends MutablePropertySpec<any>
|
|
17
|
+
? K
|
|
18
|
+
: never;
|
|
19
|
+
}[string & keyof Spec];
|
|
20
|
+
|
|
21
|
+
type SetEnginePropertyState<Spec extends StateSpec> = <K extends MutableKeyOf<Spec>>(
|
|
22
|
+
key: K,
|
|
23
|
+
newValue: SpecifiedState<Spec>[K]
|
|
24
|
+
) => SpecifiedState<Spec>[K];
|
|
25
|
+
|
|
26
|
+
export interface SharedNodeState<Spec extends StateSpec> {
|
|
27
|
+
readonly spec: Spec;
|
|
28
|
+
readonly engineState: EngineState<Spec>;
|
|
29
|
+
readonly clientState: ClientState<Spec>;
|
|
30
|
+
readonly currentState: CurrentState<Spec>;
|
|
31
|
+
readonly setProperty: SetEnginePropertyState<Spec>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SharedNodeStateOptions<
|
|
35
|
+
Factory extends OpaqueReactiveObjectFactory,
|
|
36
|
+
Spec extends StateSpec,
|
|
37
|
+
> {
|
|
38
|
+
readonly clientStateFactory: SpecifiedClientStateFactory<Factory, Spec>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const createSharedNodeState = <
|
|
42
|
+
Factory extends OpaqueReactiveObjectFactory,
|
|
43
|
+
Spec extends StateSpec,
|
|
44
|
+
>(
|
|
45
|
+
scope: ReactiveScope,
|
|
46
|
+
spec: Spec,
|
|
47
|
+
options: SharedNodeStateOptions<Factory, Spec>
|
|
48
|
+
): SharedNodeState<Spec> => {
|
|
49
|
+
const engineState = createEngineState(scope, spec);
|
|
50
|
+
const clientState = createClientState(scope, engineState, options.clientStateFactory);
|
|
51
|
+
const currentState = createCurrentState(scope, clientState);
|
|
52
|
+
|
|
53
|
+
const specKeys = getPropertyKeys(spec);
|
|
54
|
+
const mutableKeys = specKeys.filter((key) => {
|
|
55
|
+
return isMutablePropertySpec(spec[key]);
|
|
56
|
+
});
|
|
57
|
+
const computedKeys = specKeys.filter((key) => {
|
|
58
|
+
return isComputedPropertySpec(spec[key]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const setProperty: SetEnginePropertyState<Spec> = (key, value) => {
|
|
62
|
+
if (!mutableKeys.includes(key)) {
|
|
63
|
+
const specType = computedKeys.includes(key) ? 'computed' : 'static';
|
|
64
|
+
throw new TypeError(`Cannot write to '${key}': property is ${specType}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return scope.runTask(() => {
|
|
68
|
+
return (engineState[key] = value);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
spec,
|
|
74
|
+
engineState,
|
|
75
|
+
clientState,
|
|
76
|
+
currentState,
|
|
77
|
+
setProperty,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts';
|
|
2
|
+
import type {
|
|
3
|
+
ComputedPropertySpec,
|
|
4
|
+
MutablePropertySpec,
|
|
5
|
+
StatePropertySpec,
|
|
6
|
+
StaticPropertySpec,
|
|
7
|
+
} from './createSpecifiedState.ts';
|
|
8
|
+
import {
|
|
9
|
+
isComputedPropertySpec,
|
|
10
|
+
isMutablePropertySpec,
|
|
11
|
+
isStaticPropertySpec,
|
|
12
|
+
} from './createSpecifiedState.ts';
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
export interface SpecifiedPropertyDescriptor<T = any> extends TypedPropertyDescriptor<T> {
|
|
16
|
+
readonly configurable: true;
|
|
17
|
+
readonly enumerable: true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface MutableDescriptor<T> extends SpecifiedPropertyDescriptor<T> {
|
|
21
|
+
readonly get: () => T;
|
|
22
|
+
readonly set: (newValue: T) => void;
|
|
23
|
+
readonly writable?: never;
|
|
24
|
+
readonly value?: never;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const mutableDesciptor = <T>(propertySpec: MutablePropertySpec<T>): MutableDescriptor<T> => {
|
|
28
|
+
const [get, set] = propertySpec;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
configurable: true,
|
|
32
|
+
enumerable: true,
|
|
33
|
+
get,
|
|
34
|
+
set,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface ComputedDescriptor<T> extends SpecifiedPropertyDescriptor<T> {
|
|
39
|
+
readonly get: () => T;
|
|
40
|
+
readonly set?: never;
|
|
41
|
+
readonly writable?: never;
|
|
42
|
+
readonly value?: never;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const computedDescriptor = <T>(propertySpec: ComputedPropertySpec<T>): ComputedDescriptor<T> => {
|
|
46
|
+
return {
|
|
47
|
+
configurable: true,
|
|
48
|
+
enumerable: true,
|
|
49
|
+
get: propertySpec,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
interface StaticDescriptor<T> extends SpecifiedPropertyDescriptor<T> {
|
|
54
|
+
readonly get?: never;
|
|
55
|
+
readonly set?: never;
|
|
56
|
+
readonly writable: false;
|
|
57
|
+
readonly value: T;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const staticDescriptor = <T>(propertySpec: StaticPropertySpec<T>): StaticDescriptor<T> => {
|
|
61
|
+
return {
|
|
62
|
+
configurable: true,
|
|
63
|
+
enumerable: true,
|
|
64
|
+
writable: false,
|
|
65
|
+
value: propertySpec,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const createSpecifiedPropertyDescriptor = <T>(
|
|
70
|
+
propertySpec: StatePropertySpec<T>
|
|
71
|
+
): SpecifiedPropertyDescriptor<T> => {
|
|
72
|
+
if (isMutablePropertySpec(propertySpec)) {
|
|
73
|
+
return mutableDesciptor(propertySpec);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isComputedPropertySpec(propertySpec)) {
|
|
77
|
+
return computedDescriptor(propertySpec);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isStaticPropertySpec(propertySpec)) {
|
|
81
|
+
return staticDescriptor(propertySpec);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new UnreachableError(propertySpec);
|
|
85
|
+
};
|