FS FormScript
Package@formscript/runtime
Specification4.10, 8.7, 11, 11.1, 11.2, 11.3
Depends onast, decimal, errors, evaluator, types

@formscript/runtime

Remote data and actions, per §11 of the specification: action (§11.1), source (§11.2) and offline behaviour (§11.3).

An action or a source is data, not code (Spec-11.1): the runtime owns transport, auth and serialisation, and the document contributes values. This package models the declarations, builds the requests, and drives the transport the host supplies.

The declarations

ts
import { readActionProperties, resolveAction } from '@formscript/runtime';

const properties = readActionProperties(actionStatement);
const action = resolveAction(properties, { url: 'https://api.example.com/applications' });

action.method;   // 'POST' — the default of §8.7
action.conflict; // 'skip' — the default of Spec-11.10

readActionProperties and readSourceProperties read the document-level properties of an action or a source block and keep the expressions that need evaluation (url, params, body, when). resolveAction and resolveSource turn them into the runtime declarations.

The request

buildSourceRequest and buildActionRequest are pure: they take a declaration and the evaluated values, and they return a request description. No network, no clock, no randomness.

ts
const built = buildSourceRequest(source, { params: { region: 'GB' } });
built.request.method; // 'GET'
built.request.url;    // 'https://api.example.com/countries?region=GB'

The body of an action is $form.output — the projected output document (§4.10) — unless the action declares its own body (Spec-11.4).

The parameter mapping

Mapped value Outcome
a value bound; the parameter is sent
absent (null, unknown) bound to an empty value — asked, not answered (Spec-4.35)
inapplicable omitted — not asked (Spec-8.3)
the error value the mapping fails and the request is not built (Spec-6.3, E-EVAL-07)

An array is sent as repeated fields of the same name.

Running an action

ts
import { executeAction } from '@formscript/runtime';

const execution = await executeAction(action, { transport, clock });
execution.status; // 'idle' | 'pending' | 'success' | 'error' (Spec-11.2)

The status is a normal derived value, so a form can branch on it (match $submit_application.status). ActionStatusStore holds the status of every action for a host that wants to read it.

retry uses exponential backoff with jitter and is capped at the declared value; a 4xx response is never retried (Spec-11.3).

Resolving a source

ts
import { SourceResolver } from '@formscript/runtime';

const resolver = new SourceResolver({ transport, clock, sources });
resolver.status('countries'); // 'idle' — nothing is fetched until it is referenced (Spec-11.5)
await resolver.resolve('countries');
resolver.status('countries'); // 'ready' — the source produced its value (Spec-6.3)

A source is lazily evaluated and cached (Spec-11.5). Its cache lifetime is none, 5m, 1h, 24h, 7d or an explicit duration (Spec-11.6); an explicit duration is calendar-relative (Spec-3.12). A field whose options come from a source renders in a pending state until the source resolves (Spec-11.7).

Offline behaviour

ts
import { OfflineQueue } from '@formscript/runtime';

const queue = new OfflineQueue(store, clock);
await queue.enqueue(action, request);
const outcomes = await queue.replay(transport);

A runtime operating offline must preserve unsent submissions in a durable queue and replay them on reconnect, in submission order (Spec-11.9). The store is the host's, because durability is the host's business. Each submission follows the conflict policy of its action (Spec-11.10): retry sends it again, skip leaves it alone, and fail stops the replay. The default is skip, so an action that declares no policy is never re-sent. Every outcome is reported, so a replay is never silent.

The host interfaces

ts
interface Transport {
  send(request: TransportRequest): Promise<TransportResponse>;
}

interface Clock {
  now(): number;
  sleep(ms: number): Promise<void>;
}

Transport is the network the runtime does not own (Spec-11.1). Clock supplies the time and the wait the backoff needs (Spec-11.3). A test supplies its own, which is why nothing in this package opens a socket.

Judgement calls

  • The action's request target is its url. §8.7 gives an action a url, a method, a body, a when, a timeout, a retry and a conflict, and no target and no params: params belongs to source. The model accepts a host-supplied parameter map and headers on both declarations, and a document that declares params on an action is reported by the analyzer as E-PROP-02.
  • The parameter mapping is an extension by analogy. §11.1 states no mapping rules, so the rules above extend the two absences of the language: an absent value is asked-but-not-answered (Spec-4.35) and an inapplicable value is not-asked (Spec-8.3).
  • The retry cap is a cap on the number of retries. "retry MUST use exponential backoff with jitter and MUST cap at the declared value" (Spec-11.3) is read as: at most retry retries, so retry + 1 attempts.
  • The backoff shape is not specified. The first delay is 1000 ms, the delay doubles with every attempt, and the jitter is half the exponential delay. The source of the jitter is injected, so a test is deterministic.
  • The headers are the host's. §8.7 lists no headers property and Spec-11.1 gives auth to the runtime, so the headers of a declaration are host-supplied rather than document-declared. The runtime adds content-type: application/json when it sends a body and the host has not set one.
  • The source status has its own vocabulary. Spec-6.3 names it: idle | pending | ready | error. A source that has produced its value is ready, which is what fieldReadAfterSource tests for; the runtime declares the vocabulary itself, because the engine declares the same one and the dependency must not run from the engine to the runtime (Spec-6.3, Spec-11.7).
  • A failed source leaves the field unknown. The diagnostic is E-IO-01 ("source fetch failed"). The field keeps its content, so it reads null (Spec-5.8); it does not become inapplicable, because the question was asked, and it does not take the error value, which would poison the data plane (Spec-5.3).
  • A source that fails is not retried. Spec-11.3 gives the retry policy to actions only.
  • The queue removes a skipped submission and keeps a failed one. A skip submission is deliberately not sent, so it leaves the queue; a fail submission stays queued and stops the replay, so that the submission order of Spec-11.9 is preserved.

