@cynodia/axiom-runtime 0.5.2-alpha.1 → 0.6.1-alpha.1
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 +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mutation/values.d.ts +10 -0
- package/dist/mutation/values.js +23 -3
- package/dist/remote.d.ts +51 -0
- package/dist/remote.js +101 -0
- package/dist/runtime-types.d.ts +19 -0
- package/dist/runtime-types.js +1 -0
- package/dist/runtime.d.ts +102 -3
- package/dist/runtime.js +271 -13
- package/dist/source.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,13 +19,13 @@ Main exports: `createAxiomRuntime`, `createBrowserHost`, `createMemoryHost`,
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm install @cynodia/axiom-runtime
|
|
22
|
+
npm install @cynodia/axiom-runtime
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Most applications should install the facade package instead, which re-exports this one:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npm install @cynodia/axiom
|
|
28
|
+
npm install @cynodia/axiom
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './mutation/store.js';
|
|
|
4
4
|
export * from './mutation/transaction.js';
|
|
5
5
|
export * from './mutation/resolve-location.js';
|
|
6
6
|
export * from './mutation/mutation-engine.js';
|
|
7
|
+
export * from './remote.js';
|
|
7
8
|
export * from './format.js';
|
|
8
9
|
export * from './presentation-classes.js';
|
|
9
10
|
export * from './runtime.js';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export * from './mutation/store.js';
|
|
|
4
4
|
export * from './mutation/transaction.js';
|
|
5
5
|
export * from './mutation/resolve-location.js';
|
|
6
6
|
export * from './mutation/mutation-engine.js';
|
|
7
|
+
export * from './remote.js';
|
|
7
8
|
export * from './format.js';
|
|
8
9
|
export * from './presentation-classes.js';
|
|
9
10
|
export * from './runtime.js';
|
|
@@ -31,6 +31,16 @@ export declare function isPresent(value: unknown): boolean;
|
|
|
31
31
|
export declare function isEmptyValue(value: unknown): boolean;
|
|
32
32
|
export declare function toBoolean(value: unknown): boolean;
|
|
33
33
|
export declare function toText(value: unknown): string;
|
|
34
|
+
/**
|
|
35
|
+
* Lexicographic order by Unicode code point.
|
|
36
|
+
*
|
|
37
|
+
* Not locale collation, and deliberately not the language's default string comparison:
|
|
38
|
+
* ordering is part of the semantic contract, so two conforming runtimes must agree on it
|
|
39
|
+
* character for character. Code points are the one ordering every language can reproduce
|
|
40
|
+
* exactly — a UTF-16 comparison, which is what `<` does here, disagrees with a UTF-8 one
|
|
41
|
+
* whenever a string mixes astral characters with U+E000..U+FFFF.
|
|
42
|
+
*/
|
|
43
|
+
export declare function compareText(left: string, right: string): number;
|
|
34
44
|
export declare function compareValues(left: unknown, right: unknown): number;
|
|
35
45
|
export declare function valuesEqual(left: unknown, right: unknown): boolean;
|
|
36
46
|
//# sourceMappingURL=values.d.ts.map
|
package/dist/mutation/values.js
CHANGED
|
@@ -76,13 +76,33 @@ export function toText(value) {
|
|
|
76
76
|
}
|
|
77
77
|
return JSON.stringify(value);
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Lexicographic order by Unicode code point.
|
|
81
|
+
*
|
|
82
|
+
* Not locale collation, and deliberately not the language's default string comparison:
|
|
83
|
+
* ordering is part of the semantic contract, so two conforming runtimes must agree on it
|
|
84
|
+
* character for character. Code points are the one ordering every language can reproduce
|
|
85
|
+
* exactly — a UTF-16 comparison, which is what `<` does here, disagrees with a UTF-8 one
|
|
86
|
+
* whenever a string mixes astral characters with U+E000..U+FFFF.
|
|
87
|
+
*/
|
|
88
|
+
export function compareText(left, right) {
|
|
89
|
+
const leftPoints = Array.from(left);
|
|
90
|
+
const rightPoints = Array.from(right);
|
|
91
|
+
const shared = Math.min(leftPoints.length, rightPoints.length);
|
|
92
|
+
for (let index = 0; index < shared; index += 1) {
|
|
93
|
+
const leftPoint = leftPoints[index].codePointAt(0);
|
|
94
|
+
const rightPoint = rightPoints[index].codePointAt(0);
|
|
95
|
+
if (leftPoint !== rightPoint) {
|
|
96
|
+
return leftPoint < rightPoint ? -1 : 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return leftPoints.length === rightPoints.length ? 0 : leftPoints.length < rightPoints.length ? -1 : 1;
|
|
100
|
+
}
|
|
79
101
|
export function compareValues(left, right) {
|
|
80
102
|
if (typeof left === 'number' && typeof right === 'number') {
|
|
81
103
|
return left === right ? 0 : left < right ? -1 : 1;
|
|
82
104
|
}
|
|
83
|
-
|
|
84
|
-
const rightText = toText(right);
|
|
85
|
-
return leftText === rightText ? 0 : leftText < rightText ? -1 : 1;
|
|
105
|
+
return compareText(toText(left), toText(right));
|
|
86
106
|
}
|
|
87
107
|
/** A stable serialization, so record comparison does not depend on key order. */
|
|
88
108
|
function canonical(value) {
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { NodeId, RuntimeDiagnostic } from './runtime-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A browser-safe gateway to an Axiom authority.
|
|
4
|
+
*
|
|
5
|
+
* It lives here, in the runtime, rather than in the server package, because a browser
|
|
6
|
+
* client needs it and that package imports Node built-ins a browser has no use for.
|
|
7
|
+
* Everything below uses `fetch` and nothing else, so a generated page can reach an
|
|
8
|
+
* authority with no application code and no Node dependency at all.
|
|
9
|
+
*
|
|
10
|
+
* The protocol is the semantic one: the client asks for actions, never for URLs. The
|
|
11
|
+
* endpoint is a single path, the same for every application.
|
|
12
|
+
*/
|
|
13
|
+
/** Must match `PROTOCOL_VERSION` in `@cynodia/axiom-server`. */
|
|
14
|
+
export declare const AXIOM_PROTOCOL_VERSION = "axiom.protocol.v1";
|
|
15
|
+
/** The endpoint an Axiom authority answers on, unless a host says otherwise. */
|
|
16
|
+
export declare const AXIOM_DEFAULT_ENDPOINT = "/axiom";
|
|
17
|
+
export interface HttpRemoteGatewayOptions {
|
|
18
|
+
/** Defaults to `/axiom` on the current origin. */
|
|
19
|
+
endpoint?: string;
|
|
20
|
+
/** Read per request, so a host can refresh a credential without rebuilding the client. */
|
|
21
|
+
credential?: () => string | null;
|
|
22
|
+
/** Defaults to the global `fetch`. */
|
|
23
|
+
fetch?: typeof globalThis.fetch;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Builds the gateway a client runtime is configured with.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* createAxiomRuntime({ ir, rootElement, host, remote: createHttpRemoteGateway() });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* A transport failure becomes a structured diagnostic rather than an exception escaping
|
|
34
|
+
* into application code, so a refused or unreachable authority reaches the interface the
|
|
35
|
+
* same way a refused local action does.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createHttpRemoteGateway(options?: HttpRemoteGatewayOptions): {
|
|
38
|
+
invoke(request: {
|
|
39
|
+
actionId: NodeId;
|
|
40
|
+
arguments: Record<string, unknown>;
|
|
41
|
+
requestId: string;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
ok: boolean;
|
|
44
|
+
diagnostics: RuntimeDiagnostic[];
|
|
45
|
+
changes: Record<NodeId, unknown>;
|
|
46
|
+
}>;
|
|
47
|
+
snapshot(): Promise<{
|
|
48
|
+
states: Record<NodeId, unknown>;
|
|
49
|
+
}>;
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=remote.d.ts.map
|
package/dist/remote.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A browser-safe gateway to an Axiom authority.
|
|
3
|
+
*
|
|
4
|
+
* It lives here, in the runtime, rather than in the server package, because a browser
|
|
5
|
+
* client needs it and that package imports Node built-ins a browser has no use for.
|
|
6
|
+
* Everything below uses `fetch` and nothing else, so a generated page can reach an
|
|
7
|
+
* authority with no application code and no Node dependency at all.
|
|
8
|
+
*
|
|
9
|
+
* The protocol is the semantic one: the client asks for actions, never for URLs. The
|
|
10
|
+
* endpoint is a single path, the same for every application.
|
|
11
|
+
*/
|
|
12
|
+
/** Must match `PROTOCOL_VERSION` in `@cynodia/axiom-server`. */
|
|
13
|
+
export const AXIOM_PROTOCOL_VERSION = 'axiom.protocol.v1';
|
|
14
|
+
/** The endpoint an Axiom authority answers on, unless a host says otherwise. */
|
|
15
|
+
export const AXIOM_DEFAULT_ENDPOINT = '/axiom';
|
|
16
|
+
function unreachable(message) {
|
|
17
|
+
return {
|
|
18
|
+
code: 'AUTHORITY_UNREACHABLE',
|
|
19
|
+
message,
|
|
20
|
+
severity: 'error',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Builds the gateway a client runtime is configured with.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* createAxiomRuntime({ ir, rootElement, host, remote: createHttpRemoteGateway() });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* A transport failure becomes a structured diagnostic rather than an exception escaping
|
|
31
|
+
* into application code, so a refused or unreachable authority reaches the interface the
|
|
32
|
+
* same way a refused local action does.
|
|
33
|
+
*/
|
|
34
|
+
export function createHttpRemoteGateway(options = {}) {
|
|
35
|
+
const endpoint = options.endpoint ?? AXIOM_DEFAULT_ENDPOINT;
|
|
36
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
37
|
+
async function send(body) {
|
|
38
|
+
if (!fetchImpl) {
|
|
39
|
+
throw new Error('No fetch implementation is available');
|
|
40
|
+
}
|
|
41
|
+
const controller = options.timeoutMs && typeof AbortController === 'function' ? new AbortController() : undefined;
|
|
42
|
+
const timer = controller ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetchImpl(endpoint, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'content-type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
...body,
|
|
49
|
+
protocol: AXIOM_PROTOCOL_VERSION,
|
|
50
|
+
credential: options.credential?.() ?? null,
|
|
51
|
+
}),
|
|
52
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`The authority answered ${response.status}`);
|
|
56
|
+
}
|
|
57
|
+
return (await response.json());
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
if (timer !== undefined) {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
async invoke(request) {
|
|
67
|
+
try {
|
|
68
|
+
const answer = await send({
|
|
69
|
+
kind: 'invoke',
|
|
70
|
+
actionId: request.actionId,
|
|
71
|
+
arguments: request.arguments,
|
|
72
|
+
requestId: request.requestId,
|
|
73
|
+
});
|
|
74
|
+
if (answer.kind === 'result') {
|
|
75
|
+
return {
|
|
76
|
+
ok: answer.ok === true,
|
|
77
|
+
diagnostics: answer.diagnostics ?? [],
|
|
78
|
+
changes: answer.changes ?? {},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { ok: false, diagnostics: answer.diagnostics ?? [], changes: {} };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
diagnostics: [unreachable(error instanceof Error ? error.message : String(error))],
|
|
87
|
+
changes: {},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
async snapshot() {
|
|
92
|
+
// A failed snapshot must reject: `start()` distinguishes "not loaded" from "empty",
|
|
93
|
+
// and swallowing the failure here would erase that difference.
|
|
94
|
+
const answer = await send({ kind: 'snapshot' });
|
|
95
|
+
if (answer.kind !== 'snapshot' || !answer.snapshot) {
|
|
96
|
+
throw new Error('The authority did not return a snapshot');
|
|
97
|
+
}
|
|
98
|
+
return { states: answer.snapshot.states ?? {} };
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The handful of types the browser-safe gateway needs, declared without importing the
|
|
3
|
+
* runtime module.
|
|
4
|
+
*
|
|
5
|
+
* `remote.ts` is inlined into generated pages, and a value import from core would be
|
|
6
|
+
* stripped from that bundle. Keeping these local also means the gateway pulls in nothing
|
|
7
|
+
* else at all.
|
|
8
|
+
*/
|
|
9
|
+
export type NodeId = string & {
|
|
10
|
+
readonly __brand?: 'NodeId';
|
|
11
|
+
};
|
|
12
|
+
export interface RuntimeDiagnostic {
|
|
13
|
+
code: string;
|
|
14
|
+
message: string;
|
|
15
|
+
severity: 'error' | 'warning';
|
|
16
|
+
details?: Record<string, unknown>;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=runtime-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApplicationIR, CompiledRoute, FieldId, Location, NodeId } from '@cynodia/axiom-core';
|
|
1
|
+
import type { ApplicationIR, CompiledRoute, Expression, FieldId, Location, NodeId } from '@cynodia/axiom-core';
|
|
2
2
|
import type { DomElement, HostEnvironment } from './dom.js';
|
|
3
3
|
import type { MutationLogEntry } from './mutation/mutation-engine.js';
|
|
4
4
|
/**
|
|
@@ -29,6 +29,12 @@ export declare const RUNTIME_DIAGNOSTIC_CODES: {
|
|
|
29
29
|
readonly UNSUPPORTED_UI_NODE: "UNSUPPORTED_UI_NODE";
|
|
30
30
|
readonly INPUT_REJECTED: "INPUT_REJECTED";
|
|
31
31
|
readonly PERSISTED_STATE_UNREADABLE: "PERSISTED_STATE_UNREADABLE";
|
|
32
|
+
/** A local write was attempted against state whose authority is the server. */
|
|
33
|
+
readonly SERVER_STATE_WRITE: "SERVER_STATE_WRITE";
|
|
34
|
+
/** An action belongs to the authority, but no gateway to it was configured. */
|
|
35
|
+
readonly REMOTE_ACTION_UNAVAILABLE: "REMOTE_ACTION_UNAVAILABLE";
|
|
36
|
+
/** The authority could not be reached. Authoritative state was not loaded. */
|
|
37
|
+
readonly AUTHORITY_UNREACHABLE: "AUTHORITY_UNREACHABLE";
|
|
32
38
|
};
|
|
33
39
|
export type RuntimeDiagnosticCode = (typeof RUNTIME_DIAGNOSTIC_CODES)[keyof typeof RUNTIME_DIAGNOSTIC_CODES];
|
|
34
40
|
/**
|
|
@@ -52,6 +58,34 @@ export interface RuntimeDiagnostic {
|
|
|
52
58
|
export interface ActionResult {
|
|
53
59
|
ok: boolean;
|
|
54
60
|
diagnostics: RuntimeDiagnostic[];
|
|
61
|
+
/**
|
|
62
|
+
* Set when the invocation was dispatched to the authority. `ok` is not yet meaningful:
|
|
63
|
+
* the outcome arrives later, and reaches the interface through the action's recorded
|
|
64
|
+
* outcome and any `diagnostic` node presenting it.
|
|
65
|
+
*/
|
|
66
|
+
pending?: true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* How a client reaches the authority.
|
|
70
|
+
*
|
|
71
|
+
* The client requests **semantic actions**; it never sends operations. A remote invocation
|
|
72
|
+
* is dispatched and answered later, so `invokeAction` returns `pending` and the outcome
|
|
73
|
+
* arrives through the same diagnostic lifecycle a local refusal uses.
|
|
74
|
+
*/
|
|
75
|
+
export interface RemoteGateway {
|
|
76
|
+
invoke(request: {
|
|
77
|
+
actionId: NodeId;
|
|
78
|
+
arguments: Record<string, unknown>;
|
|
79
|
+
requestId: string;
|
|
80
|
+
}): Promise<{
|
|
81
|
+
ok: boolean;
|
|
82
|
+
diagnostics: RuntimeDiagnostic[];
|
|
83
|
+
changes: Record<NodeId, unknown>;
|
|
84
|
+
}>;
|
|
85
|
+
/** The authoritative values of every observable state. */
|
|
86
|
+
snapshot?(): Promise<{
|
|
87
|
+
states: Record<NodeId, unknown>;
|
|
88
|
+
}>;
|
|
55
89
|
}
|
|
56
90
|
/**
|
|
57
91
|
* The outcome of an action's most recent invocation.
|
|
@@ -63,7 +97,8 @@ export interface ActionResult {
|
|
|
63
97
|
*/
|
|
64
98
|
export interface ActionOutcome {
|
|
65
99
|
actionId: NodeId;
|
|
66
|
-
outcome
|
|
100
|
+
/** `pending` means the request is with the authority and the outcome is not yet known. */
|
|
101
|
+
outcome: 'ok' | 'failed' | 'cancelled' | 'pending';
|
|
67
102
|
diagnostics: RuntimeDiagnostic[];
|
|
68
103
|
}
|
|
69
104
|
export interface RouteMatch {
|
|
@@ -83,12 +118,45 @@ export interface AxiomRuntimeOptions {
|
|
|
83
118
|
rootElement: DomElement;
|
|
84
119
|
host: HostEnvironment;
|
|
85
120
|
nativeOperations?: Record<string, NativeImplementation>;
|
|
121
|
+
/**
|
|
122
|
+
* How to reach the authority. Required if the application has server-authoritative
|
|
123
|
+
* state; without it a remote invocation reports `REMOTE_ACTION_UNAVAILABLE`.
|
|
124
|
+
*/
|
|
125
|
+
remote?: RemoteGateway;
|
|
86
126
|
inputValidation?: InputValidationMode;
|
|
87
127
|
/** Records previous and next values in the mutation log. */
|
|
88
128
|
recordMutationValues?: boolean;
|
|
89
129
|
}
|
|
90
130
|
export interface AxiomRuntime {
|
|
91
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Brings the runtime to its initial usable state.
|
|
133
|
+
*
|
|
134
|
+
* The lifecycle is fixed and does not depend on whether a gateway is configured:
|
|
135
|
+
*
|
|
136
|
+
* 1. local state is initialized from `initialValue` and persistence, before anything else;
|
|
137
|
+
* 2. route matching is resolved and the application renders once, so a slow authority
|
|
138
|
+
* never leaves a blank page;
|
|
139
|
+
* 3. if a `remote` gateway with a snapshot is configured, authoritative state is loaded
|
|
140
|
+
* and applied, and the application renders again.
|
|
141
|
+
*
|
|
142
|
+
* `start()` returns a promise that settles when step 3 has completed. Awaiting it is the
|
|
143
|
+
* whole startup sequence: **there is no second call to remember.** Ignoring the promise
|
|
144
|
+
* is safe — steps 1 and 2 have already run synchronously — but authoritative state may
|
|
145
|
+
* not have arrived yet.
|
|
146
|
+
*
|
|
147
|
+
* If synchronization fails, the failure is reported as an `AUTHORITY_UNREACHABLE`
|
|
148
|
+
* diagnostic and `authoritativeStateLoaded()` stays false, so an empty authoritative
|
|
149
|
+
* collection is never mistaken for a loaded one.
|
|
150
|
+
*/
|
|
151
|
+
start(): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Whether authoritative state has been loaded successfully.
|
|
154
|
+
*
|
|
155
|
+
* `false` with a configured gateway means the authority has not answered — which is not
|
|
156
|
+
* the same as an authoritative collection that is genuinely empty. Applications with no
|
|
157
|
+
* remote gateway are always `true`: all their state is local.
|
|
158
|
+
*/
|
|
159
|
+
authoritativeStateLoaded(): boolean;
|
|
92
160
|
render(): void;
|
|
93
161
|
/** A deep clone of the value. Derived state is recomputed. */
|
|
94
162
|
getState(id: NodeId): unknown;
|
|
@@ -121,6 +189,37 @@ export interface AxiomRuntime {
|
|
|
121
189
|
/** Every mutation this runtime has applied, in order, with its semantic location. */
|
|
122
190
|
getMutationLog(): MutationLogEntry[];
|
|
123
191
|
registerNativeOperation(implementationId: string, implementation: NativeImplementation): void;
|
|
192
|
+
/**
|
|
193
|
+
* Invokes an action and waits for its outcome. For a remote action this awaits the
|
|
194
|
+
* authority's answer; for a local one it is `invokeAction` in promise form.
|
|
195
|
+
*/
|
|
196
|
+
invokeActionAsync(id: NodeId, args?: Record<string, unknown>): Promise<ActionResult>;
|
|
197
|
+
/**
|
|
198
|
+
* Loads the authoritative snapshot and applies it. Called by `start()` when a gateway
|
|
199
|
+
* provides one.
|
|
200
|
+
*/
|
|
201
|
+
syncAuthoritativeState(): Promise<void>;
|
|
202
|
+
/**
|
|
203
|
+
* Resolves when no remote invocation is outstanding.
|
|
204
|
+
*
|
|
205
|
+
* An action started from the interface has no promise the caller can hold; this is how a
|
|
206
|
+
* test, a script or a host waits for the authority to have answered without guessing a
|
|
207
|
+
* delay. Resolves immediately when nothing is in flight.
|
|
208
|
+
*/
|
|
209
|
+
settled(): Promise<void>;
|
|
210
|
+
/**
|
|
211
|
+
* Evaluates an expression in the root scope, reporting rather than throwing. It is a
|
|
212
|
+
* pure read: an expression cannot change state.
|
|
213
|
+
*
|
|
214
|
+
* An authority uses it to evaluate an authorization rule before opening a transaction.
|
|
215
|
+
*/
|
|
216
|
+
evaluate(expression: Expression): {
|
|
217
|
+
ok: true;
|
|
218
|
+
value: unknown;
|
|
219
|
+
} | {
|
|
220
|
+
ok: false;
|
|
221
|
+
diagnostic: RuntimeDiagnostic;
|
|
222
|
+
};
|
|
124
223
|
}
|
|
125
224
|
export declare function createAxiomRuntime(options: AxiomRuntimeOptions): AxiomRuntime;
|
|
126
225
|
/** Builds a host bound to the browser globals. Used by generated pages. */
|
package/dist/runtime.js
CHANGED
|
@@ -33,6 +33,12 @@ export const RUNTIME_DIAGNOSTIC_CODES = {
|
|
|
33
33
|
UNSUPPORTED_UI_NODE: 'UNSUPPORTED_UI_NODE',
|
|
34
34
|
INPUT_REJECTED: 'INPUT_REJECTED',
|
|
35
35
|
PERSISTED_STATE_UNREADABLE: 'PERSISTED_STATE_UNREADABLE',
|
|
36
|
+
/** A local write was attempted against state whose authority is the server. */
|
|
37
|
+
SERVER_STATE_WRITE: 'SERVER_STATE_WRITE',
|
|
38
|
+
/** An action belongs to the authority, but no gateway to it was configured. */
|
|
39
|
+
REMOTE_ACTION_UNAVAILABLE: 'REMOTE_ACTION_UNAVAILABLE',
|
|
40
|
+
/** The authority could not be reached. Authoritative state was not loaded. */
|
|
41
|
+
AUTHORITY_UNREACHABLE: 'AUTHORITY_UNREACHABLE',
|
|
36
42
|
};
|
|
37
43
|
const MISSING = Symbol('missing');
|
|
38
44
|
function unwrapType(type) {
|
|
@@ -85,6 +91,17 @@ function describeValue(value) {
|
|
|
85
91
|
}
|
|
86
92
|
return `${typeof value} ${JSON.stringify(value)}`;
|
|
87
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Distinguishes runtimes that share a process.
|
|
96
|
+
*
|
|
97
|
+
* `host.uuid()` is the only entropy a runtime has, and a deterministic host — the memory
|
|
98
|
+
* host, a conformance host, a test double — hands every runtime it constructs the same
|
|
99
|
+
* sequence. Two clients would then generate the same request id for their first remote
|
|
100
|
+
* invocation, and the authority would answer the second from the first one's idempotency
|
|
101
|
+
* record. A counter that lives above the host closes that: within a process it is what
|
|
102
|
+
* separates two runtimes, and across processes a real host's uuid is.
|
|
103
|
+
*/
|
|
104
|
+
let runtimeSessions = 0;
|
|
88
105
|
export function createAxiomRuntime(options) {
|
|
89
106
|
const { ir, rootElement, host } = options;
|
|
90
107
|
const store = createStateStore();
|
|
@@ -93,12 +110,35 @@ export function createAxiomRuntime(options) {
|
|
|
93
110
|
const diagnostics = [];
|
|
94
111
|
/** Rendered controls, keyed by render instance — not by node id. */
|
|
95
112
|
const inputElements = new Map();
|
|
113
|
+
/**
|
|
114
|
+
* How each rendered form submits, keyed by the form's render instance.
|
|
115
|
+
*
|
|
116
|
+
* A form with a declared submit control must invoke the action **exactly** as that button
|
|
117
|
+
* would on its own — same arguments, evaluated in the button's own scope. Registering the
|
|
118
|
+
* button's invocation here is what makes the two paths literally the same code, rather
|
|
119
|
+
* than two that have to be kept in step.
|
|
120
|
+
*/
|
|
121
|
+
const submitInvokers = new Map();
|
|
96
122
|
let focusedInstance = null;
|
|
97
123
|
let focusedCaret = null;
|
|
98
124
|
let started = false;
|
|
99
125
|
let transactionCounter = 0;
|
|
100
126
|
const mutationLog = [];
|
|
101
127
|
const inputValidation = options.inputValidation ?? 'immediate';
|
|
128
|
+
const remote = options.remote;
|
|
129
|
+
const remoteActionIds = new Set(ir.remoteActionIds ?? []);
|
|
130
|
+
/**
|
|
131
|
+
* Set only while an authoritative answer is being applied. The authority owns the value;
|
|
132
|
+
* every other path is refused, which is what makes the boundary structural rather than a
|
|
133
|
+
* convention about where inputs are bound.
|
|
134
|
+
*/
|
|
135
|
+
let applyingAuthoritative = false;
|
|
136
|
+
let remoteRequests = 0;
|
|
137
|
+
/** Generated once, and only when this runtime can actually talk to an authority. */
|
|
138
|
+
const sessionId = remote ? `s${(runtimeSessions += 1)}-${host.uuid()}` : '';
|
|
139
|
+
/** False until the authority has answered, when one is configured. */
|
|
140
|
+
let authoritativeLoaded = options.remote?.snapshot === undefined;
|
|
141
|
+
let startup = Promise.resolve();
|
|
102
142
|
const theme = ir.theme;
|
|
103
143
|
const locale = theme?.locale ?? 'en-US';
|
|
104
144
|
/**
|
|
@@ -272,6 +312,18 @@ export function createAxiomRuntime(options) {
|
|
|
272
312
|
}
|
|
273
313
|
/** The only place the store is written. Values are frozen on the way in. */
|
|
274
314
|
function writeState(stateId, value) {
|
|
315
|
+
if (!applyingAuthoritative && ir.authority?.[stateId] === 'server') {
|
|
316
|
+
// Whatever the path — an action, an input, an administrative hydrate — a client does
|
|
317
|
+
// not commit state the authority owns.
|
|
318
|
+
report({
|
|
319
|
+
code: RUNTIME_DIAGNOSTIC_CODES.SERVER_STATE_WRITE,
|
|
320
|
+
message: `${stateId} is server-authoritative and cannot be written by this client`,
|
|
321
|
+
severity: 'error',
|
|
322
|
+
nodeId: stateId,
|
|
323
|
+
stateId: stateId,
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
275
327
|
if (!statesById.has(stateId)) {
|
|
276
328
|
report({
|
|
277
329
|
code: RUNTIME_DIAGNOSTIC_CODES.UNKNOWN_STATE,
|
|
@@ -501,19 +553,26 @@ export function createAxiomRuntime(options) {
|
|
|
501
553
|
}
|
|
502
554
|
const left = evaluate(leftExpression, scope);
|
|
503
555
|
const right = evaluate(rightExpression, scope);
|
|
556
|
+
/**
|
|
557
|
+
* A number that is not finite has no place in an ordering. Every ordered comparison
|
|
558
|
+
* against one is false, so a guard fails closed rather than passing on a value that
|
|
559
|
+
* could not be computed — or one a hostile caller supplied.
|
|
560
|
+
*/
|
|
561
|
+
const unordered = (typeof left === 'number' && !Number.isFinite(left)) ||
|
|
562
|
+
(typeof right === 'number' && !Number.isFinite(right));
|
|
504
563
|
switch (operator) {
|
|
505
564
|
case 'eq':
|
|
506
565
|
return valuesEqual(left, right);
|
|
507
566
|
case 'neq':
|
|
508
567
|
return !valuesEqual(left, right);
|
|
509
568
|
case 'gt':
|
|
510
|
-
return compareValues(left, right) > 0;
|
|
569
|
+
return !unordered && compareValues(left, right) > 0;
|
|
511
570
|
case 'gte':
|
|
512
|
-
return compareValues(left, right) >= 0;
|
|
571
|
+
return !unordered && compareValues(left, right) >= 0;
|
|
513
572
|
case 'lt':
|
|
514
|
-
return compareValues(left, right) < 0;
|
|
573
|
+
return !unordered && compareValues(left, right) < 0;
|
|
515
574
|
case 'lte':
|
|
516
|
-
return compareValues(left, right) <= 0;
|
|
575
|
+
return !unordered && compareValues(left, right) <= 0;
|
|
517
576
|
case 'add':
|
|
518
577
|
return Number(left ?? 0) + Number(right ?? 0);
|
|
519
578
|
case 'subtract':
|
|
@@ -930,7 +989,135 @@ export function createAxiomRuntime(options) {
|
|
|
930
989
|
});
|
|
931
990
|
}
|
|
932
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* Applies an authoritative answer. The only path permitted to write server-owned state —
|
|
994
|
+
* and it still goes through `writeState`, so the store keeps exactly one writer.
|
|
995
|
+
*/
|
|
996
|
+
function applyAuthoritative(changes) {
|
|
997
|
+
applyingAuthoritative = true;
|
|
998
|
+
try {
|
|
999
|
+
for (const [stateId, value] of Object.entries(changes)) {
|
|
1000
|
+
if (statesById.has(stateId)) {
|
|
1001
|
+
writeState(stateId, cloneValue(value));
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
finally {
|
|
1006
|
+
applyingAuthoritative = false;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Dispatches a semantic action to the authority.
|
|
1011
|
+
*
|
|
1012
|
+
* The client sends an action id and typed arguments — never operations. The answer is
|
|
1013
|
+
* applied when it arrives, and recorded through the same outcome lifecycle a local
|
|
1014
|
+
* refusal uses, so a `diagnostic` node presents a server refusal exactly as it presents
|
|
1015
|
+
* a local one.
|
|
1016
|
+
*/
|
|
1017
|
+
function runRemoteAction(action, args) {
|
|
1018
|
+
if (!remote) {
|
|
1019
|
+
const failure = {
|
|
1020
|
+
code: RUNTIME_DIAGNOSTIC_CODES.REMOTE_ACTION_UNAVAILABLE,
|
|
1021
|
+
message: `${action.name ?? action.id} executes on the authority, but no gateway to it is configured`,
|
|
1022
|
+
severity: 'error',
|
|
1023
|
+
nodeId: action.id,
|
|
1024
|
+
actionId: action.id,
|
|
1025
|
+
};
|
|
1026
|
+
report(failure);
|
|
1027
|
+
recordOutcome(action.id, 'failed', [failure]);
|
|
1028
|
+
renderApplication();
|
|
1029
|
+
return { ok: false, diagnostics: [failure] };
|
|
1030
|
+
}
|
|
1031
|
+
if (action.requiresConfirmation && !askForConfirmation(action)) {
|
|
1032
|
+
// Confirmation is interaction, and it happens here. The authority never treats it as
|
|
1033
|
+
// an authorization mechanism.
|
|
1034
|
+
recordOutcome(action.id, 'cancelled', []);
|
|
1035
|
+
renderApplication();
|
|
1036
|
+
return { ok: false, diagnostics: [] };
|
|
1037
|
+
}
|
|
1038
|
+
remoteRequests += 1;
|
|
1039
|
+
// A stable key, so a retry after a lost answer cannot execute the action twice — and one
|
|
1040
|
+
// carrying this runtime's own session identity, so two clients never claim the same key.
|
|
1041
|
+
const requestId = `${ir.id}:${sessionId}:${action.id}:${remoteRequests}:${host.uuid()}`;
|
|
1042
|
+
recordOutcome(action.id, 'pending', []);
|
|
1043
|
+
renderApplication();
|
|
1044
|
+
const settle = remote
|
|
1045
|
+
.invoke({ actionId: action.id, arguments: args, requestId })
|
|
1046
|
+
.then((answer) => {
|
|
1047
|
+
applyAuthoritative(answer.changes ?? {});
|
|
1048
|
+
answer.diagnostics.forEach(report);
|
|
1049
|
+
recordOutcome(action.id, answer.ok ? 'ok' : 'failed', answer.ok ? [] : answer.diagnostics);
|
|
1050
|
+
renderApplication();
|
|
1051
|
+
return { ok: answer.ok, diagnostics: answer.diagnostics };
|
|
1052
|
+
})
|
|
1053
|
+
.catch((error) => {
|
|
1054
|
+
// A transport failure becomes a structured diagnostic, not an escaping exception.
|
|
1055
|
+
const failure = {
|
|
1056
|
+
code: RUNTIME_DIAGNOSTIC_CODES.REMOTE_ACTION_UNAVAILABLE,
|
|
1057
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1058
|
+
severity: 'error',
|
|
1059
|
+
nodeId: action.id,
|
|
1060
|
+
actionId: action.id,
|
|
1061
|
+
};
|
|
1062
|
+
report(failure);
|
|
1063
|
+
recordOutcome(action.id, 'failed', [failure]);
|
|
1064
|
+
renderApplication();
|
|
1065
|
+
return { ok: false, diagnostics: [failure] };
|
|
1066
|
+
});
|
|
1067
|
+
pending.set(action.id, settle);
|
|
1068
|
+
void settle.finally(() => {
|
|
1069
|
+
if (pending.get(action.id) === settle) {
|
|
1070
|
+
pending.delete(action.id);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
return { ok: false, pending: true, diagnostics: [] };
|
|
1074
|
+
}
|
|
1075
|
+
/** In-flight remote invocations, so `invokeActionAsync` can await one. */
|
|
1076
|
+
const pending = new Map();
|
|
1077
|
+
/**
|
|
1078
|
+
* Waits until nothing is outstanding with an authority.
|
|
1079
|
+
*
|
|
1080
|
+
* A remote action started from the interface — a click, a form submit — returns to the
|
|
1081
|
+
* event handler immediately, so there is no promise for the caller to hold. Without this,
|
|
1082
|
+
* anything driving the UI has to guess a delay. It loops because settling one invocation
|
|
1083
|
+
* may start another.
|
|
1084
|
+
*/
|
|
1085
|
+
async function allSettled() {
|
|
1086
|
+
while (pending.size > 0) {
|
|
1087
|
+
await Promise.allSettled([...pending.values()]);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Loads authoritative state and applies it.
|
|
1092
|
+
*
|
|
1093
|
+
* A failure is a diagnostic, not an exception, and leaves `authoritativeLoaded` false —
|
|
1094
|
+
* so an application can tell "the authority has not answered" from "the collection is
|
|
1095
|
+
* empty", which are very different things to show a person.
|
|
1096
|
+
*/
|
|
1097
|
+
async function syncAuthoritative() {
|
|
1098
|
+
if (!remote?.snapshot) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
const snapshot = await remote.snapshot();
|
|
1103
|
+
applyAuthoritative(snapshot.states ?? {});
|
|
1104
|
+
authoritativeLoaded = true;
|
|
1105
|
+
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
authoritativeLoaded = false;
|
|
1108
|
+
report({
|
|
1109
|
+
code: RUNTIME_DIAGNOSTIC_CODES.AUTHORITY_UNREACHABLE,
|
|
1110
|
+
message: `Authoritative state could not be loaded: ${error instanceof Error ? error.message : String(error)}`,
|
|
1111
|
+
severity: 'error',
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
renderApplication();
|
|
1115
|
+
}
|
|
933
1116
|
function runAction(actionId, args = {}) {
|
|
1117
|
+
const remoteAction = remoteActionIds.has(actionId) ? ir.actions[actionId] : undefined;
|
|
1118
|
+
if (remoteAction) {
|
|
1119
|
+
return runRemoteAction(remoteAction, args);
|
|
1120
|
+
}
|
|
934
1121
|
return collecting((collected) => {
|
|
935
1122
|
const started = collected.length;
|
|
936
1123
|
const result = runActionCollecting(actionId, args, collected);
|
|
@@ -1331,6 +1518,18 @@ export function createAxiomRuntime(options) {
|
|
|
1331
1518
|
}
|
|
1332
1519
|
return target;
|
|
1333
1520
|
};
|
|
1521
|
+
/**
|
|
1522
|
+
* `data-control` names the one element a person actually operates.
|
|
1523
|
+
*
|
|
1524
|
+
* A single semantic node can render as more than one element — an input is a label
|
|
1525
|
+
* wrapping a control, and both carry `data-node`, because both are that node. Anything
|
|
1526
|
+
* that wants to type into it, click it or read its value needs the inner one, and
|
|
1527
|
+
* `data-node` cannot say which that is. This can.
|
|
1528
|
+
*/
|
|
1529
|
+
const asControl = (target) => {
|
|
1530
|
+
target.setAttribute('data-control', node.id);
|
|
1531
|
+
return target;
|
|
1532
|
+
};
|
|
1334
1533
|
switch (node.kind) {
|
|
1335
1534
|
case 'view': {
|
|
1336
1535
|
const container = identify(element('div', nodeClasses(node, 'axiom-view')));
|
|
@@ -1429,8 +1628,15 @@ export function createAxiomRuntime(options) {
|
|
|
1429
1628
|
actions.appendChild(submit);
|
|
1430
1629
|
form.appendChild(actions);
|
|
1431
1630
|
}
|
|
1631
|
+
const formInstance = instanceKey(node.id, path);
|
|
1432
1632
|
form.addEventListener('submit', (event) => {
|
|
1433
1633
|
event.preventDefault?.();
|
|
1634
|
+
// A declared control carries the arguments; a generated one has none to carry.
|
|
1635
|
+
const declared = submitInvokers.get(formInstance);
|
|
1636
|
+
if (declared) {
|
|
1637
|
+
declared();
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1434
1640
|
runAction(submitActionId);
|
|
1435
1641
|
});
|
|
1436
1642
|
}
|
|
@@ -1466,7 +1672,10 @@ export function createAxiomRuntime(options) {
|
|
|
1466
1672
|
const describedBy = [];
|
|
1467
1673
|
const control = identify(element(grouped ? 'div' : descriptor.tag, grouped ? 'axiom-radio-group' : 'axiom-control'));
|
|
1468
1674
|
if (descriptor.variant) {
|
|
1469
|
-
control.setAttribute('data-
|
|
1675
|
+
control.setAttribute('data-variant', descriptor.variant);
|
|
1676
|
+
}
|
|
1677
|
+
if (!grouped) {
|
|
1678
|
+
asControl(control);
|
|
1470
1679
|
}
|
|
1471
1680
|
if (!grouped) {
|
|
1472
1681
|
control.setAttribute('id', controlId);
|
|
@@ -1484,7 +1693,9 @@ export function createAxiomRuntime(options) {
|
|
|
1484
1693
|
}
|
|
1485
1694
|
}
|
|
1486
1695
|
if (grouped) {
|
|
1487
|
-
|
|
1696
|
+
// A radio group has no single element to operate, so the group itself is it.
|
|
1697
|
+
wrapper.setAttribute('data-variant', descriptor.variant ?? 'radio-group');
|
|
1698
|
+
asControl(wrapper);
|
|
1488
1699
|
}
|
|
1489
1700
|
if (descriptor.variant === 'switch') {
|
|
1490
1701
|
control.setAttribute('role', 'switch');
|
|
@@ -1628,7 +1839,7 @@ export function createAxiomRuntime(options) {
|
|
|
1628
1839
|
}
|
|
1629
1840
|
case 'button': {
|
|
1630
1841
|
const submits = submitControls.get(node.id);
|
|
1631
|
-
const button = identify(element('button', nodeClasses(node, 'axiom-button', submits ? 'axiom-submit' : '')));
|
|
1842
|
+
const button = asControl(identify(element('button', nodeClasses(node, 'axiom-button', submits ? 'axiom-submit' : ''))));
|
|
1632
1843
|
button.setAttribute('type', 'button');
|
|
1633
1844
|
const label = typeof node.label === 'string' ? node.label : toText(evaluate(node.label, scope));
|
|
1634
1845
|
if (presentation?.icon) {
|
|
@@ -1654,18 +1865,40 @@ export function createAxiomRuntime(options) {
|
|
|
1654
1865
|
if (reporting) {
|
|
1655
1866
|
button.setAttribute('aria-describedby', reporting);
|
|
1656
1867
|
}
|
|
1868
|
+
// A remote action is in flight until the authority answers, and a person watching a
|
|
1869
|
+
// button that does nothing will press it again. `pending` is already a semantic
|
|
1870
|
+
// runtime outcome; this is the whole of its presentation — the control says it is
|
|
1871
|
+
// working, and refuses a second invocation until it is not. No async model, no
|
|
1872
|
+
// spinner vocabulary, nothing an author writes.
|
|
1873
|
+
const pending = actionOutcomes.get(node.actionId)?.outcome === 'pending';
|
|
1874
|
+
if (pending) {
|
|
1875
|
+
button.setAttribute('data-pending', 'true');
|
|
1876
|
+
button.setAttribute('aria-busy', 'true');
|
|
1877
|
+
button.setAttribute('disabled', 'true');
|
|
1878
|
+
}
|
|
1879
|
+
/** What this button does, wherever the interaction came from. */
|
|
1880
|
+
const invoke = () => {
|
|
1881
|
+
if (pending) {
|
|
1882
|
+
// The authority has not answered the last one. Pressing again would be a second
|
|
1883
|
+
// transaction, not a retry of the first.
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
const args = {};
|
|
1887
|
+
for (const [parameterId, argument] of Object.entries(node.arguments ?? {})) {
|
|
1888
|
+
args[parameterId] = evaluate(argument, scope);
|
|
1889
|
+
}
|
|
1890
|
+
runAction(node.actionId, args);
|
|
1891
|
+
};
|
|
1657
1892
|
if (submits) {
|
|
1658
1893
|
// Native form submission runs the action; a click handler here would run it twice.
|
|
1894
|
+
// The form invokes exactly this, so the button's arguments are not lost.
|
|
1659
1895
|
button.setAttribute('type', 'submit');
|
|
1896
|
+
submitInvokers.set(instanceKey(submits.formId, path), invoke);
|
|
1660
1897
|
return button;
|
|
1661
1898
|
}
|
|
1662
1899
|
button.addEventListener('click', (event) => {
|
|
1663
1900
|
event.preventDefault?.();
|
|
1664
|
-
|
|
1665
|
-
for (const [parameterId, argument] of Object.entries(node.arguments ?? {})) {
|
|
1666
|
-
args[parameterId] = evaluate(argument, scope);
|
|
1667
|
-
}
|
|
1668
|
-
runAction(node.actionId, args);
|
|
1901
|
+
invoke();
|
|
1669
1902
|
});
|
|
1670
1903
|
return button;
|
|
1671
1904
|
}
|
|
@@ -1713,6 +1946,7 @@ export function createAxiomRuntime(options) {
|
|
|
1713
1946
|
}
|
|
1714
1947
|
function renderApplication() {
|
|
1715
1948
|
inputElements.clear();
|
|
1949
|
+
submitInvokers.clear();
|
|
1716
1950
|
const scope = rootScope();
|
|
1717
1951
|
if (!activeRoute) {
|
|
1718
1952
|
const missing = element('div', 'axiom-no-route');
|
|
@@ -1747,7 +1981,7 @@ export function createAxiomRuntime(options) {
|
|
|
1747
1981
|
return {
|
|
1748
1982
|
start() {
|
|
1749
1983
|
if (started) {
|
|
1750
|
-
return;
|
|
1984
|
+
return startup;
|
|
1751
1985
|
}
|
|
1752
1986
|
started = true;
|
|
1753
1987
|
host.onPathChange(() => {
|
|
@@ -1758,7 +1992,14 @@ export function createAxiomRuntime(options) {
|
|
|
1758
1992
|
inputErrors.clear();
|
|
1759
1993
|
renderApplication();
|
|
1760
1994
|
});
|
|
1995
|
+
// Local state and a first render happen synchronously, so an application is on screen
|
|
1996
|
+
// before the authority is consulted.
|
|
1761
1997
|
syncRoute();
|
|
1998
|
+
startup = remote?.snapshot ? syncAuthoritative() : Promise.resolve();
|
|
1999
|
+
return startup;
|
|
2000
|
+
},
|
|
2001
|
+
authoritativeStateLoaded() {
|
|
2002
|
+
return authoritativeLoaded;
|
|
1762
2003
|
},
|
|
1763
2004
|
render: renderApplication,
|
|
1764
2005
|
getState(id) {
|
|
@@ -1804,6 +2045,23 @@ export function createAxiomRuntime(options) {
|
|
|
1804
2045
|
registerNativeOperation(implementationId, implementation) {
|
|
1805
2046
|
natives.set(implementationId, implementation);
|
|
1806
2047
|
},
|
|
2048
|
+
async invokeActionAsync(id, args = {}) {
|
|
2049
|
+
const result = runAction(id, args);
|
|
2050
|
+
if (!result.pending) {
|
|
2051
|
+
return result;
|
|
2052
|
+
}
|
|
2053
|
+
return (await pending.get(id)) ?? result;
|
|
2054
|
+
},
|
|
2055
|
+
syncAuthoritativeState() {
|
|
2056
|
+
return syncAuthoritative();
|
|
2057
|
+
},
|
|
2058
|
+
settled() {
|
|
2059
|
+
return allSettled();
|
|
2060
|
+
},
|
|
2061
|
+
evaluate(expression) {
|
|
2062
|
+
const outcome = tryEvaluate(expression, rootScope(), {});
|
|
2063
|
+
return outcome.ok ? { ok: true, value: outcome.value } : { ok: false, diagnostic: outcome.diagnostic };
|
|
2064
|
+
},
|
|
1807
2065
|
};
|
|
1808
2066
|
}
|
|
1809
2067
|
/** Builds a host bound to the browser globals. Used by generated pages. */
|
package/dist/source.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1-alpha.1",
|
|
4
4
|
"description": "Domain-independent runtime that executes an Axiom application graph.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AskTech AS",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@cynodia/axiom-core": "0.
|
|
34
|
+
"@cynodia/axiom-core": "0.6.1-alpha.1"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc -b tsconfig.json tsconfig.test.json",
|