Skip to content

Commit 1aabc15

Browse files
engine: initial interface for restoring instance state (implementation WIP)
(TODO: write more about this approach) - - - **Note on client interface:** I’d hoped that restoring instance state would be synchronous (from `LoadFormResult` and its implementations). In hindsight it makes sense that this is not possible: instance XML is stored there as a `File` in `InstanceData` (`FormData`). Reading `File` data synchronously would likely need to go through [`FileReaderSync`](https://developer.mozilla.org/en-US/docs/Web/API/FileReaderSync), which can’t be used on the main thread. This isn’t a huge deal in any case, as we can expect the non-restore edit case to be async as well. Open question: is it better, for _consistency_ to make the synchronous `LoadFormResult.createInstance` signature return `Promise<CreatedFormInstance>`? Asynchrony there would be superfluous, but it might be nice for clients to have symmetry across calls to these similar APIs.
1 parent 9292465 commit 1aabc15

21 files changed

+310
-25
lines changed

packages/xforms-engine/src/client/form/FormInstance.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,29 @@ import type { LoadFormResult } from './LoadFormResult.ts';
1414
export type FormInstanceCreateMode = 'create';
1515

1616
/**
17-
* @todo Other modes incoming!
17+
* Represents an instance restored from previously filled state, as represented
18+
* in an {@link InstancePayload}. Clients may serialize and persist an instance
19+
* payload as appropriate for their use cases, and can restore the instance with
20+
* a partial instance payload structure, defined by the
21+
* {@link RestoreFormInstanceInput} interface.
22+
*
23+
* A restored instance is populated by the engine with the answers as they had
24+
* been filled at the time the {@link InstancePayload}
25+
* ({@link RestoreFormInstanceInput}) was created.
26+
*
27+
* Computations are performed on initialization as specified by
28+
* {@link https://getodk.github.io/xforms-spec/#event:odk-instance-load | ODK XForms},
29+
* as a
30+
* {@link https://getodk.github.io/xforms-spec/#event:odk-instance-load | subsequent load}
31+
* (i.e. **NOT** "first load", as is the case with newly
32+
* {@link FormInstanceCreateMode | created} instances}) of the instance.
1833
*/
34+
export type FormInstanceRestoreMode = 'restore';
35+
1936
// prettier-ignore
2037
export type FormInstanceInitializationMode =
21-
| FormInstanceCreateMode;
38+
| FormInstanceCreateMode
39+
| FormInstanceRestoreMode;
2240

2341
/**
2442
* @todo this could hypothetically convey warnings and/or errors, just as

packages/xforms-engine/src/client/form/LoadFormResult.ts

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { UnknownObject } from '@getodk/common/lib/type-assertions/assertUnk
22
import type { AnyFunction } from '@getodk/common/types/helpers.js';
33
import type { LoadFormFailureError } from '../../error/LoadFormFailureError.ts';
44
import type { CreateFormInstance } from './CreateFormInstance.ts';
5+
import type { RestoreFormInstance } from './RestoreFormInstance.ts';
56

67
// Re-export for client access
78
export type { LoadFormFailureError };
@@ -42,27 +43,31 @@ interface BaseLoadFormResult {
4243
readonly warnings: LoadFormWarnings | null;
4344
readonly error: LoadFormFailureError | null;
4445
readonly createInstance: FallibleLoadFormResultMethod<CreateFormInstance>;
46+
readonly restoreInstance: FallibleLoadFormResultMethod<RestoreFormInstance>;
4547
}
4648

4749
export interface LoadFormSuccessResult extends BaseLoadFormResult {
4850
readonly status: 'success';
4951
readonly warnings: null;
5052
readonly error: null;
5153
readonly createInstance: CreateFormInstance;
54+
readonly restoreInstance: RestoreFormInstance;
5255
}
5356

5457
export interface LoadFormWarningResult extends BaseLoadFormResult {
5558
readonly status: 'warning';
5659
readonly warnings: LoadFormWarnings;
5760
readonly error: null;
5861
readonly createInstance: CreateFormInstance;
62+
readonly restoreInstance: RestoreFormInstance;
5963
}
6064

6165
export interface LoadFormFailureResult extends BaseLoadFormResult {
6266
readonly status: 'failure';
6367
readonly warnings: LoadFormWarnings | null;
6468
readonly error: LoadFormFailureError;
6569
readonly createInstance: FailedLoadFormResultMethod<CreateFormInstance>;
70+
readonly restoreInstance: FailedLoadFormResultMethod<RestoreFormInstance>;
6671
}
6772

6873
// prettier-ignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { InstanceData } from '../serialization/InstanceData.ts';
2+
import type { FormInstance, FormInstanceRestoreMode } from './FormInstance.ts';
3+
import type { FormInstanceConfig } from './FormInstanceConfig.ts';
4+
5+
export interface RestoreFormInstanceInput {
6+
readonly data: readonly [InstanceData, ...InstanceData[]];
7+
}
8+
9+
export type RestoredFormInstance = FormInstance<FormInstanceRestoreMode>;
10+
11+
export type RestoreFormInstance = (
12+
input: RestoreFormInstanceInput,
13+
config?: FormInstanceConfig
14+
) => Promise<RestoredFormInstance>;

packages/xforms-engine/src/client/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type * from './form/FormInstanceConfig.ts';
66
export type * from './form/FormResource.ts';
77
export type * from './form/LoadForm.ts';
88
export type * from './form/LoadFormResult.ts';
9+
export type * from './form/RestoreFormInstance.ts';
910
export type * from './FormLanguage.ts';
1011
export type * from './GroupNode.ts';
1112
export type {

packages/xforms-engine/src/client/serialization/InstanceData.ts

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import type { INSTANCE_FILE_NAME, InstanceFile } from './InstanceFile.ts';
22

33
export interface InstanceData extends FormData {
44
get(name: INSTANCE_FILE_NAME): InstanceFile;
5+
6+
/**
7+
* @todo Can we guarantee (both in static types and at runtime) that
8+
* {@link InstanceData} only contains files?
9+
*/
510
get(name: string): FormDataEntryValue | null;
611

712
has(name: INSTANCE_FILE_NAME): true;

packages/xforms-engine/src/entrypoints/FormInstance.ts

+20-10
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,41 @@ import type {
55
} from '../client/form/FormInstance.ts';
66
import type { FormInstanceConfig } from '../client/index.ts';
77
import type { InstanceConfig } from '../instance/internal-api/InstanceConfig.ts';
8-
import type { PrimaryInstanceOptions } from '../instance/PrimaryInstance.ts';
8+
import type {
9+
BasePrimaryInstanceOptions,
10+
PrimaryInstanceInitialState,
11+
PrimaryInstanceOptions,
12+
} from '../instance/PrimaryInstance.ts';
913
import { PrimaryInstance } from '../instance/PrimaryInstance.ts';
1014
import type { Root } from '../instance/Root.ts';
1115

12-
export interface FormInstanceBaseOptions extends Omit<PrimaryInstanceOptions, 'config'> {}
16+
interface FormInstanceOptions<Mode extends FormInstanceInitializationMode> {
17+
readonly mode: Mode;
18+
readonly initialState: PrimaryInstanceInitialState<Mode>;
19+
readonly instanceOptions: BasePrimaryInstanceOptions;
20+
readonly instanceConfig: FormInstanceConfig;
21+
}
1322

1423
export class FormInstance<Mode extends FormInstanceInitializationMode>
1524
implements ClientFormInstance<Mode>
1625
{
26+
readonly mode: Mode;
1727
readonly root: Root;
1828

19-
constructor(
20-
readonly mode: Mode,
21-
baseOptions: FormInstanceBaseOptions,
22-
baseConfig?: FormInstanceConfig
23-
) {
29+
constructor(options: FormInstanceOptions<Mode>) {
30+
const { mode, initialState } = options;
2431
const config: InstanceConfig = {
25-
clientStateFactory: baseConfig?.stateFactory ?? identity,
32+
clientStateFactory: options.instanceConfig?.stateFactory ?? identity,
2633
};
27-
const primaryInstanceOptions: PrimaryInstanceOptions = {
28-
...baseOptions,
34+
const primaryInstanceOptions: PrimaryInstanceOptions<Mode> = {
35+
...options.instanceOptions,
36+
mode,
37+
initialState,
2938
config,
3039
};
3140
const { root } = new PrimaryInstance(primaryInstanceOptions);
3241

42+
this.mode = mode;
3343
this.root = root;
3444
}
3545
}

packages/xforms-engine/src/entrypoints/FormResult/FormFailureResult.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
LoadFormFailureResult,
66
LoadFormWarnings,
77
} from '../../client/form/LoadFormResult.ts';
8+
import type { RestoreFormInstance } from '../../client/form/RestoreFormInstance.ts';
89
import { LoadFormFailureError } from '../../error/LoadFormFailureError.ts';
910
import { BaseFormResult } from './BaseFormResult.ts';
1011

@@ -23,6 +24,7 @@ const failedFormResultMethodFactory = <T extends AnyFunction>(
2324

2425
export class FormFailureResult extends BaseFormResult<'failure'> implements LoadFormFailureResult {
2526
readonly createInstance: FailedLoadFormResultMethod<CreateFormInstance>;
27+
readonly restoreInstance: FailedLoadFormResultMethod<RestoreFormInstance>;
2628

2729
constructor(options: FormFailureOptions) {
2830
const { error, warnings } = options;
@@ -34,5 +36,6 @@ export class FormFailureResult extends BaseFormResult<'failure'> implements Load
3436
});
3537

3638
this.createInstance = failedFormResultMethodFactory(error);
39+
this.restoreInstance = failedFormResultMethodFactory(error);
3740
}
3841
}

packages/xforms-engine/src/entrypoints/FormResult/FormSuccessResult.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import type { LoadFormSuccessResult } from '../../client/index.ts';
2+
import type { BasePrimaryInstanceOptions } from '../../instance/PrimaryInstance.ts';
23
import type { FormResource } from '../../instance/resource.ts';
34
import type { ReactiveScope } from '../../lib/reactivity/scope.ts';
4-
import type { FormInstanceBaseOptions } from '../FormInstance.ts';
55
import { BaseInstantiableFormResult } from './InstantiableFormResult.ts';
66

77
export interface FormSuccessResultOptions {
88
readonly warnings: null;
99
readonly error: null;
1010
readonly scope: ReactiveScope;
1111
readonly formResource: FormResource;
12-
readonly instanceOptions: FormInstanceBaseOptions;
12+
readonly instanceOptions: BasePrimaryInstanceOptions;
1313
}
1414

1515
export class FormSuccessResult

packages/xforms-engine/src/entrypoints/FormResult/FormWarningResult.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import type { LoadFormWarningResult, LoadFormWarnings } from '../../client/form/LoadFormResult.ts';
2+
import type { BasePrimaryInstanceOptions } from '../../instance/PrimaryInstance.ts';
23
import type { FormResource } from '../../instance/resource.ts';
34
import type { ReactiveScope } from '../../lib/reactivity/scope.ts';
4-
import type { FormInstanceBaseOptions } from '../FormInstance.ts';
55
import { BaseInstantiableFormResult } from './InstantiableFormResult.ts';
66

77
export interface FormWarningResultOptions {
88
readonly warnings: LoadFormWarnings;
99
readonly error: null;
1010
readonly scope: ReactiveScope;
1111
readonly formResource: FormResource;
12-
readonly instanceOptions: FormInstanceBaseOptions;
12+
readonly instanceOptions: BasePrimaryInstanceOptions;
1313
}
1414

1515
export class FormWarningResult

packages/xforms-engine/src/entrypoints/FormResult/InstantiableFormResult.ts

+30-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import type { CreateFormInstance } from '../../client/form/CreateFormInstance.ts';
22
import type { FormInstanceConfig } from '../../client/form/FormInstanceConfig.ts';
3+
import type {
4+
RestoreFormInstance,
5+
RestoreFormInstanceInput,
6+
} from '../../client/form/RestoreFormInstance.ts';
7+
import { InitialInstanceState } from '../../instance/input/InitialInstanceState.ts';
8+
import type { BasePrimaryInstanceOptions } from '../../instance/PrimaryInstance.ts';
39
import type { FormResource } from '../../instance/resource.ts';
410
import type { ReactiveScope } from '../../lib/reactivity/scope.ts';
5-
import { FormInstance, type FormInstanceBaseOptions } from '../FormInstance.ts';
11+
import { FormInstance } from '../FormInstance.ts';
612
import type { BaseFormResultProperty } from './BaseFormResult.ts';
713
import { BaseFormResult } from './BaseFormResult.ts';
814

@@ -17,13 +23,14 @@ export interface InstantiableFormResultOptions<Status extends InstantiableFormRe
1723
readonly error: null;
1824
readonly scope: ReactiveScope;
1925
readonly formResource: FormResource;
20-
readonly instanceOptions: FormInstanceBaseOptions;
26+
readonly instanceOptions: BasePrimaryInstanceOptions;
2127
}
2228

