@cynodia/axiom-runtime 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AskTech AS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Axiom Runtime
2
+
3
+ Part of [Axiom](https://github.com/cynodia/axiom), an AI-native semantic web application
4
+ framework.
5
+
6
+ **Status: experimental / alpha.** The API may change between alpha releases.
7
+
8
+ The domain-independent runtime: the state store, expression evaluation, the mutation
9
+ engine, constraint checking, the semantic UI renderer and routing. It takes its whole
10
+ environment through a `HostEnvironment`, so it runs in a browser or headlessly.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @cynodia/axiom-runtime@alpha
16
+ ```
17
+
18
+ Most applications should install the facade package instead, which re-exports this one:
19
+
20
+ ```bash
21
+ npm install @cynodia/axiom@alpha
22
+ ```
23
+
24
+ ## License
25
+
26
+ MIT
27
+
28
+ Copyright (c) 2026 AskTech AS.
package/dist/dom.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The narrow DOM surface the renderer uses. Declaring it structurally keeps the runtime
3
+ * free of DOM library types and lets tests supply an in-memory implementation.
4
+ */
5
+ export interface DomEvent {
6
+ type: string;
7
+ preventDefault?(): void;
8
+ target?: unknown;
9
+ }
10
+ export type DomListener = (event: DomEvent) => void;
11
+ export interface DomElement {
12
+ tagName: string;
13
+ textContent: string | null;
14
+ value?: string;
15
+ checked?: boolean;
16
+ selectionStart?: number | null;
17
+ setAttribute(name: string, value: string): void;
18
+ appendChild(child: DomElement): unknown;
19
+ replaceChildren(...children: DomElement[]): void;
20
+ addEventListener(type: string, listener: DomListener): void;
21
+ focus?(): void;
22
+ }
23
+ export interface DomDocument {
24
+ createElement(tagName: string): DomElement;
25
+ }
26
+ export interface StorageAdapter {
27
+ read(key: string): string | null;
28
+ write(key: string, value: string): void;
29
+ }
30
+ /** Everything the runtime needs from its environment, so nothing is read from globals. */
31
+ export interface HostEnvironment {
32
+ document: DomDocument;
33
+ getPath(): string;
34
+ pushPath(path: string): void;
35
+ onPathChange(listener: () => void): void;
36
+ confirm(message: string): boolean;
37
+ now(): string;
38
+ uuid(): string;
39
+ storage?: StorageAdapter;
40
+ report?(message: string): void;
41
+ }
42
+ //# sourceMappingURL=dom.d.ts.map
package/dist/dom.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ export * from './dom.js';
2
+ export * from './mutation/values.js';
3
+ export * from './mutation/store.js';
4
+ export * from './mutation/transaction.js';
5
+ export * from './mutation/resolve-location.js';
6
+ export * from './mutation/mutation-engine.js';
7
+ export * from './runtime.js';
8
+ export * from './memory-host.js';
9
+ export * from './source.js';
10
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from './dom.js';
2
+ export * from './mutation/values.js';
3
+ export * from './mutation/store.js';
4
+ export * from './mutation/transaction.js';
5
+ export * from './mutation/resolve-location.js';
6
+ export * from './mutation/mutation-engine.js';
7
+ export * from './runtime.js';
8
+ export * from './memory-host.js';
9
+ export * from './source.js';
@@ -0,0 +1,52 @@
1
+ import type { DomDocument, DomElement, DomEvent, DomListener, HostEnvironment, StorageAdapter } from './dom.js';
2
+ /**
3
+ * An in-memory DOM and host. The runtime never touches browser globals directly, so the
4
+ * same renderer that drives a real page can be driven headlessly in tests.
5
+ */
6
+ export declare class MemoryElement implements DomElement {
7
+ readonly tagName: string;
8
+ readonly attributes: Map<string, string>;
9
+ children: MemoryElement[];
10
+ textContent: string | null;
11
+ value?: string;
12
+ checked?: boolean;
13
+ selectionStart: number | null;
14
+ focused: boolean;
15
+ private readonly listeners;
16
+ constructor(tagName: string);
17
+ setAttribute(name: string, value: string): void;
18
+ getAttribute(name: string): string | null;
19
+ appendChild(child: DomElement): unknown;
20
+ replaceChildren(...children: DomElement[]): void;
21
+ addEventListener(type: string, listener: DomListener): void;
22
+ focus(): void;
23
+ dispatch(type: string, event?: Partial<DomEvent>): void;
24
+ hasListener(type: string): boolean;
25
+ }
26
+ export declare class MemoryDocument implements DomDocument {
27
+ createElement(tagName: string): DomElement;
28
+ }
29
+ export interface MemoryHostOptions {
30
+ path?: string;
31
+ confirm?: boolean | (() => boolean);
32
+ storage?: boolean;
33
+ }
34
+ export interface MemoryHost extends HostEnvironment {
35
+ root: MemoryElement;
36
+ path: string;
37
+ reports: string[];
38
+ confirmations: string[];
39
+ storage?: StorageAdapter;
40
+ }
41
+ export declare function createMemoryHost(options?: MemoryHostOptions): MemoryHost;
42
+ /** Depth-first collection of every element matching a predicate. */
43
+ export declare function findAll(root: MemoryElement, predicate: (element: MemoryElement) => boolean): MemoryElement[];
44
+ export declare function findByNodeId(root: MemoryElement, nodeId: string): MemoryElement[];
45
+ export declare function findByTag(root: MemoryElement, tagName: string): MemoryElement[];
46
+ /** Concatenated text of an element and its descendants. */
47
+ export declare function textOf(element: MemoryElement): string;
48
+ export declare function typeInto(element: MemoryElement, value: string): void;
49
+ export declare function toggle(element: MemoryElement, checked: boolean): void;
50
+ export declare function click(element: MemoryElement): void;
51
+ export declare function submit(element: MemoryElement): void;
52
+ //# sourceMappingURL=memory-host.d.ts.map
@@ -0,0 +1,145 @@
1
+ /**
2
+ * An in-memory DOM and host. The runtime never touches browser globals directly, so the
3
+ * same renderer that drives a real page can be driven headlessly in tests.
4
+ */
5
+ export class MemoryElement {
6
+ tagName;
7
+ attributes = new Map();
8
+ children = [];
9
+ textContent = null;
10
+ value;
11
+ checked;
12
+ selectionStart = null;
13
+ focused = false;
14
+ listeners = new Map();
15
+ constructor(tagName) {
16
+ this.tagName = tagName;
17
+ }
18
+ setAttribute(name, value) {
19
+ this.attributes.set(name, value);
20
+ }
21
+ getAttribute(name) {
22
+ return this.attributes.get(name) ?? null;
23
+ }
24
+ appendChild(child) {
25
+ this.children.push(child);
26
+ return child;
27
+ }
28
+ replaceChildren(...children) {
29
+ this.children = children.map((child) => child);
30
+ }
31
+ addEventListener(type, listener) {
32
+ const existing = this.listeners.get(type) ?? [];
33
+ existing.push(listener);
34
+ this.listeners.set(type, existing);
35
+ }
36
+ focus() {
37
+ this.focused = true;
38
+ }
39
+ dispatch(type, event = {}) {
40
+ const payload = { type, target: this, preventDefault: () => undefined, ...event };
41
+ for (const listener of this.listeners.get(type) ?? []) {
42
+ listener(payload);
43
+ }
44
+ }
45
+ hasListener(type) {
46
+ return (this.listeners.get(type) ?? []).length > 0;
47
+ }
48
+ }
49
+ export class MemoryDocument {
50
+ createElement(tagName) {
51
+ return new MemoryElement(tagName);
52
+ }
53
+ }
54
+ export function createMemoryHost(options = {}) {
55
+ const listeners = [];
56
+ const values = new Map();
57
+ let counter = 0;
58
+ const confirmResult = options.confirm ?? true;
59
+ const host = {
60
+ root: new MemoryElement('div'),
61
+ path: options.path ?? '/',
62
+ reports: [],
63
+ confirmations: [],
64
+ document: new MemoryDocument(),
65
+ getPath: () => host.path,
66
+ pushPath: (next) => {
67
+ host.path = next;
68
+ },
69
+ onPathChange: (listener) => {
70
+ listeners.push(listener);
71
+ },
72
+ confirm: (message) => {
73
+ host.confirmations.push(message);
74
+ return typeof confirmResult === 'function' ? confirmResult() : confirmResult;
75
+ },
76
+ now: () => {
77
+ counter += 1;
78
+ return `2026-01-01T00:00:${String(counter).padStart(2, '0')}.000Z`;
79
+ },
80
+ uuid: () => {
81
+ counter += 1;
82
+ return `id-${counter}`;
83
+ },
84
+ report: (message) => {
85
+ host.reports.push(message);
86
+ },
87
+ };
88
+ if (options.storage) {
89
+ host.storage = {
90
+ read: (key) => values.get(key) ?? null,
91
+ write: (key, value) => {
92
+ values.set(key, value);
93
+ },
94
+ };
95
+ }
96
+ return host;
97
+ }
98
+ /** Depth-first collection of every element matching a predicate. */
99
+ export function findAll(root, predicate) {
100
+ const found = [];
101
+ const visit = (element) => {
102
+ if (predicate(element)) {
103
+ found.push(element);
104
+ }
105
+ for (const child of element.children) {
106
+ visit(child);
107
+ }
108
+ };
109
+ visit(root);
110
+ return found;
111
+ }
112
+ export function findByNodeId(root, nodeId) {
113
+ return findAll(root, (element) => element.getAttribute('data-node') === nodeId);
114
+ }
115
+ export function findByTag(root, tagName) {
116
+ return findAll(root, (element) => element.tagName === tagName);
117
+ }
118
+ /** Concatenated text of an element and its descendants. */
119
+ export function textOf(element) {
120
+ const parts = [];
121
+ const visit = (current) => {
122
+ if (current.textContent) {
123
+ parts.push(current.textContent);
124
+ }
125
+ for (const child of current.children) {
126
+ visit(child);
127
+ }
128
+ };
129
+ visit(element);
130
+ return parts.join(' ');
131
+ }
132
+ export function typeInto(element, value) {
133
+ element.value = value;
134
+ element.dispatch('input');
135
+ }
136
+ export function toggle(element, checked) {
137
+ element.checked = checked;
138
+ element.dispatch('change');
139
+ }
140
+ export function click(element) {
141
+ element.dispatch('click');
142
+ }
143
+ export function submit(element) {
144
+ element.dispatch('submit');
145
+ }
@@ -0,0 +1,44 @@
1
+ import type { MutationOperation, NodeId } from '@cynodia/axiom-core';
2
+ import type { LocationRuntime, ResolvedPath } from './resolve-location.js';
3
+ /** Where a mutation came from, so every state change stays attributable. */
4
+ export interface MutationContext {
5
+ source: 'action' | 'ui' | 'system' | 'native';
6
+ sourceNodeId?: NodeId;
7
+ transactionId?: string;
8
+ }
9
+ export interface MutationResult {
10
+ affectedStates: NodeId[];
11
+ affectedLocations: ResolvedPath[];
12
+ }
13
+ export interface MutationLogEntry {
14
+ transactionId?: string;
15
+ source: MutationContext['source'];
16
+ sourceNodeId?: NodeId;
17
+ operation: MutationOperation['kind'];
18
+ path: ResolvedPath;
19
+ description: string;
20
+ oldValue?: unknown;
21
+ newValue?: unknown;
22
+ /** Set when the surrounding transaction settles. */
23
+ outcome?: 'committed' | 'rolled-back';
24
+ }
25
+ export interface MutationEngineOptions {
26
+ runtime: LocationRuntime;
27
+ /** Records previous and next values in the log. */
28
+ recordValues?: boolean;
29
+ onLog?(entry: MutationLogEntry): void;
30
+ }
31
+ export interface MutationEngine {
32
+ apply(operation: MutationOperation, scope: unknown, context: MutationContext): MutationResult;
33
+ /** Applies a value directly to a location, used by inputs and native results. */
34
+ set(location: MutationOperation extends {
35
+ target: infer L;
36
+ } ? L : never, value: unknown, scope: unknown, context: MutationContext): MutationResult;
37
+ }
38
+ /**
39
+ * The single place Axiom-managed state is written. Everything else — actions, inputs,
40
+ * native results — goes through here, which is what makes every mutation observable and
41
+ * attributable.
42
+ */
43
+ export declare function createMutationEngine(options: MutationEngineOptions): MutationEngine;
44
+ //# sourceMappingURL=mutation-engine.d.ts.map
@@ -0,0 +1,92 @@
1
+ import { describePath, resolveLocation } from './resolve-location.js';
2
+ import { cloneValue, isRecord, valuesEqual } from './values.js';
3
+ /**
4
+ * The single place Axiom-managed state is written. Everything else — actions, inputs,
5
+ * native results — goes through here, which is what makes every mutation observable and
6
+ * attributable.
7
+ */
8
+ export function createMutationEngine(options) {
9
+ const { runtime } = options;
10
+ const recordValues = options.recordValues !== false;
11
+ function log(entry) {
12
+ options.onLog?.(entry);
13
+ }
14
+ function applyValue(location, value, scope, context, kind) {
15
+ const resolved = resolveLocation(location, scope, runtime);
16
+ const previous = recordValues ? cloneValue(resolved.read()) : undefined;
17
+ resolved.write(value);
18
+ log({
19
+ ...context,
20
+ operation: kind,
21
+ path: resolved.path,
22
+ description: describePath(resolved.path),
23
+ ...(recordValues ? { oldValue: previous, newValue: cloneValue(value) } : {}),
24
+ });
25
+ return { affectedStates: [resolved.rootStateId], affectedLocations: [resolved.path] };
26
+ }
27
+ return {
28
+ set(location, value, scope, context) {
29
+ return applyValue(location, value, scope, context, 'set');
30
+ },
31
+ apply(operation, scope, context) {
32
+ switch (operation.kind) {
33
+ case 'set':
34
+ return applyValue(operation.target, runtime.evaluate(operation.value, scope), scope, context, 'set');
35
+ case 'insert': {
36
+ const resolved = resolveLocation(operation.target, scope, runtime);
37
+ const current = resolved.read();
38
+ const items = Array.isArray(current) ? current : [];
39
+ const value = cloneValue(runtime.evaluate(operation.value, scope));
40
+ const next = operation.position === 'start' ? [value, ...items] : [...items, value];
41
+ resolved.write(next);
42
+ log({
43
+ ...context,
44
+ operation: 'insert',
45
+ path: resolved.path,
46
+ description: describePath(resolved.path),
47
+ ...(recordValues ? { newValue: value } : {}),
48
+ });
49
+ return { affectedStates: [resolved.rootStateId], affectedLocations: [resolved.path] };
50
+ }
51
+ case 'remove': {
52
+ const collection = resolveLocation(operation.target.collection, scope, runtime);
53
+ const current = collection.read();
54
+ const items = Array.isArray(current) ? current : [];
55
+ const selector = operation.target.selector;
56
+ const position = selector.kind === 'identity'
57
+ ? items.findIndex((item) => isRecord(item) &&
58
+ valuesEqual(item[selector.fieldId], runtime.evaluate(selector.value, scope)))
59
+ : Number(runtime.evaluate(selector.index, scope));
60
+ if (position < 0 || position >= items.length) {
61
+ return { affectedStates: [], affectedLocations: [] };
62
+ }
63
+ const removed = recordValues ? cloneValue(items[position]) : undefined;
64
+ collection.write(items.filter((_, index) => index !== position));
65
+ const path = {
66
+ rootStateId: collection.path.rootStateId,
67
+ segments: [
68
+ ...collection.path.segments,
69
+ selector.kind === 'identity'
70
+ ? {
71
+ kind: 'collection-item',
72
+ fieldId: selector.fieldId,
73
+ identity: runtime.evaluate(selector.value, scope),
74
+ }
75
+ : { kind: 'collection-item', index: position },
76
+ ],
77
+ };
78
+ log({
79
+ ...context,
80
+ operation: 'remove',
81
+ path,
82
+ description: describePath(path),
83
+ ...(recordValues ? { oldValue: removed } : {}),
84
+ });
85
+ return { affectedStates: [collection.path.rootStateId], affectedLocations: [path] };
86
+ }
87
+ default:
88
+ throw new Error(`Unknown mutation kind "${operation.kind}"`);
89
+ }
90
+ },
91
+ };
92
+ }
@@ -0,0 +1,37 @@
1
+ import type { Expression, FieldId, Location, NodeId } from '@cynodia/axiom-core';
2
+ export interface ResolvedPathSegment {
3
+ kind: 'field' | 'collection-item';
4
+ fieldId?: FieldId;
5
+ /** The resolved identity value, for an identity selector. */
6
+ identity?: unknown;
7
+ index?: number;
8
+ }
9
+ /** Semantic provenance of a resolved location: which state, then how to get inside it. */
10
+ export interface ResolvedPath {
11
+ rootStateId: NodeId;
12
+ segments: ResolvedPathSegment[];
13
+ }
14
+ export interface ResolvedLocation<T = unknown> {
15
+ read(): T;
16
+ write(value: T): void;
17
+ rootStateId: NodeId;
18
+ path: ResolvedPath;
19
+ }
20
+ export declare class LocationResolutionError extends Error {
21
+ readonly path: ResolvedPath;
22
+ constructor(message: string, path: ResolvedPath);
23
+ }
24
+ export interface LocationRuntime {
25
+ readState(stateId: NodeId): unknown;
26
+ writeState(stateId: NodeId, value: unknown): void;
27
+ evaluate(expression: Expression, scope: unknown): unknown;
28
+ }
29
+ /** Renders a resolved path the way the inspector and the mutation log show it. */
30
+ export declare function describePath(path: ResolvedPath): string;
31
+ /**
32
+ * Turns a Location into a readable and writable address. Writes rebuild the path from
33
+ * the root state, so a mutation never depends on the identity of an object that some
34
+ * expression happened to return.
35
+ */
36
+ export declare function resolveLocation<T = unknown>(location: Location, scope: unknown, runtime: LocationRuntime): ResolvedLocation<T>;
37
+ //# sourceMappingURL=resolve-location.d.ts.map
@@ -0,0 +1,108 @@
1
+ import { isRecord, valuesEqual } from './values.js';
2
+ export class LocationResolutionError extends Error {
3
+ path;
4
+ constructor(message, path) {
5
+ super(message);
6
+ this.name = 'LocationResolutionError';
7
+ this.path = path;
8
+ }
9
+ }
10
+ /** Renders a resolved path the way the inspector and the mutation log show it. */
11
+ export function describePath(path) {
12
+ const segments = path.segments.map((segment) => {
13
+ if (segment.kind === 'field') {
14
+ return String(segment.fieldId);
15
+ }
16
+ return segment.index === undefined ? `[${String(segment.identity)}]` : `[#${segment.index}]`;
17
+ });
18
+ return [path.rootStateId, ...segments].join(' → ');
19
+ }
20
+ function toPath(location, scope, runtime) {
21
+ switch (location.kind) {
22
+ case 'state':
23
+ return { rootStateId: location.stateId, segments: [] };
24
+ case 'field': {
25
+ const parent = toPath(location.target, scope, runtime);
26
+ return {
27
+ rootStateId: parent.rootStateId,
28
+ segments: [...parent.segments, { kind: 'field', fieldId: location.fieldId }],
29
+ };
30
+ }
31
+ case 'collection-item': {
32
+ const parent = toPath(location.collection, scope, runtime);
33
+ const segment = location.selector.kind === 'identity'
34
+ ? {
35
+ kind: 'collection-item',
36
+ fieldId: location.selector.fieldId,
37
+ identity: runtime.evaluate(location.selector.value, scope),
38
+ }
39
+ : {
40
+ kind: 'collection-item',
41
+ index: Number(runtime.evaluate(location.selector.index, scope)),
42
+ };
43
+ return { rootStateId: parent.rootStateId, segments: [...parent.segments, segment] };
44
+ }
45
+ default:
46
+ throw new Error(`Unknown location kind "${location.kind}"`);
47
+ }
48
+ }
49
+ function indexOfItem(collection, segment) {
50
+ if (segment.index !== undefined) {
51
+ return segment.index >= 0 && segment.index < collection.length ? segment.index : -1;
52
+ }
53
+ return collection.findIndex((item) => isRecord(item) && valuesEqual(item[String(segment.fieldId)], segment.identity));
54
+ }
55
+ function readAt(value, segments, index) {
56
+ if (index >= segments.length) {
57
+ return value;
58
+ }
59
+ const segment = segments[index];
60
+ if (segment.kind === 'field') {
61
+ const source = isRecord(value) ? value[String(segment.fieldId)] : undefined;
62
+ return readAt(source === undefined ? null : source, segments, index + 1);
63
+ }
64
+ if (!Array.isArray(value)) {
65
+ return null;
66
+ }
67
+ const position = indexOfItem(value, segment);
68
+ return position < 0 ? null : readAt(value[position], segments, index + 1);
69
+ }
70
+ /** Rebuilds the value with one position replaced. Nothing existing is mutated. */
71
+ function writeAt(value, segments, index, next, path) {
72
+ if (index >= segments.length) {
73
+ return next;
74
+ }
75
+ const segment = segments[index];
76
+ if (segment.kind === 'field') {
77
+ const base = isRecord(value) ? value : {};
78
+ const key = String(segment.fieldId);
79
+ return { ...base, [key]: writeAt(base[key], segments, index + 1, next, path) };
80
+ }
81
+ if (!Array.isArray(value)) {
82
+ throw new LocationResolutionError(`${describePath(path)} does not address a collection`, path);
83
+ }
84
+ const position = indexOfItem(value, segment);
85
+ if (position < 0) {
86
+ throw new LocationResolutionError(`No item matches ${describePath(path)}`, path);
87
+ }
88
+ const copy = value.slice();
89
+ copy[position] = writeAt(value[position], segments, index + 1, next, path);
90
+ return copy;
91
+ }
92
+ /**
93
+ * Turns a Location into a readable and writable address. Writes rebuild the path from
94
+ * the root state, so a mutation never depends on the identity of an object that some
95
+ * expression happened to return.
96
+ */
97
+ export function resolveLocation(location, scope, runtime) {
98
+ const path = toPath(location, scope, runtime);
99
+ return {
100
+ rootStateId: path.rootStateId,
101
+ path,
102
+ read: () => readAt(runtime.readState(path.rootStateId), path.segments, 0),
103
+ write: (value) => {
104
+ const root = runtime.readState(path.rootStateId);
105
+ runtime.writeState(path.rootStateId, writeAt(root, path.segments, 0, value, path));
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The state store. It hands out only frozen values and is the sole owner of the map, so
3
+ * no consumer can change application state by holding on to something it read.
4
+ */
5
+ export interface StateStore {
6
+ has(stateId: string): boolean;
7
+ read(stateId: string): unknown;
8
+ write(stateId: string, value: unknown): void;
9
+ keys(): string[];
10
+ capture(): unknown;
11
+ restore(snapshot: unknown): void;
12
+ }
13
+ export declare function createStateStore(): StateStore;
14
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { deepFreeze } from './values.js';
2
+ export function createStateStore() {
3
+ const values = new Map();
4
+ return {
5
+ has: (stateId) => values.has(stateId),
6
+ read: (stateId) => values.get(stateId),
7
+ write: (stateId, value) => {
8
+ values.set(stateId, deepFreeze(value));
9
+ },
10
+ keys: () => [...values.keys()],
11
+ capture: () => new Map(values),
12
+ restore: (snapshot) => {
13
+ values.clear();
14
+ for (const [key, value] of snapshot) {
15
+ values.set(key, value);
16
+ }
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Runtime transactions. The semantic API is independent of the snapshot strategy, so a
3
+ * coarse whole-store snapshot can be replaced later without touching graph semantics.
4
+ */
5
+ export interface StoreSnapshot {
6
+ capture(): unknown;
7
+ restore(snapshot: unknown): void;
8
+ }
9
+ export interface RuntimeTransaction {
10
+ id: string;
11
+ /** True for the outermost transaction; nested ones join their parent. */
12
+ isRoot: boolean;
13
+ commit(): void;
14
+ rollback(): void;
15
+ }
16
+ export interface TransactionManager {
17
+ begin(): RuntimeTransaction;
18
+ currentId(): string | undefined;
19
+ }
20
+ export declare function createTransactionManager(store: StoreSnapshot, nextId: () => string): TransactionManager;
21
+ //# sourceMappingURL=transaction.d.ts.map