diff --git a/packages/xforms-engine/src/client/BaseNode.ts b/packages/xforms-engine/src/client/BaseNode.ts index 7c06ca7be..5fec6402c 100644 --- a/packages/xforms-engine/src/client/BaseNode.ts +++ b/packages/xforms-engine/src/client/BaseNode.ts @@ -3,6 +3,7 @@ import type { AnyNodeDefinition } from '../parse/model/NodeDefinition.ts'; import type { NodeAppearances } from './NodeAppearances.ts'; import type { OpaqueReactiveObjectFactory } from './OpaqueReactiveObjectFactory.ts'; import type { TextRange } from './TextRange.ts'; +import type { FormNodeID } from './identity.ts'; import type { InstanceNodeType } from './node-types.ts'; import type { AncestorNodeValidationState, @@ -129,8 +130,6 @@ export interface BaseNodeState { get value(): unknown; } -type FormNodeID = string; - /** * Base interface for common/shared aspects of any node type. */ diff --git a/packages/xforms-engine/src/client/RootNode.ts b/packages/xforms-engine/src/client/RootNode.ts index 32adee1a7..8ca57cfcf 100644 --- a/packages/xforms-engine/src/client/RootNode.ts +++ b/packages/xforms-engine/src/client/RootNode.ts @@ -3,6 +3,8 @@ import type { RootDefinition } from '../parse/model/RootDefinition.ts'; import type { BaseNode, BaseNodeState } from './BaseNode.ts'; import type { ActiveLanguage, FormLanguage, FormLanguages } from './FormLanguage.ts'; import type { GeneralChildNode } from './hierarchy.ts'; +import type { SubmissionChunkedType, SubmissionOptions } from './submission/SubmissionOptions.ts'; +import type { SubmissionResult } from './submission/SubmissionResult.ts'; import type { AncestorNodeValidationState } from './validation.ts'; export interface RootNodeState extends BaseNodeState { @@ -57,4 +59,35 @@ export interface RootNode extends BaseNode { readonly languages: FormLanguages; setLanguage(language: FormLanguage): RootNode; + + /** + * Prepares the current form instance state for submission. + * + * A {@link SubmissionResult} will be prepared even if the current form state + * includes `constraint` or `required` violations. This is intended to serve + * two purposes: + * + * - A client may effectively use this method as a part of its own "submit" + * routine, and use any violations included in the {@link SubmissionResult} + * to prompt users to address those violations. + * + * - A client may inspect the submission state of a form at any time. + * Depending on the client and use case, this may be a convenience (e.g. for + * developers to inspect that form state at a current point in time); or it + * may provide necessary functionality (e.g. for test or tooling clients). + * + * Note on asynchrony: preparing a {@link SubmissionResult} is expected to be + * a fast operation. It may even be nearly instantaneous, or roughly + * proportionate to the size of the form itself. However, this method is + * designed to be asynchronous out of an abundance of caution, anticipating + * that some as-yet undeveloped operations on binary data (e.g. form + * attachments) may themselves impose asynchrony (i.e. by interfaces provided + * by the platform and/or external dependencies). + * + * A client may specify {@link SubmissionOptions<'chunked'>}, in which case a + * {@link SubmissionResult<'chunked'>} will be produced, with form attachments + */ + prepareSubmission( + options?: SubmissionOptions + ): Promise>; } diff --git a/packages/xforms-engine/src/client/constants.ts b/packages/xforms-engine/src/client/constants.ts index 0fb7f1a15..1c722e1bb 100644 --- a/packages/xforms-engine/src/client/constants.ts +++ b/packages/xforms-engine/src/client/constants.ts @@ -8,3 +8,9 @@ export const VALIDATION_TEXT = { type ValidationTextDefaults = typeof VALIDATION_TEXT; export type ValidationTextDefault = ValidationTextDefaults[Role]; + +export const SUBMISSION_INSTANCE_FILE_NAME = 'xml_submission_file'; +export type SubmissionInstanceFileName = typeof SUBMISSION_INSTANCE_FILE_NAME; + +export const SUBMISSION_INSTANCE_FILE_TYPE = 'text/xml'; +export type SubmissionInstanceFileType = typeof SUBMISSION_INSTANCE_FILE_TYPE; diff --git a/packages/xforms-engine/src/client/identity.ts b/packages/xforms-engine/src/client/identity.ts new file mode 100644 index 000000000..e685582f6 --- /dev/null +++ b/packages/xforms-engine/src/client/identity.ts @@ -0,0 +1,16 @@ +type ODKXFormsUUID = `uuid:${string}`; + +/** + * @see {@link https://getodk.github.io/xforms-spec/#metadata} + */ +export type InstanceID = ODKXFormsUUID; + +/** + * @see {@link https://getodk.github.io/xforms-spec/#metadata} + */ +export type DeprecatedID = ODKXFormsUUID; + +/** + * Represents a session-stable identifier for any particular node i + */ +export type FormNodeID = `node:${string}`; diff --git a/packages/xforms-engine/src/client/submission/SubmissionData.ts b/packages/xforms-engine/src/client/submission/SubmissionData.ts new file mode 100644 index 000000000..d9dcb3408 --- /dev/null +++ b/packages/xforms-engine/src/client/submission/SubmissionData.ts @@ -0,0 +1,12 @@ +import type { + SubmissionInstanceFile, + SubmissionInstanceFileName, +} from './SubmissionInstanceFile.ts'; + +export interface SubmissionData extends FormData { + get(name: SubmissionInstanceFileName): SubmissionInstanceFile; + get(name: string): FormDataEntryValue | null; + + has(name: SubmissionInstanceFileName): true; + has(name: string): boolean; +} diff --git a/packages/xforms-engine/src/client/submission/SubmissionDefinition.ts b/packages/xforms-engine/src/client/submission/SubmissionDefinition.ts new file mode 100644 index 000000000..d458f77fd --- /dev/null +++ b/packages/xforms-engine/src/client/submission/SubmissionDefinition.ts @@ -0,0 +1,16 @@ +export interface SubmissionDefinition { + /** + * @see {@link https://getodk.github.io/xforms-spec/#submission-attributes | `action` submission attribute} + */ + readonly submissionAction: URL | null; + + /** + * @see {@link https://getodk.github.io/xforms-spec/#submission-attributes | `method` submission attribute} + */ + readonly submissionMethod: 'post'; + + /** + * @see {@link https://getodk.github.io/xforms-spec/#submission-attributes | `base64RsaPublicKey` submission attribute} + */ + readonly encryptionKey: string | null; +} diff --git a/packages/xforms-engine/src/client/submission/SubmissionInstanceFile.ts b/packages/xforms-engine/src/client/submission/SubmissionInstanceFile.ts new file mode 100644 index 000000000..6cd115a27 --- /dev/null +++ b/packages/xforms-engine/src/client/submission/SubmissionInstanceFile.ts @@ -0,0 +1,9 @@ +import type { SubmissionInstanceFileName, SubmissionInstanceFileType } from '../constants.ts'; + +// Re-export for convenient `SubmissionInstanceFile` construction/access flows +export type { SubmissionInstanceFileName, SubmissionInstanceFileType }; + +export interface SubmissionInstanceFile extends File { + readonly name: SubmissionInstanceFileName; + readonly type: SubmissionInstanceFileType; +} diff --git a/packages/xforms-engine/src/client/submission/SubmissionOptions.ts b/packages/xforms-engine/src/client/submission/SubmissionOptions.ts new file mode 100644 index 000000000..13184d379 --- /dev/null +++ b/packages/xforms-engine/src/client/submission/SubmissionOptions.ts @@ -0,0 +1,28 @@ +export type SubmissionChunkedType = 'chunked' | 'monolithic'; + +interface BaseSubmissionOptions { + readonly chunked?: ChunkedType | undefined; + + /** + * As described in the + * {@link https://docs.getodk.org/openrosa-form-submission/#extended-transmission-considerations | OpenRosa Form Submission API}, + * clients may obtain this value from an OpenRosa server's + * `X-OpenRosa-Accept-Content-Length` header. + */ + readonly maxSize?: number; +} + +interface ChunkedSubmissionOptions extends BaseSubmissionOptions<'chunked'> { + readonly maxSize: number; +} + +interface MonolithicSubmissionOptions extends BaseSubmissionOptions<'monolithic'> { + readonly chunked?: 'monolithic' | undefined; + readonly maxSize?: never; +} + +// prettier-ignore +export type SubmissionOptions = { + chunked: ChunkedSubmissionOptions; + monolithic: MonolithicSubmissionOptions; +}[ChunkedType]; diff --git a/packages/xforms-engine/src/client/submission/SubmissionResult.ts b/packages/xforms-engine/src/client/submission/SubmissionResult.ts new file mode 100644 index 000000000..54437d416 --- /dev/null +++ b/packages/xforms-engine/src/client/submission/SubmissionResult.ts @@ -0,0 +1,124 @@ +import type { AnyViolation, DescendantNodeViolationReference } from '../validation.ts'; +import type { SubmissionData } from './SubmissionData.ts'; +import type { SubmissionDefinition } from './SubmissionDefinition.ts'; +import type { SubmissionChunkedType, SubmissionOptions } from './SubmissionOptions.ts'; + +// prettier-ignore +export type SubmissionResultStatus = + // eslint-disable-next-line @typescript-eslint/sort-type-constituents + | 'pending' + | 'max-size-exceeded' + | 'ready'; + +// prettier-ignore +type SubmissionResultData = { + chunked: readonly [SubmissionData, ...SubmissionData[]]; + monolithic: SubmissionData; +}[ChunkedType]; + +/** + * Provides detail about an individual submission attachment {@link File}s which + * exceeds the client-specified {@link maxSize} for a + * {@link SubmissionResult<'chunked'> | chunked submission result}. Clients may + * use this value to provide guidance to users. + * + * @todo We may want to consider (a) making {@link maxSize} a configuration the + * client can provide when initializing a form instance, rather than only on + * submission; and then (b) treating a maximum size violation as another kind of + * node-level violation. This would go beyond the kinds of validation specified + * by ODK XForms, but it would make a lot of _conceptual sense_. + * + * It would almost certainly be helpful to alert users to violations as the + * occur, rather than only at submission time (where they have likely already + * moved on). This is something clients can do without engine support, but it + * would likely promote good usability patterns if the engine makes it an + * obvious and uniform option at the main engine/client entrypoint. + * + * @todo If we consider the above, we'd want to reframe _this interface_ to + * match the shape of other {@link AnyViolation | violations} (adding it as a + * member of that union). We'd also likely eliminate + * {@link MaxSizeExceededResult} in the process, since + * {@link PendingSubmissionResult} would then cover the case. + */ +interface MaxSizeViolation { + /** + * Specifies the index of + * {@link SubmissionResultData<'chunked'> | chunked submission data} where a + * submission attachment {@link File} exceeds the client-specified + * {@link maxSize}. + */ + readonly dataIndex: number; + + /** + * Specifies the name of the file which exceeds the client-specified + * {@link maxSize}. This name can also be used as a key to access the + * violating {@link File}/submission attachment, in the {@link SubmissionData} + * at the specified {@link dataIndex}. + */ + readonly fileName: string; + + /** + * Reflects the client-specified maximum size for each chunk of a + * {@link SubmissionResult<'chunked'> | chunked submission result}. + */ + readonly maxSize: number; + + /** + * Details the actual size of the violating {@link File}/submission + * attachment. Along with {@link maxSize}. Clients may use the delta between + * this value and {@link maxSize} to provide detailed guidance to users. + */ + readonly actualSize: number; +} + +// prettier-ignore +type SubmissionResultViolation = + | DescendantNodeViolationReference + | MaxSizeViolation; + +interface BaseSubmissionResult { + readonly status: SubmissionResultStatus; + readonly definition: SubmissionDefinition; + get violations(): readonly SubmissionResultViolation[] | null; + + /** + * Submission data may be chunked according to the + * {@link SubmissionOptions.maxSize | maxSize submission option} + */ + readonly data: SubmissionResultData; +} + +interface PendingSubmissionResult + extends BaseSubmissionResult { + readonly status: 'pending'; + get violations(): readonly DescendantNodeViolationReference[]; +} + +interface MaxSizeExceededResult extends BaseSubmissionResult<'chunked'> { + readonly status: 'max-size-exceeded'; + get violations(): readonly MaxSizeViolation[]; +} + +interface ReadySubmissionResult + extends BaseSubmissionResult { + readonly status: 'ready'; + get violations(): null; +} + +// prettier-ignore +type CommonSubmissionResult = + | PendingSubmissionResult + | ReadySubmissionResult; + +// prettier-ignore +export type ChunkedSubmissionResult = + | CommonSubmissionResult<'chunked'> + | MaxSizeExceededResult; + +export type MonolithicSubmissionResult = CommonSubmissionResult<'monolithic'>; + +// prettier-ignore +export type SubmissionResult = { + chunked: ChunkedSubmissionResult; + monolithic: MonolithicSubmissionResult; +}[ChunkedType]; diff --git a/packages/xforms-engine/src/client/validation.ts b/packages/xforms-engine/src/client/validation.ts index e29b009d9..99b4bc73b 100644 --- a/packages/xforms-engine/src/client/validation.ts +++ b/packages/xforms-engine/src/client/validation.ts @@ -1,5 +1,5 @@ -import type { NodeID } from '../instance/identity.ts'; import type { BaseNode, BaseNodeState } from './BaseNode.ts'; +import type { FormNodeID } from './identity.ts'; import type { OpaqueReactiveObjectFactory } from './OpaqueReactiveObjectFactory.ts'; import type { RootNode } from './RootNode.ts'; import type { TextRange } from './TextRange.ts'; @@ -176,7 +176,7 @@ export interface LeafNodeValidationState { * each property will be directly computed from the affected node. */ export interface DescendantNodeViolationReference { - readonly nodeId: NodeID; + readonly nodeId: FormNodeID; get reference(): string; get violation(): AnyViolation; diff --git a/packages/xforms-engine/src/index.ts b/packages/xforms-engine/src/index.ts index a30bbe9fd..767ba29e6 100644 --- a/packages/xforms-engine/src/index.ts +++ b/packages/xforms-engine/src/index.ts @@ -28,6 +28,12 @@ export type * from './client/repeat/RepeatRangeUncontrolledNode.ts'; export type * from './client/RootNode.ts'; export type * from './client/SelectNode.ts'; export type * from './client/StringNode.ts'; +export type * from './client/submission/SubmissionData.ts'; +export type * from './client/submission/SubmissionDefinition.ts'; +export type * from './client/submission/SubmissionInstanceFile.ts'; +export type * from './client/submission/SubmissionOptions.ts'; +export type * from './client/submission/SubmissionResult.ts'; +export type * from './client/submission/SubmissionState.ts'; export type * from './client/SubtreeNode.ts'; export type * from './client/TextRange.ts'; export type * from './client/TriggerNode.ts'; diff --git a/packages/xforms-engine/src/instance/Group.ts b/packages/xforms-engine/src/instance/Group.ts index 44b08f913..74c35225d 100644 --- a/packages/xforms-engine/src/instance/Group.ts +++ b/packages/xforms-engine/src/instance/Group.ts @@ -1,5 +1,6 @@ import type { Accessor } from 'solid-js'; import type { GroupDefinition, GroupNode, GroupNodeAppearances } from '../client/GroupNode.ts'; +import type { FormNodeID } from '../client/identity.ts'; import type { TextRange } from '../client/TextRange.ts'; import type { AncestorNodeValidationState } from '../client/validation.ts'; import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts'; @@ -16,7 +17,6 @@ import type { DescendantNodeSharedStateSpec } from './abstract/DescendantNode.ts import { DescendantNode } from './abstract/DescendantNode.ts'; import { buildChildren } from './children.ts'; import type { GeneralChildNode, GeneralParentNode } from './hierarchy.ts'; -import type { NodeID } from './identity.ts'; import type { EvaluationContext } from './internal-api/EvaluationContext.ts'; import type { SubscribableDependency } from './internal-api/SubscribableDependency.ts'; @@ -24,7 +24,7 @@ import type { SubscribableDependency } from './internal-api/SubscribableDependen interface GroupStateSpec extends DescendantNodeSharedStateSpec { readonly label: Accessor | null>; readonly hint: null; - readonly children: Accessor; + readonly children: Accessor; readonly valueOptions: null; readonly value: null; } diff --git a/packages/xforms-engine/src/instance/Root.ts b/packages/xforms-engine/src/instance/Root.ts index 4828eb4e8..bad32e92d 100644 --- a/packages/xforms-engine/src/instance/Root.ts +++ b/packages/xforms-engine/src/instance/Root.ts @@ -3,6 +3,12 @@ import type { Accessor, Signal } from 'solid-js'; import { createSignal } from 'solid-js'; import type { ActiveLanguage, FormLanguage, FormLanguages } from '../client/FormLanguage.ts'; import type { RootNode } from '../client/RootNode.ts'; +import type { FormNodeID } from '../client/identity.ts'; +import type { + SubmissionChunkedType, + SubmissionOptions, +} from '../client/submission/SubmissionOptions.ts'; +import type { SubmissionResult } from '../client/submission/SubmissionResult.ts'; import type { AncestorNodeValidationState } from '../client/validation.ts'; import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts'; import { createChildrenState } from '../lib/reactivity/createChildrenState.ts'; @@ -19,7 +25,6 @@ import type { XFormDOM } from '../parse/XFormDOM.ts'; import { InstanceNode } from './abstract/InstanceNode.ts'; import { buildChildren } from './children.ts'; import type { GeneralChildNode } from './hierarchy.ts'; -import type { NodeID } from './identity.ts'; import type { EvaluationContext, EvaluationContextRoot } from './internal-api/EvaluationContext.ts'; import type { InstanceConfig } from './internal-api/InstanceConfig.ts'; import type { SubscribableDependency } from './internal-api/SubscribableDependency.ts'; @@ -32,7 +37,7 @@ interface RootStateSpec { readonly required: boolean; readonly label: null; readonly hint: null; - readonly children: Accessor; + readonly children: Accessor; readonly valueOptions: null; readonly value: null; @@ -217,6 +222,12 @@ export class Root return this; } + prepareSubmission( + _options?: SubmissionOptions + ): Promise> { + throw new Error('Not implemented'); + } + // SubscribableDependency override subscribe(): void { super.subscribe(); diff --git a/packages/xforms-engine/src/instance/Subtree.ts b/packages/xforms-engine/src/instance/Subtree.ts index ea05774ad..d65d373a0 100644 --- a/packages/xforms-engine/src/instance/Subtree.ts +++ b/packages/xforms-engine/src/instance/Subtree.ts @@ -1,4 +1,5 @@ import { type Accessor } from 'solid-js'; +import type { FormNodeID } from '../client/identity.ts'; import type { SubtreeDefinition, SubtreeNode } from '../client/SubtreeNode.ts'; import type { AncestorNodeValidationState } from '../client/validation.ts'; import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts'; @@ -14,14 +15,13 @@ import type { DescendantNodeSharedStateSpec } from './abstract/DescendantNode.ts import { DescendantNode } from './abstract/DescendantNode.ts'; import { buildChildren } from './children.ts'; import type { GeneralChildNode, GeneralParentNode } from './hierarchy.ts'; -import type { NodeID } from './identity.ts'; import type { EvaluationContext } from './internal-api/EvaluationContext.ts'; import type { SubscribableDependency } from './internal-api/SubscribableDependency.ts'; interface SubtreeStateSpec extends DescendantNodeSharedStateSpec { readonly label: null; readonly hint: null; - readonly children: Accessor; + readonly children: Accessor; readonly valueOptions: null; readonly value: null; } diff --git a/packages/xforms-engine/src/instance/abstract/InstanceNode.ts b/packages/xforms-engine/src/instance/abstract/InstanceNode.ts index 543988f3d..76e3e2f43 100644 --- a/packages/xforms-engine/src/instance/abstract/InstanceNode.ts +++ b/packages/xforms-engine/src/instance/abstract/InstanceNode.ts @@ -2,6 +2,7 @@ import type { XFormsXPathEvaluator } from '@getodk/xpath'; import type { Accessor, Signal } from 'solid-js'; import type { BaseNode } from '../../client/BaseNode.ts'; import type { NodeAppearances } from '../../client/NodeAppearances.ts'; +import type { FormNodeID } from '../../client/identity.ts'; import type { InstanceNodeType } from '../../client/node-types.ts'; import type { NodeValidationState } from '../../client/validation.ts'; import type { TextRange } from '../../index.ts'; @@ -15,8 +16,7 @@ import type { SimpleAtomicState } from '../../lib/reactivity/types.ts'; import type { AnyNodeDefinition } from '../../parse/model/NodeDefinition.ts'; import type { Root } from '../Root.ts'; import type { AnyChildNode, AnyNode, AnyParentNode } from '../hierarchy.ts'; -import type { NodeID } from '../identity.ts'; -import { declareNodeID } from '../identity.ts'; +import { nodeID } from '../identity.ts'; import type { EvaluationContext } from '../internal-api/EvaluationContext.ts'; import type { InstanceConfig } from '../internal-api/InstanceConfig.ts'; import type { SubscribableDependency } from '../internal-api/SubscribableDependency.ts'; @@ -28,7 +28,7 @@ export interface InstanceNodeStateSpec { readonly required: Accessor | boolean; readonly label: Accessor | null> | null; readonly hint: Accessor | null> | null; - readonly children: Accessor | null; + readonly children: Accessor | null; readonly valueOptions: Accessor | Accessor | null; readonly value: Signal | SimpleAtomicState | null; } @@ -109,7 +109,7 @@ export abstract class InstanceNode< abstract readonly isRelevant: Accessor; // BaseNode: identity - readonly nodeId: NodeID; + readonly nodeId: FormNodeID; // BaseNode: node types and variants (e.g. for narrowing) abstract readonly nodeType: InstanceNodeType; @@ -161,7 +161,7 @@ export abstract class InstanceNode< this.scope = createReactiveScope(); this.engineConfig = engineConfig; - this.nodeId = declareNodeID(engineConfig.createUniqueId()); + this.nodeId = nodeID(engineConfig.createUniqueId()); this.definition = definition; } diff --git a/packages/xforms-engine/src/instance/identity.ts b/packages/xforms-engine/src/instance/identity.ts index 388321402..924c2828f 100644 --- a/packages/xforms-engine/src/instance/identity.ts +++ b/packages/xforms-engine/src/instance/identity.ts @@ -1,11 +1,5 @@ -declare const NODE_ID_BRAND: unique symbol; -type NODE_ID_BRAND = typeof NODE_ID_BRAND; +import type { FormNodeID } from '../client/identity.ts'; -export type NodeID = string & { readonly [NODE_ID_BRAND]: true }; - -// Just another added safeguard to ensure we're not mistakenly handling -// rando `NodeID` strings which aren't explicitly attached to the node -// types we expect. -export const declareNodeID = (id: string): NodeID => { - return id as NodeID; +export const nodeID = (id: string): FormNodeID => { + return `node:${id}` satisfies FormNodeID; }; diff --git a/packages/xforms-engine/src/instance/internal-api/ValidationContext.ts b/packages/xforms-engine/src/instance/internal-api/ValidationContext.ts index b0562ee79..00314fc7e 100644 --- a/packages/xforms-engine/src/instance/internal-api/ValidationContext.ts +++ b/packages/xforms-engine/src/instance/internal-api/ValidationContext.ts @@ -1,7 +1,7 @@ +import type { FormNodeID } from '../../client/identity.ts'; import type { AnyViolation } from '../../client/validation.ts'; import type { BindComputationExpression } from '../../parse/expression/BindComputationExpression.ts'; import type { MessageDefinition } from '../../parse/text/MessageDefinition.ts'; -import type { NodeID } from '../identity.ts'; import type { EvaluationContext } from './EvaluationContext.ts'; import type { SubscribableDependency } from './SubscribableDependency.ts'; @@ -21,7 +21,7 @@ interface ValidationContextDefinition { } export interface ValidationContext extends EvaluationContext, SubscribableDependency { - readonly nodeId: NodeID; + readonly nodeId: FormNodeID; readonly definition: ValidationContextDefinition; readonly currentState: ValidationContextCurrentState; diff --git a/packages/xforms-engine/src/instance/repeat/BaseRepeatRange.ts b/packages/xforms-engine/src/instance/repeat/BaseRepeatRange.ts index f0a4ad321..1000480da 100644 --- a/packages/xforms-engine/src/instance/repeat/BaseRepeatRange.ts +++ b/packages/xforms-engine/src/instance/repeat/BaseRepeatRange.ts @@ -1,5 +1,6 @@ import { insertAtIndex } from '@getodk/common/lib/array/insert.ts'; import { untrack, type Accessor } from 'solid-js'; +import type { FormNodeID } from '../../client/identity.ts'; import type { NodeAppearances } from '../../client/NodeAppearances.ts'; import type { BaseRepeatRangeNode } from '../../client/repeat/BaseRepeatRangeNode.ts'; import type { TextRange } from '../../client/TextRange.ts'; @@ -25,7 +26,6 @@ import type { } from '../abstract/DescendantNode.ts'; import { DescendantNode } from '../abstract/DescendantNode.ts'; import type { RepeatRange } from '../hierarchy.ts'; -import type { NodeID } from '../identity.ts'; import type { EvaluationContext } from '../internal-api/EvaluationContext.ts'; import type { SubscribableDependency } from '../internal-api/SubscribableDependency.ts'; import { RepeatInstance, type RepeatDefinition } from './RepeatInstance.ts'; @@ -33,7 +33,7 @@ import { RepeatInstance, type RepeatDefinition } from './RepeatInstance.ts'; interface RepeatRangeStateSpec extends DescendantNodeSharedStateSpec { readonly hint: null; readonly label: Accessor | null>; - readonly children: Accessor; + readonly children: Accessor; readonly valueOptions: null; readonly value: null; } diff --git a/packages/xforms-engine/src/instance/repeat/RepeatInstance.ts b/packages/xforms-engine/src/instance/repeat/RepeatInstance.ts index 8c87c3e05..ce3c538ec 100644 --- a/packages/xforms-engine/src/instance/repeat/RepeatInstance.ts +++ b/packages/xforms-engine/src/instance/repeat/RepeatInstance.ts @@ -1,5 +1,6 @@ import type { Accessor } from 'solid-js'; import { createComputed, createSignal, on } from 'solid-js'; +import type { FormNodeID } from '../../client/identity.ts'; import type { RepeatDefinition, RepeatInstanceNode, @@ -21,7 +22,6 @@ import type { DescendantNodeSharedStateSpec } from '../abstract/DescendantNode.t import { DescendantNode } from '../abstract/DescendantNode.ts'; import { buildChildren } from '../children.ts'; import type { AnyChildNode, GeneralChildNode, RepeatRange } from '../hierarchy.ts'; -import type { NodeID } from '../identity.ts'; import type { EvaluationContext } from '../internal-api/EvaluationContext.ts'; import type { SubscribableDependency } from '../internal-api/SubscribableDependency.ts'; @@ -30,7 +30,7 @@ export type { RepeatDefinition }; interface RepeatInstanceStateSpec extends DescendantNodeSharedStateSpec { readonly label: Accessor | null>; readonly hint: null; - readonly children: Accessor; + readonly children: Accessor; readonly valueOptions: null; readonly value: null; } diff --git a/packages/xforms-engine/src/lib/reactivity/createChildrenState.ts b/packages/xforms-engine/src/lib/reactivity/createChildrenState.ts index 1de5215d5..f8d558804 100644 --- a/packages/xforms-engine/src/lib/reactivity/createChildrenState.ts +++ b/packages/xforms-engine/src/lib/reactivity/createChildrenState.ts @@ -1,8 +1,8 @@ import type { Accessor, Setter, Signal } from 'solid-js'; import { createSignal } from 'solid-js'; +import type { FormNodeID } from '../../client/identity.ts'; import type { OpaqueReactiveObjectFactory } from '../../index.ts'; import type { AnyChildNode, AnyParentNode } from '../../instance/hierarchy.ts'; -import type { NodeID } from '../../instance/identity.ts'; import type { materializeCurrentStateChildren } from './materializeCurrentStateChildren.ts'; import type { ClientState } from './node-state/createClientState.ts'; import type { CurrentState } from './node-state/createCurrentState.ts'; @@ -12,14 +12,15 @@ export interface ChildrenState { readonly children: Signal; readonly getChildren: Accessor; readonly setChildren: Setter; - readonly childIds: Accessor; + readonly childIds: Accessor; } /** * Creates a synchronized pair of: * * - Internal children state suitable for all parent node types - * - The same children state computed as an array of each child's {@link NodeID} + * - The same children state computed as an array of each child's + * {@link FormNodeID} * * This state is used, in tandem with {@link materializeCurrentStateChildren}, * to ensure children in **client-facing** state are not written into nested @@ -34,11 +35,12 @@ export interface ChildrenState { * The produced {@link ChildrenState.childIds} memo is intended to be used to * specify each parent node's `children` in an instance of {@link EngineState}. * In so doing, the node's corresponding (internal, synchronized) - * {@link ClientState} will likewise store only the children's {@link NodeID}s. + * {@link ClientState} will likewise store only the children's + * {@link FormNodeID}s. * * As a client reacts to changes in a given parent node's `children` state, that * node's {@link CurrentState} should produce the child nodes corresponding to - * those {@link NodeID}s with the aforementioned + * those {@link FormNodeID}s with the aforementioned * {@link materializeCurrentStateChildren}. */ export const createChildrenState = ( @@ -65,7 +67,7 @@ export const createChildrenState = ([]); + const ids = createSignal([]); const [childIds, setChildIds] = ids; type ChildrenSetterCallback = (prev: readonly Child[]) => readonly Child[]; diff --git a/packages/xforms-engine/src/lib/reactivity/materializeCurrentStateChildren.ts b/packages/xforms-engine/src/lib/reactivity/materializeCurrentStateChildren.ts index 58f614d3d..78a74e1d8 100644 --- a/packages/xforms-engine/src/lib/reactivity/materializeCurrentStateChildren.ts +++ b/packages/xforms-engine/src/lib/reactivity/materializeCurrentStateChildren.ts @@ -1,13 +1,13 @@ +import type { FormNodeID } from '../../client/identity.ts'; import type { AnyChildNode } from '../../instance/hierarchy.ts'; -import type { NodeID } from '../../instance/identity.ts'; import type { ChildrenState, createChildrenState } from './createChildrenState.ts'; import type { ClientState } from './node-state/createClientState.ts'; import type { CurrentState } from './node-state/createCurrentState.ts'; import type { ReactiveScope } from './scope.ts'; interface InconsistentChildrenStateDetails { - readonly missingIds: readonly NodeID[]; - readonly unexpectedIds: readonly NodeID[]; + readonly missingIds: readonly FormNodeID[]; + readonly unexpectedIds: readonly FormNodeID[]; } class InconsistentChildrenStateError extends Error { @@ -37,7 +37,7 @@ class InconsistentChildrenStateError extends Error { } export interface EncodedParentState { - readonly children: readonly NodeID[]; + readonly children: readonly FormNodeID[]; } /** @@ -58,7 +58,7 @@ export interface EncodedParentState { * @todo should we throw rather than warn until we have this confidence? */ const reportInconsistentChildrenState = ( - expectedClientIds: readonly NodeID[], + expectedClientIds: readonly FormNodeID[], actualNodes: readonly AnyChildNode[] ): void => { const actualIds = actualNodes.map((node) => node.nodeId); @@ -88,8 +88,8 @@ export type MaterializedChildren< /** * Creates a wrapper proxy around a parent node's {@link CurrentState} to map * `children` state, which is written to the node's (internal, synchronized) - * {@link ClientState} as an array of {@link NodeID}s, back to the node objects - * corresponding to those IDs. + * {@link ClientState} as an array of {@link FormNodeID}s, back to the node + * objects corresponding to those IDs. * * @see {@link createChildrenState} for further detail. */