2329
export abstract class BaseInstantiableFormResult<
2430
Status extends InstantiableFormResultStatus,
2531
> extends BaseFormResult<Status> {
2632
readonly createInstance: CreateFormInstance;
33+
readonly restoreInstance: RestoreFormInstance;
2734

2835
constructor(options: InstantiableFormResultOptions<Status>) {
2936
const { status, warnings, error, instanceOptions } = options;
@@ -34,8 +41,27 @@ export abstract class BaseInstantiableFormResult<
3441
error,
3542
});
3643

37-
this.createInstance = (config?: FormInstanceConfig) => {
38-
return new FormInstance('create', instanceOptions, config);
44+
this.createInstance = (instanceConfig: FormInstanceConfig = {}) => {
45+
return new FormInstance({
46+
mode: 'create',
47+
instanceOptions,
48+
initialState: null,
49+
instanceConfig,
50+
});
51+
};
52+
53+
this.restoreInstance = async (
54+
input: RestoreFormInstanceInput,
55+
instanceConfig: FormInstanceConfig = {}
56+
) => {
57+
const initialState = await InitialInstanceState.from(input.data);
58+
59+
return new FormInstance({
60+
mode: 'restore',
61+
instanceOptions,
62+
initialState,
63+
instanceConfig,
64+
});
3965
};
4066
}
4167
}

