Skip to content

Commit

Permalink
engine/client: expand FetchResourceResponse to include optional `he…
Browse files Browse the repository at this point in the history
…aders`

This moves the existing resource-related types into a dedicated module (they probably already should have been!). It also revises their JSDoc to reflect some of the semantic expectations clarified in #201.
  • Loading branch information
eyelidlessness committed Nov 25, 2024
1 parent 7b59a0b commit 75bc335
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 41 deletions.
39 changes: 1 addition & 38 deletions packages/xforms-engine/src/client/EngineConfig.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,6 @@
import type { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts';
import type { initializeForm } from '../instance/index.ts';
import type { OpaqueReactiveObjectFactory } from './OpaqueReactiveObjectFactory.ts';

/**
* @todo this is currently a strict subset of the web standard `Response`. Is it
* sufficient? Ways it might not be:
*
* - No way to convey metadata about the resource
* - Ambiguous if a client supplies an alternative implementation which doesn't
* exhaust the body on access
*/
export interface FetchResourceResponse {
readonly ok?: boolean;
readonly body?: ReadableStream<Uint8Array> | null;
readonly bodyUsed?: boolean;

readonly blob: () => Promise<Blob>;
readonly text: () => Promise<string>;
}

/**
* @todo this is a strict subset of the web standard `fetch` interface. It
* implicitly assumes that the engine itself will only ever issue `GET`-like
* requests. It also provides no further request-like semantics to the engine.
* This is presumed sufficient for now, but notably doesn't expose any notion of
* content negotiation (e.g. the ability to supply `Accept` headers).
*
* This also completely ignores any notion of mapping
* {@link https://getodk.github.io/xforms-spec/#uris | `jr:` URLs} to their
* actual resources (likely, but not necessarily, accessed at a corresponding
* HTTP URL).
*/
export type FetchResource<Resource extends URL = URL> = (
resource: Resource
) => Promise<FetchResourceResponse>;

export type FormAttachmentURL = JRResourceURL;

export type FetchFormAttachment = FetchResource<FormAttachmentURL>;
import type { FetchFormAttachment, FetchResource } from './resources.ts';

/**
* Options provided by a client to specify certain aspects of engine runtime
Expand Down
118 changes: 118 additions & 0 deletions packages/xforms-engine/src/client/resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts';
import type { initializeForm } from '../instance/index.ts';

interface FetchResourceHeadersIterator<T>
extends IteratorObject<
T,
// Note: we use this weird TypeScript intrinsic type so a built-in
// `HeadersIterator` is assignable regardless of a client's configured
// TypeScript or linting strictness. We don't actually care about the type, or
// consume the value it represents.
BuiltinIteratorReturn,
unknown
> {
[Symbol.iterator](): FetchResourceHeadersIterator<T>;
}

type FetchResourceHeadersForEachCallbackFn = (
value: string,
key: string,
parent: FetchResourceResponseHeaders
) => void;

/**
* A read-only strict subset of the web standard {@link Headers}.
*
* Note that the engine may make the following assumptions about
* {@link FetchResourceResponse.headers}:
*
* - If {@link FetchResourceResponse} is an instance of {@link Response}, it
* will be assumed its {@link FetchResourceResponse.headers | headers object}
* _is present_, and that it is an instance of {@link Headers}. In other
* words: for the purposes of resource resolution, we explicitly expect that
* clients using APIs provided by the runtime platform (or polyfills thereof)
* will not monkey-patch properties of values produced by those APIs.
*
* - If the object is an instance of {@link Headers} (whether by inference as a
* property of {@link Response}, or by a direct instance type check), the
* engine will assume it is safe to treat header names as case insensitive for
* any lookups it may perform. In other words: we explicitly expect that
* clients _providing access_ to APIs rovided by the runtime platform (or
* polyfills thereof) will not alter the guarantees of those APIs.
*
* - If the object is not an instance of {@link Headers}, it will be treated as
* a {@link ReadonlyMap<string, string>}. In other words: we explicitly expect
* that clients, when providing a bespoke implementation of
* {@link FetchResourceResponse} and its constituent parts, will likely
* implement them partially (and in the case of
* {@link FetchResourceResponse.headers}, with the nearest common idiom
* available). In this case, we will favor a best effort at correctness,
* generally at some expense of performance.
*/
export interface FetchResourceResponseHeaders {
[Symbol.iterator](): FetchResourceHeadersIterator<[string, string]>;

entries(): FetchResourceHeadersIterator<[string, string]>;
keys(): FetchResourceHeadersIterator<string>;
values(): FetchResourceHeadersIterator<string>;

get(name: string): string | null;
has(name: string): boolean;
forEach(callbackfn: FetchResourceHeadersForEachCallbackFn): void;
}

/**
* This is a strict subset of the web standard {@link Response}. Clients are
* encouraged to use the global {@link Response} constructor (as provided by the
* runtime platform, or by a global runtime polyfill), but may also provide a
* bespoke implementation if it suits their needs.
*
* Since we cannot assume a client's implementation will always be an instance
* of {@link Response}, we make some assumptions about its {@link headers}
* object (if available, as detailed on {@link FetchResourceResponseHeaders}).
*
* For other properties, we make the following assumptions (all of which are
* assumptions we would make about a platform-provided/polyfilled
* {@link Response}, but are explicitly stated for the benefit of confidence in
* client implementations):
*
* - If we read {@link body} directly, we will assume it is consumed on first
* read, and will not read it again.
*
* - We assume that {@link blob} and {@link text} indirectly consume
* {@link body} on first read as well, and will only ever read one of each of
* these properties, and only ever once.
*
* Furthermore, if the engine intends to read {@link body} (or its indirect
* {@link blob} or {@link text} consumers), it will do so in the course of a
* client's call to {@link initializeForm}, and before the
* {@link Promise<PrimaryInstance>} returned by that call is resolved.
*/
export interface FetchResourceResponse {
readonly ok?: boolean;
readonly status?: number;
readonly body?: ReadableStream<Uint8Array> | null;
readonly bodyUsed?: boolean;
readonly headers?: FetchResourceResponseHeaders;

readonly blob: () => Promise<Blob>;
readonly text: () => Promise<string>;
}

/**
* This is a strict subset of the web standard `fetch` interface. It implicitly
* assumes that the engine itself will only ever perform `GET`-like network/IO
* requests. It also provides no further request-like semantics to the engine.
*
* This is presumed sufficient for now, but notably doesn't expose any notion of
* content negotiation (e.g. the ability for the engine to include `Accept`
* headers in resource requests issued to a client's {@link FetchResource}
* implementation).
*/
export type FetchResource<Resource extends URL = URL> = (
resource: Resource
) => Promise<FetchResourceResponse>;

export type FormAttachmentURL = JRResourceURL;

export type FetchFormAttachment = FetchResource<FormAttachmentURL>;
1 change: 1 addition & 0 deletions packages/xforms-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type * from './client/OpaqueReactiveObjectFactory.ts';
export type * from './client/repeat/RepeatInstanceNode.ts';
export type * from './client/repeat/RepeatRangeControlledNode.ts';
export type * from './client/repeat/RepeatRangeUncontrolledNode.ts';
export type * from './client/resources.ts';
export type * from './client/RootNode.ts';
export type * from './client/SelectNode.ts';
export type * from './client/StringNode.ts';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FetchFormAttachment, FetchResource } from '../../client/EngineConfig.ts';
import type { OpaqueReactiveObjectFactory } from '../../client/OpaqueReactiveObjectFactory.ts';
import type { FetchFormAttachment, FetchResource } from '../../client/resources.ts';
import type { CreateUniqueId } from '../../lib/unique-id.ts';

export interface InstanceConfig {
Expand Down
2 changes: 1 addition & 1 deletion packages/xforms-engine/src/instance/resource.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts';
import { getBlobText } from '@getodk/common/lib/web-compat/blob.ts';
import type { FetchResource, FetchResourceResponse } from '../client/EngineConfig.ts';
import type { FormResource } from '../client/index.ts';
import type { FetchResource, FetchResourceResponse } from '../client/resources.ts';

export type { FetchResource, FetchResourceResponse, FormResource };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import {
title,
} from '@getodk/common/test/fixtures/xform-dsl';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { FetchResource } from '../../src/client/EngineConfig.ts';
import type { ActiveLanguage } from '../../src/client/FormLanguage.ts';
import type { OpaqueReactiveObjectFactory } from '../../src/client/OpaqueReactiveObjectFactory.ts';
import type { RootNode } from '../../src/client/RootNode.ts';
import type { FetchResource } from '../../src/client/resources.ts';
import { PrimaryInstance } from '../../src/instance/PrimaryInstance.ts';
import { Root } from '../../src/instance/Root.ts';
import { InstanceNode } from '../../src/instance/abstract/InstanceNode.ts';
Expand Down

0 comments on commit 75bc335

Please sign in to comment.