Ambiguities in the specification

  • Spec-11.3's "MUST cap at the declared value" does not say whether the cap is on the number of retries or on the delay.
  • Spec-11.10 does not say how a runtime reports an action whose conflict policy was not chosen explicitly. The runtime records it as conflictDeclared and leaves the reporting to the host.

Two ambiguities that earlier revisions of this package listed here have been repaired in the specification itself: §11.3 now says what an unreachable source does (Spec-11.11), and Spec-11.10 now states the default conflict policy and that it MUST NOT be retry.

Public API

74 exported symbols.

ACTION_STATUSES re-export
ActionDeclaration interface

A resolved `action` declaration; the runtime owns transport, auth and serialisation (Spec-11.1).

ActionExecution interface
ActionExecutionOptions interface
ActionProperties interface

The document-level properties of an action, before evaluation (Spec-11.1).

ActionResolution interface

The values a host has evaluated for a declaration.

ActionState interface
ActionStatus type

The status of an action (Spec-11.2); a normal derived value a form can branch on.

ActionStatusStore re-export
buildActionRequest re-export
buildRequest re-export
buildSourceRequest re-export
cacheDurationMs function

The lifetime of a cache policy in milliseconds; an explicit duration is calendar-relative (Spec-3.12).

CachePolicy type

The cache lifetimes of §11.6: the five named values, or an explicit duration.

canRetry function

Whether the action may be tried again; a 4xx response is never retried (Spec-11.3).

Clock interface

The clock the host must supply.

CONFLICT_POLICIES const
ConflictPolicy type

The replay conflict policies of §11.3.

DEFAULT_ACTION_METHOD const

The default method of an action (§8.7).

DEFAULT_CACHE_POLICY const

The default cache lifetime of a source (§8.7).

DEFAULT_CONFLICT_POLICY const

The default replay conflict policy of an action (Spec-11.10).

DEFAULT_SOURCE_METHOD const

The default method of a source (§8.7).

Duration interface

Duration literals (§3.6): `30d`, `6mo`, `2y`, `PT30M`; calendar-relative (Spec-3.12).

durationMs function

The length of a duration literal in milliseconds; `undefined` when invalid.

durationMsOf function

The length of a duration in milliseconds, measured from `from` (Spec-3.12).

EnqueueResult type
executeAction function

Runs an action; the status moves `idle` → `pending` → `success` or `error` (Spec-11.2).

fieldReadAfterSource re-export
HTTP_METHODS const
HttpMethod type

The HTTP methods a request may use (§8.7).

isActionStatus re-export
isCachePolicy function
isClientErrorStatus re-export
isConflictPolicy function
isHttpMethod function
isSourceStatus re-export
isSuccessStatus re-export
mapParameter re-export
mapParameters re-export
MappedParameter interface
NAMED_CACHE_POLICIES const
OfflineQueue re-export
OfflineStore interface

The durable storage of the queue; a runtime operating offline must preserve submissions (Spec-11.9).

ParameterMap type

The evaluated values of the parameters of a request (§8.7).

ParameterReason type

Why a parameter was omitted or failed.

ParameterState type

What happened to one parameter of a request.

parseDuration function

Parses a duration literal; `undefined` when the text is not one.

QueuedSubmission interface

A submission that has not reached the server yet.

QueueFullError re-export
readActionProperties function

Reads the properties of an `action` block (§8.7, §11.1).

readSourceProperties function

Reads the properties of a `source` block (§8.7, §11.2).

ReplayOutcome interface
ReplayOutcomeKind type
RequestBuildResult type
RequestDescription interface

A request the runtime is about to send. The transport is the host's.

RequestOptions interface

The values a host supplies when it asks for a request.

resolveAction function

Builds the runtime declaration of an action from its properties (§11.1).

resolveSource function

Builds the runtime declaration of a source from its properties (§11.2).

RETRY_BASE_DELAY_MS const

The first delay of the exponential backoff of Spec-11.3.

retryDelayMs function

The delay before the next attempt: exponential backoff with jitter (Spec-11.3).

serializeBody re-export
SOURCE_STATUSES re-export
SourceDeclaration interface

A resolved `source` declaration (§11.2).

SourceFetchResult interface
SourceProperties interface

The document-level properties of a source, before evaluation (§11.2).

SourceResolution interface
SourceResolver re-export
SourceResolverOptions interface
SourceState interface
SourceStatus type

The status of a source (Spec-6.3); a resolved source is `ready`.

Transport interface

The transport the host must supply; the runtime owns the request, never the network (Spec-11.1).

TransportRequest interface

The request the runtime hands to the host's transport.

TransportResponse interface

What the host's transport reports back.

TransportTimeoutError re-export