packages/xforms-engine/src/entrypoints/createInstance.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { CreatedFormInstance } from '../client/form/CreateFormInstance.ts';
12
import type { FormInstanceConfig } from '../client/form/FormInstanceConfig.ts';
23
import type { LoadFormOptions } from '../client/form/LoadForm.ts';
3-
import type { CreatedFormInstance } from '../client/index.ts';
44
import type { FormResource } from '../instance/resource.ts';
55
import { loadForm } from './loadForm.ts';
66

Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './createInstance.ts';
22
export * from './loadForm.ts';
3+
export * from './restoreInstance.ts';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { FormInstanceConfig } from '../client/form/FormInstanceConfig.ts';
2+
import type { LoadFormOptions } from '../client/form/LoadForm.ts';
3+
import type {
4+
RestoredFormInstance,
5+
RestoreFormInstanceInput,
6+
} from '../client/form/RestoreFormInstance.ts';
7+
import type { FormResource } from '../instance/resource.ts';
8+
import { loadForm } from './loadForm.ts';
9+
10+
export interface RestoreInstanceOptions {
11+
readonly form?: LoadFormOptions;
12+
readonly instance?: FormInstanceConfig;
13+
}
14+
15+
export const restoreInstance = async (
16+
formResource: FormResource,
17+
input: RestoreFormInstanceInput,
18+
options?: RestoreInstanceOptions
19+
): Promise<RestoredFormInstance> => {
20+
const form = await loadForm(formResource, options?.form);
21+
22+
if (form.status === 'failure') {
23+
throw form.error;
24+
}
25+
26+
return form.restoreInstance(input, options?.instance);
27+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ErrorProductionDesignPendingError } from './ErrorProductionDesignPendingError.ts';
2+
3+
export class MalformedInstanceDataError extends ErrorProductionDesignPendingError {}

0 commit comments

Comments
 (0)