@intent-framework/core 0.1.0-alpha.0
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 +21 -0
- package/dist/act.d.ts +49 -0
- package/dist/ask.d.ts +39 -0
- package/dist/flow.d.ts +23 -0
- package/dist/graph.d.ts +52 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +854 -0
- package/dist/registry.d.ts +25 -0
- package/dist/resource.d.ts +59 -0
- package/dist/runtime.d.ts +28 -0
- package/dist/screen.d.ts +44 -0
- package/dist/signal.d.ts +21 -0
- package/dist/state.d.ts +28 -0
- package/dist/surface.d.ts +15 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mahyar
|
|
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/dist/act.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Condition } from "./signal.js";
|
|
2
|
+
export type NavigationService = (name: string, params?: Record<string, string>) => void;
|
|
3
|
+
export type DefaultScreenServices = {
|
|
4
|
+
navigate?: NavigationService;
|
|
5
|
+
};
|
|
6
|
+
export type ActionExecutionContext<TServices extends object = DefaultScreenServices> = Readonly<TServices>;
|
|
7
|
+
export type FeedbackConfig = {
|
|
8
|
+
pending?: string;
|
|
9
|
+
success?: string;
|
|
10
|
+
failure?: string | ((error: Error) => string);
|
|
11
|
+
};
|
|
12
|
+
export type ActStatus = "idle" | "pending" | "success" | "failure";
|
|
13
|
+
export type ActCondition = {
|
|
14
|
+
check: () => boolean;
|
|
15
|
+
message?: string;
|
|
16
|
+
source?: Condition;
|
|
17
|
+
};
|
|
18
|
+
export declare const kResourceMap: unique symbol;
|
|
19
|
+
export type ActNode<TServices extends object = DefaultScreenServices> = {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
primary: boolean;
|
|
23
|
+
conditions: ActCondition[];
|
|
24
|
+
handler: ((context: ActionExecutionContext<TServices>) => Promise<void> | void) | null;
|
|
25
|
+
feedback?: FeedbackConfig;
|
|
26
|
+
invalidatedResourceIds: string[];
|
|
27
|
+
status: ActStatus;
|
|
28
|
+
statusMessage: string | null;
|
|
29
|
+
enabled: Condition;
|
|
30
|
+
blockedReasons: string[];
|
|
31
|
+
execute: (context?: ActionExecutionContext<TServices>) => Promise<void>;
|
|
32
|
+
onStatusChange: (fn: () => void) => () => void;
|
|
33
|
+
};
|
|
34
|
+
export declare function createActNode<TServices extends object = DefaultScreenServices>(id: string, label: string, conditions: ActCondition[], handler: ((context: ActionExecutionContext<TServices>) => Promise<void> | void) | null, feedback: FeedbackConfig | undefined, primary: boolean, invalidatedResourceIds?: string[]): ActNode<TServices>;
|
|
35
|
+
export declare class ActBuilder<TServices extends object = DefaultScreenServices> {
|
|
36
|
+
private node;
|
|
37
|
+
constructor(label: string);
|
|
38
|
+
get enabled(): Condition;
|
|
39
|
+
primary(): this;
|
|
40
|
+
when(condition: Condition | boolean | (() => boolean) | {
|
|
41
|
+
valid?: Condition | boolean;
|
|
42
|
+
}, message?: string): this;
|
|
43
|
+
does(fn: (context: ActionExecutionContext<TServices>) => Promise<void> | void): this;
|
|
44
|
+
invalidates(...resources: {
|
|
45
|
+
id: string;
|
|
46
|
+
}[]): this;
|
|
47
|
+
feedback(fb: FeedbackConfig): this;
|
|
48
|
+
toNode(): ActNode<TServices>;
|
|
49
|
+
}
|
package/dist/ask.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Condition } from "./signal.js";
|
|
2
|
+
export type AskKind = "text" | "contact" | "secret" | "choice";
|
|
3
|
+
export type AskNode<T> = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
kind: AskKind;
|
|
7
|
+
contactKind?: string;
|
|
8
|
+
required: boolean;
|
|
9
|
+
requiredMessage?: string;
|
|
10
|
+
isPrivate: boolean;
|
|
11
|
+
hintText?: string;
|
|
12
|
+
validators: Array<(value: T) => string | boolean>;
|
|
13
|
+
state: {
|
|
14
|
+
value: T;
|
|
15
|
+
};
|
|
16
|
+
valid: Condition;
|
|
17
|
+
error: string | null;
|
|
18
|
+
subscribe: (fn: () => void) => () => void;
|
|
19
|
+
};
|
|
20
|
+
export type AnyAskNode = AskNode<unknown>;
|
|
21
|
+
export declare function createAskNode<T>(id: string, label: string, stateRef: {
|
|
22
|
+
value: T;
|
|
23
|
+
}, notifySub?: (fn: () => void) => () => void): AskNode<T>;
|
|
24
|
+
export declare class AskBuilder<T> {
|
|
25
|
+
private node;
|
|
26
|
+
constructor(label: string, stateRef: {
|
|
27
|
+
value: T;
|
|
28
|
+
onChange?: (fn: (value: T) => void) => () => void;
|
|
29
|
+
});
|
|
30
|
+
get valid(): Condition;
|
|
31
|
+
asContact(kind: string): this;
|
|
32
|
+
asSecret(): this;
|
|
33
|
+
asChoice(): this;
|
|
34
|
+
required(message?: string): this;
|
|
35
|
+
validate(fn: (value: T) => string | boolean): this;
|
|
36
|
+
private(): this;
|
|
37
|
+
hint(text: string): this;
|
|
38
|
+
toNode(): AskNode<T>;
|
|
39
|
+
}
|
package/dist/flow.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AnyAskNode } from "./ask.js";
|
|
2
|
+
import type { ActNode } from "./act.js";
|
|
3
|
+
import { AskBuilder } from "./ask.js";
|
|
4
|
+
import { ActBuilder } from "./act.js";
|
|
5
|
+
export type FlowStep = {
|
|
6
|
+
type: "ask";
|
|
7
|
+
node: AnyAskNode;
|
|
8
|
+
} | {
|
|
9
|
+
type: "act";
|
|
10
|
+
node: ActNode;
|
|
11
|
+
};
|
|
12
|
+
export type FlowNode = {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
steps: FlowStep[];
|
|
16
|
+
};
|
|
17
|
+
export declare class FlowBuilder {
|
|
18
|
+
private node;
|
|
19
|
+
constructor(name: string);
|
|
20
|
+
startsWith(ask: AnyAskNode | AskBuilder<any>): this;
|
|
21
|
+
then(item: AnyAskNode | ActNode | AskBuilder<any> | ActBuilder<any>): this;
|
|
22
|
+
toNode(): FlowNode;
|
|
23
|
+
}
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ScreenDefinition } from "./screen.js";
|
|
2
|
+
import type { DefaultScreenServices } from "./act.js";
|
|
3
|
+
import type { AnyResourceNode } from "./resource.js";
|
|
4
|
+
export type DiagnosticSeverity = "info" | "warning" | "error";
|
|
5
|
+
export type GraphDiagnostic = {
|
|
6
|
+
severity: DiagnosticSeverity;
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
nodeId?: string;
|
|
10
|
+
};
|
|
11
|
+
export type InspectedScreen = {
|
|
12
|
+
name: string;
|
|
13
|
+
asks: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
kind: string;
|
|
17
|
+
required: boolean;
|
|
18
|
+
isPrivate: boolean;
|
|
19
|
+
valid: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
}>;
|
|
22
|
+
acts: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
primary: boolean;
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
blockedReasons: string[];
|
|
28
|
+
status: string;
|
|
29
|
+
statusMessage: string | null;
|
|
30
|
+
invalidates: string[];
|
|
31
|
+
}>;
|
|
32
|
+
flows: Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
stepCount: number;
|
|
36
|
+
}>;
|
|
37
|
+
surfaces: Array<{
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
itemCount: number;
|
|
41
|
+
}>;
|
|
42
|
+
resources: Array<{
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
status: string;
|
|
46
|
+
hasValue: boolean;
|
|
47
|
+
stale: boolean;
|
|
48
|
+
error: string | undefined;
|
|
49
|
+
}>;
|
|
50
|
+
diagnostics: GraphDiagnostic[];
|
|
51
|
+
};
|
|
52
|
+
export declare function inspectScreen<TServices extends object = DefaultScreenServices>(screenDef: ScreenDefinition<TServices>, runtimeResources?: AnyResourceNode[]): InspectedScreen;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { screen } from "./screen.js";
|
|
2
|
+
export type { ScreenDefinition, ScreenBuilder } from "./screen.js";
|
|
3
|
+
export { inspectScreen } from "./graph.js";
|
|
4
|
+
export type { InspectedScreen, GraphDiagnostic, DiagnosticSeverity } from "./graph.js";
|
|
5
|
+
export type { TextState, BooleanState, ChoiceState } from "./state.js";
|
|
6
|
+
export type { AskNode, AnyAskNode, AskKind, AskBuilder } from "./ask.js";
|
|
7
|
+
export type { ActNode, ActCondition, ActStatus, FeedbackConfig, ActBuilder, NavigationService, ActionExecutionContext, DefaultScreenServices } from "./act.js";
|
|
8
|
+
export type { FlowNode, FlowStep, FlowBuilder } from "./flow.js";
|
|
9
|
+
export type { SurfaceNode, SurfaceBuilder } from "./surface.js";
|
|
10
|
+
export type { ResourceNode, ResourceConfig, ResourceLoadContext, ResourceStatus, AnyResourceNode } from "./resource.js";
|
|
11
|
+
export { ResourceRef, createResourceNode } from "./resource.js";
|
|
12
|
+
export { createScreenRuntime } from "./runtime.js";
|
|
13
|
+
export type { ScreenRuntime } from "./runtime.js";
|
|
14
|
+
export { resetAskRegistry, resetActRegistry, resetFlowRegistry, resetSurfaceRegistry, resetResourceRegistry, getAsks, getActs, getFlows, getSurfaces, getResources, } from "./registry.js";
|
|
15
|
+
export type { Condition } from "./signal.js";
|
|
16
|
+
export { isCondition } from "./signal.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
//#region src/signal.ts
|
|
2
|
+
function isCondition(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && "current" in value && "subscribe" in value;
|
|
4
|
+
}
|
|
5
|
+
function createCondition(compute, subscribeToChanges, reason) {
|
|
6
|
+
return {
|
|
7
|
+
get current() {
|
|
8
|
+
return compute();
|
|
9
|
+
},
|
|
10
|
+
reason,
|
|
11
|
+
subscribe(fn) {
|
|
12
|
+
return subscribeToChanges(() => fn());
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function signal(initial) {
|
|
17
|
+
let value = initial;
|
|
18
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
19
|
+
return {
|
|
20
|
+
get() {
|
|
21
|
+
return value;
|
|
22
|
+
},
|
|
23
|
+
set(newValue) {
|
|
24
|
+
if (newValue !== value) {
|
|
25
|
+
value = newValue;
|
|
26
|
+
for (const fn of listeners) fn(value);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
subscribe(fn) {
|
|
30
|
+
listeners.add(fn);
|
|
31
|
+
return () => {
|
|
32
|
+
listeners.delete(fn);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/resource.ts
|
|
40
|
+
function createResourceNode(id, name, loader, autoLoad = true) {
|
|
41
|
+
const statusSignal = signal(0);
|
|
42
|
+
const staleSignal = signal(0);
|
|
43
|
+
let currentStatus = "idle";
|
|
44
|
+
let currentValue = void 0;
|
|
45
|
+
let currentError = void 0;
|
|
46
|
+
let currentStale = false;
|
|
47
|
+
let lastContext = void 0;
|
|
48
|
+
const notify = () => statusSignal.set(statusSignal.get() + 1);
|
|
49
|
+
const staleNotify = () => staleSignal.set(staleSignal.get() + 1);
|
|
50
|
+
let _ready;
|
|
51
|
+
let _pending;
|
|
52
|
+
let _failed;
|
|
53
|
+
let _stale;
|
|
54
|
+
function getReady() {
|
|
55
|
+
if (!_ready) _ready = createCondition(() => currentStatus === "ready", (notify$1) => statusSignal.subscribe(() => notify$1()));
|
|
56
|
+
return _ready;
|
|
57
|
+
}
|
|
58
|
+
function getPending() {
|
|
59
|
+
if (!_pending) _pending = createCondition(() => currentStatus === "pending", (notify$1) => statusSignal.subscribe(() => notify$1()));
|
|
60
|
+
return _pending;
|
|
61
|
+
}
|
|
62
|
+
function getFailed() {
|
|
63
|
+
if (!_failed) _failed = createCondition(() => currentStatus === "failed", (notify$1) => statusSignal.subscribe(() => notify$1()));
|
|
64
|
+
return _failed;
|
|
65
|
+
}
|
|
66
|
+
function getStale() {
|
|
67
|
+
if (!_stale) _stale = createCondition(() => currentStale, (notify$1) => staleSignal.subscribe(() => notify$1()));
|
|
68
|
+
return _stale;
|
|
69
|
+
}
|
|
70
|
+
async function executeLoad(context) {
|
|
71
|
+
currentStale = false;
|
|
72
|
+
staleNotify();
|
|
73
|
+
currentStatus = "pending";
|
|
74
|
+
currentValue = void 0;
|
|
75
|
+
currentError = void 0;
|
|
76
|
+
notify();
|
|
77
|
+
if (context !== void 0) lastContext = context;
|
|
78
|
+
const loadContext = context ?? lastContext ?? {};
|
|
79
|
+
try {
|
|
80
|
+
const result = await Promise.resolve(loader(loadContext));
|
|
81
|
+
currentValue = result;
|
|
82
|
+
currentStatus = "ready";
|
|
83
|
+
currentStale = false;
|
|
84
|
+
notify();
|
|
85
|
+
staleNotify();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
currentError = e;
|
|
88
|
+
currentStatus = "failed";
|
|
89
|
+
currentStale = false;
|
|
90
|
+
notify();
|
|
91
|
+
staleNotify();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function invalidate() {
|
|
95
|
+
if (!currentStale) {
|
|
96
|
+
currentStale = true;
|
|
97
|
+
staleNotify();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const node = {
|
|
101
|
+
id,
|
|
102
|
+
name,
|
|
103
|
+
autoLoad,
|
|
104
|
+
get status() {
|
|
105
|
+
return currentStatus;
|
|
106
|
+
},
|
|
107
|
+
get value() {
|
|
108
|
+
return currentValue;
|
|
109
|
+
},
|
|
110
|
+
get error() {
|
|
111
|
+
return currentError;
|
|
112
|
+
},
|
|
113
|
+
get ready() {
|
|
114
|
+
return getReady();
|
|
115
|
+
},
|
|
116
|
+
get pending() {
|
|
117
|
+
return getPending();
|
|
118
|
+
},
|
|
119
|
+
get failed() {
|
|
120
|
+
return getFailed();
|
|
121
|
+
},
|
|
122
|
+
get stale() {
|
|
123
|
+
return getStale();
|
|
124
|
+
},
|
|
125
|
+
load: executeLoad,
|
|
126
|
+
reload: executeLoad,
|
|
127
|
+
invalidate,
|
|
128
|
+
subscribe(fn) {
|
|
129
|
+
return statusSignal.subscribe(fn);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return node;
|
|
133
|
+
}
|
|
134
|
+
var ResourceRef = class {
|
|
135
|
+
id;
|
|
136
|
+
name;
|
|
137
|
+
autoLoad;
|
|
138
|
+
loader;
|
|
139
|
+
_connected = null;
|
|
140
|
+
_connSignal = signal(0);
|
|
141
|
+
_nodeSub = null;
|
|
142
|
+
_readyCache;
|
|
143
|
+
_pendingCache;
|
|
144
|
+
_failedCache;
|
|
145
|
+
_staleCache;
|
|
146
|
+
constructor(id, name, loader, autoLoad) {
|
|
147
|
+
this.id = id;
|
|
148
|
+
this.name = name;
|
|
149
|
+
this.loader = loader;
|
|
150
|
+
this.autoLoad = autoLoad;
|
|
151
|
+
}
|
|
152
|
+
get status() {
|
|
153
|
+
return this._connected?.status ?? "idle";
|
|
154
|
+
}
|
|
155
|
+
get value() {
|
|
156
|
+
return this._connected?.value;
|
|
157
|
+
}
|
|
158
|
+
get error() {
|
|
159
|
+
return this._connected?.error;
|
|
160
|
+
}
|
|
161
|
+
get ready() {
|
|
162
|
+
if (!this._readyCache) this._readyCache = createCondition(() => this._connected?.status === "ready", (notify) => {
|
|
163
|
+
const unsubs = [];
|
|
164
|
+
unsubs.push(this._connSignal.subscribe(notify));
|
|
165
|
+
const node = this._connected;
|
|
166
|
+
if (node) unsubs.push(node.ready.subscribe(notify));
|
|
167
|
+
return () => {
|
|
168
|
+
for (const u of unsubs) u();
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
return this._readyCache;
|
|
172
|
+
}
|
|
173
|
+
get pending() {
|
|
174
|
+
if (!this._pendingCache) this._pendingCache = createCondition(() => this._connected?.status === "pending", (notify) => {
|
|
175
|
+
const unsubs = [];
|
|
176
|
+
unsubs.push(this._connSignal.subscribe(notify));
|
|
177
|
+
const node = this._connected;
|
|
178
|
+
if (node) unsubs.push(node.pending.subscribe(notify));
|
|
179
|
+
return () => {
|
|
180
|
+
for (const u of unsubs) u();
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
return this._pendingCache;
|
|
184
|
+
}
|
|
185
|
+
get failed() {
|
|
186
|
+
if (!this._failedCache) this._failedCache = createCondition(() => this._connected?.status === "failed", (notify) => {
|
|
187
|
+
const unsubs = [];
|
|
188
|
+
unsubs.push(this._connSignal.subscribe(notify));
|
|
189
|
+
const node = this._connected;
|
|
190
|
+
if (node) unsubs.push(node.failed.subscribe(notify));
|
|
191
|
+
return () => {
|
|
192
|
+
for (const u of unsubs) u();
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
return this._failedCache;
|
|
196
|
+
}
|
|
197
|
+
get stale() {
|
|
198
|
+
if (!this._staleCache) this._staleCache = createCondition(() => this._connected?.stale.current ?? false, (notify) => {
|
|
199
|
+
const unsubs = [];
|
|
200
|
+
unsubs.push(this._connSignal.subscribe(notify));
|
|
201
|
+
const node = this._connected;
|
|
202
|
+
if (node) unsubs.push(node.stale.subscribe(notify));
|
|
203
|
+
return () => {
|
|
204
|
+
for (const u of unsubs) u();
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
return this._staleCache;
|
|
208
|
+
}
|
|
209
|
+
load(context) {
|
|
210
|
+
return this._connected?.load(context) ?? Promise.resolve();
|
|
211
|
+
}
|
|
212
|
+
reload(context) {
|
|
213
|
+
return this._connected?.reload(context) ?? Promise.resolve();
|
|
214
|
+
}
|
|
215
|
+
invalidate() {
|
|
216
|
+
this._connected?.invalidate();
|
|
217
|
+
}
|
|
218
|
+
subscribe(fn) {
|
|
219
|
+
return this._connSignal.subscribe(fn);
|
|
220
|
+
}
|
|
221
|
+
_connect(node) {
|
|
222
|
+
this._disconnect();
|
|
223
|
+
this._connected = node;
|
|
224
|
+
this._nodeSub = node.subscribe(() => {
|
|
225
|
+
this._connSignal.set(this._connSignal.get() + 1);
|
|
226
|
+
});
|
|
227
|
+
this._connSignal.set(this._connSignal.get() + 1);
|
|
228
|
+
}
|
|
229
|
+
_disconnect(expectedNode) {
|
|
230
|
+
if (expectedNode && this._connected !== expectedNode) return;
|
|
231
|
+
this._nodeSub?.();
|
|
232
|
+
this._nodeSub = null;
|
|
233
|
+
this._connected = null;
|
|
234
|
+
this._connSignal.set(this._connSignal.get() + 1);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/state.ts
|
|
240
|
+
function createTextState(_name, initial = "") {
|
|
241
|
+
const sig = signal(initial);
|
|
242
|
+
const validCondition = createCondition(() => sig.get().length > 0, (notify) => sig.subscribe(() => notify()));
|
|
243
|
+
return {
|
|
244
|
+
get value() {
|
|
245
|
+
return sig.get();
|
|
246
|
+
},
|
|
247
|
+
get valid() {
|
|
248
|
+
return validCondition;
|
|
249
|
+
},
|
|
250
|
+
set(value) {
|
|
251
|
+
sig.set(value);
|
|
252
|
+
},
|
|
253
|
+
onChange(fn) {
|
|
254
|
+
return sig.subscribe(fn);
|
|
255
|
+
},
|
|
256
|
+
clear() {
|
|
257
|
+
sig.set("");
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function createBooleanState(_name, initial = false) {
|
|
262
|
+
const sig = signal(initial);
|
|
263
|
+
const validCondition = createCondition(() => true, (notify) => sig.subscribe(() => notify()));
|
|
264
|
+
return {
|
|
265
|
+
get value() {
|
|
266
|
+
return sig.get();
|
|
267
|
+
},
|
|
268
|
+
get valid() {
|
|
269
|
+
return validCondition;
|
|
270
|
+
},
|
|
271
|
+
set(value) {
|
|
272
|
+
sig.set(value);
|
|
273
|
+
},
|
|
274
|
+
toggle() {
|
|
275
|
+
sig.set(!sig.get());
|
|
276
|
+
},
|
|
277
|
+
onChange(fn) {
|
|
278
|
+
return sig.subscribe(fn);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function createChoiceState(_name, opts) {
|
|
283
|
+
if (!opts.options.includes(opts.initial)) throw new Error(`state.choice initial value "${opts.initial}" must be one of the provided options`);
|
|
284
|
+
const sig = signal(opts.initial);
|
|
285
|
+
const validCondition = createCondition(() => opts.options.includes(sig.get()), (notify) => sig.subscribe(() => notify()));
|
|
286
|
+
return {
|
|
287
|
+
get value() {
|
|
288
|
+
return sig.get();
|
|
289
|
+
},
|
|
290
|
+
get valid() {
|
|
291
|
+
return validCondition;
|
|
292
|
+
},
|
|
293
|
+
set(value) {
|
|
294
|
+
sig.set(value);
|
|
295
|
+
},
|
|
296
|
+
get options() {
|
|
297
|
+
return opts.options;
|
|
298
|
+
},
|
|
299
|
+
onChange(fn) {
|
|
300
|
+
return sig.subscribe(fn);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/registry.ts
|
|
307
|
+
const askMap = /* @__PURE__ */ new Map();
|
|
308
|
+
const actMap = /* @__PURE__ */ new Map();
|
|
309
|
+
const flowMap = /* @__PURE__ */ new Map();
|
|
310
|
+
const surfaceMap = /* @__PURE__ */ new Map();
|
|
311
|
+
const resourceMap = /* @__PURE__ */ new Map();
|
|
312
|
+
function registerAskNode(node) {
|
|
313
|
+
askMap.set(node.id, node);
|
|
314
|
+
}
|
|
315
|
+
function registerActNode(node) {
|
|
316
|
+
actMap.set(node.id, node);
|
|
317
|
+
}
|
|
318
|
+
function registerFlowNode(node) {
|
|
319
|
+
flowMap.set(node.id, node);
|
|
320
|
+
}
|
|
321
|
+
function registerSurfaceNode(node) {
|
|
322
|
+
surfaceMap.set(node.id, node);
|
|
323
|
+
}
|
|
324
|
+
function resetAskRegistry() {
|
|
325
|
+
askMap.clear();
|
|
326
|
+
}
|
|
327
|
+
function resetActRegistry() {
|
|
328
|
+
actMap.clear();
|
|
329
|
+
}
|
|
330
|
+
function resetFlowRegistry() {
|
|
331
|
+
flowMap.clear();
|
|
332
|
+
}
|
|
333
|
+
function resetSurfaceRegistry() {
|
|
334
|
+
surfaceMap.clear();
|
|
335
|
+
}
|
|
336
|
+
function resetResourceRegistry() {
|
|
337
|
+
resourceMap.clear();
|
|
338
|
+
}
|
|
339
|
+
function getAsks() {
|
|
340
|
+
return askMap;
|
|
341
|
+
}
|
|
342
|
+
function getActs() {
|
|
343
|
+
return actMap;
|
|
344
|
+
}
|
|
345
|
+
function getFlows() {
|
|
346
|
+
return flowMap;
|
|
347
|
+
}
|
|
348
|
+
function getSurfaces() {
|
|
349
|
+
return surfaceMap;
|
|
350
|
+
}
|
|
351
|
+
function getResources() {
|
|
352
|
+
return resourceMap;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/ask.ts
|
|
357
|
+
function createAskNode(id, label, stateRef, notifySub) {
|
|
358
|
+
const notifySignal = signal(0);
|
|
359
|
+
notifySub?.(() => {
|
|
360
|
+
notifySignal.set(notifySignal.get() + 1);
|
|
361
|
+
});
|
|
362
|
+
const validCondition = {
|
|
363
|
+
get current() {
|
|
364
|
+
return computeAskValidity(node);
|
|
365
|
+
},
|
|
366
|
+
subscribe(fn) {
|
|
367
|
+
return notifySignal.subscribe(() => fn());
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
const node = {
|
|
371
|
+
id,
|
|
372
|
+
label,
|
|
373
|
+
kind: "text",
|
|
374
|
+
required: false,
|
|
375
|
+
isPrivate: false,
|
|
376
|
+
validators: [],
|
|
377
|
+
state: stateRef,
|
|
378
|
+
get valid() {
|
|
379
|
+
return validCondition;
|
|
380
|
+
},
|
|
381
|
+
get error() {
|
|
382
|
+
return computeAskError(node);
|
|
383
|
+
},
|
|
384
|
+
subscribe(fn) {
|
|
385
|
+
return notifySignal.subscribe(fn);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
return node;
|
|
389
|
+
}
|
|
390
|
+
function computeAskValidity(node) {
|
|
391
|
+
const value = node.state.value;
|
|
392
|
+
if (node.required && (value === "" || value === void 0 || value === null)) return false;
|
|
393
|
+
for (const validator of node.validators) {
|
|
394
|
+
const result = validator(value);
|
|
395
|
+
if (result !== true) return false;
|
|
396
|
+
}
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
function computeAskError(node) {
|
|
400
|
+
const value = node.state.value;
|
|
401
|
+
if (node.required && (value === "" || value === void 0 || value === null)) return node.requiredMessage ?? "This field is required.";
|
|
402
|
+
for (const validator of node.validators) {
|
|
403
|
+
const result = validator(value);
|
|
404
|
+
if (result !== true) return typeof result === "string" ? result : "Invalid value.";
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
var AskBuilder = class {
|
|
409
|
+
node;
|
|
410
|
+
constructor(label, stateRef) {
|
|
411
|
+
const id = `ask_${label.toLowerCase().replace(/\s+/g, "_")}`;
|
|
412
|
+
const onChange = stateRef.onChange;
|
|
413
|
+
const subscribeToState = onChange ? (fn) => onChange((_v) => fn()) : void 0;
|
|
414
|
+
this.node = createAskNode(id, label, stateRef, subscribeToState);
|
|
415
|
+
registerAskNode(this.node);
|
|
416
|
+
}
|
|
417
|
+
get valid() {
|
|
418
|
+
return this.node.valid;
|
|
419
|
+
}
|
|
420
|
+
asContact(kind) {
|
|
421
|
+
this.node.kind = "contact";
|
|
422
|
+
this.node.contactKind = kind;
|
|
423
|
+
return this;
|
|
424
|
+
}
|
|
425
|
+
asSecret() {
|
|
426
|
+
this.node.kind = "secret";
|
|
427
|
+
return this;
|
|
428
|
+
}
|
|
429
|
+
asChoice() {
|
|
430
|
+
this.node.kind = "choice";
|
|
431
|
+
return this;
|
|
432
|
+
}
|
|
433
|
+
required(message) {
|
|
434
|
+
this.node.required = true;
|
|
435
|
+
this.node.requiredMessage = message;
|
|
436
|
+
return this;
|
|
437
|
+
}
|
|
438
|
+
validate(fn) {
|
|
439
|
+
this.node.validators.push(fn);
|
|
440
|
+
return this;
|
|
441
|
+
}
|
|
442
|
+
private() {
|
|
443
|
+
this.node.isPrivate = true;
|
|
444
|
+
return this;
|
|
445
|
+
}
|
|
446
|
+
hint(text) {
|
|
447
|
+
this.node.hintText = text;
|
|
448
|
+
return this;
|
|
449
|
+
}
|
|
450
|
+
toNode() {
|
|
451
|
+
return this.node;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/act.ts
|
|
457
|
+
const kResourceMap = Symbol("resourceMap");
|
|
458
|
+
function createActNode(id, label, conditions, handler, feedback, primary, invalidatedResourceIds = []) {
|
|
459
|
+
const statusSignal = signal(0);
|
|
460
|
+
const notifyStatus = () => statusSignal.set(statusSignal.get() + 1);
|
|
461
|
+
let _enabledCondition;
|
|
462
|
+
function getEnabledCondition() {
|
|
463
|
+
if (!_enabledCondition) _enabledCondition = createCondition(() => computeActEnabled(node), (notify) => {
|
|
464
|
+
const unsubs = node.conditions.filter((c) => c.source).map((c) => c.source.subscribe(() => notify()));
|
|
465
|
+
return () => {
|
|
466
|
+
for (const u of unsubs) u();
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
return _enabledCondition;
|
|
470
|
+
}
|
|
471
|
+
const node = {
|
|
472
|
+
id,
|
|
473
|
+
label,
|
|
474
|
+
primary,
|
|
475
|
+
conditions,
|
|
476
|
+
handler,
|
|
477
|
+
feedback,
|
|
478
|
+
invalidatedResourceIds,
|
|
479
|
+
status: "idle",
|
|
480
|
+
statusMessage: null,
|
|
481
|
+
get enabled() {
|
|
482
|
+
return getEnabledCondition();
|
|
483
|
+
},
|
|
484
|
+
get blockedReasons() {
|
|
485
|
+
return node.conditions.filter((c) => !c.check()).map((c) => c.message).filter((m) => m !== void 0);
|
|
486
|
+
},
|
|
487
|
+
execute: async (context) => {
|
|
488
|
+
await executeAct(node, context, notifyStatus);
|
|
489
|
+
},
|
|
490
|
+
onStatusChange(fn) {
|
|
491
|
+
return statusSignal.subscribe(fn);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
return node;
|
|
495
|
+
}
|
|
496
|
+
function computeActEnabled(node) {
|
|
497
|
+
for (const cond of node.conditions) if (!cond.check()) return false;
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
async function executeAct(node, context, notify) {
|
|
501
|
+
if (!node.enabled.current || !node.handler) return;
|
|
502
|
+
node.status = "pending";
|
|
503
|
+
node.statusMessage = node.feedback?.pending ?? null;
|
|
504
|
+
notify();
|
|
505
|
+
try {
|
|
506
|
+
await node.handler(context ?? {});
|
|
507
|
+
node.status = "success";
|
|
508
|
+
node.statusMessage = node.feedback?.success ?? null;
|
|
509
|
+
notify();
|
|
510
|
+
const resourceMap$1 = context ? context[kResourceMap] : void 0;
|
|
511
|
+
if (resourceMap$1) for (const id of node.invalidatedResourceIds) resourceMap$1.get(id)?.invalidate();
|
|
512
|
+
} catch (error) {
|
|
513
|
+
node.status = "failure";
|
|
514
|
+
const fb = node.feedback?.failure;
|
|
515
|
+
if (typeof fb === "function") node.statusMessage = fb(error instanceof Error ? error : new Error(String(error)));
|
|
516
|
+
else node.statusMessage = fb ?? null;
|
|
517
|
+
notify();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
var ActBuilder = class {
|
|
521
|
+
node;
|
|
522
|
+
constructor(label) {
|
|
523
|
+
const id = `act_${label.toLowerCase().replace(/\s+/g, "_")}`;
|
|
524
|
+
this.node = createActNode(id, label, [], null, void 0, false);
|
|
525
|
+
registerActNode(this.node);
|
|
526
|
+
}
|
|
527
|
+
get enabled() {
|
|
528
|
+
return this.node.enabled;
|
|
529
|
+
}
|
|
530
|
+
primary() {
|
|
531
|
+
this.node.primary = true;
|
|
532
|
+
return this;
|
|
533
|
+
}
|
|
534
|
+
when(condition, message) {
|
|
535
|
+
let check;
|
|
536
|
+
let source;
|
|
537
|
+
if (typeof condition === "function") check = condition;
|
|
538
|
+
else if (typeof condition === "object" && condition !== null && "valid" in condition) {
|
|
539
|
+
const state = condition;
|
|
540
|
+
if (state.valid !== void 0 && isCondition(state.valid)) {
|
|
541
|
+
const cond = state.valid;
|
|
542
|
+
source = cond;
|
|
543
|
+
check = () => cond.current;
|
|
544
|
+
} else if (state.valid !== void 0) {
|
|
545
|
+
const val = state.valid;
|
|
546
|
+
check = () => val;
|
|
547
|
+
} else check = () => false;
|
|
548
|
+
} else if (isCondition(condition)) {
|
|
549
|
+
source = condition;
|
|
550
|
+
check = () => condition.current;
|
|
551
|
+
} else check = () => condition;
|
|
552
|
+
this.node.conditions.push({
|
|
553
|
+
check,
|
|
554
|
+
message,
|
|
555
|
+
source
|
|
556
|
+
});
|
|
557
|
+
return this;
|
|
558
|
+
}
|
|
559
|
+
does(fn) {
|
|
560
|
+
this.node.handler = fn;
|
|
561
|
+
return this;
|
|
562
|
+
}
|
|
563
|
+
invalidates(...resources) {
|
|
564
|
+
this.node.invalidatedResourceIds = resources.map((r) => r.id);
|
|
565
|
+
return this;
|
|
566
|
+
}
|
|
567
|
+
feedback(fb) {
|
|
568
|
+
this.node.feedback = fb;
|
|
569
|
+
return this;
|
|
570
|
+
}
|
|
571
|
+
toNode() {
|
|
572
|
+
return this.node;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/flow.ts
|
|
578
|
+
var FlowBuilder = class {
|
|
579
|
+
node;
|
|
580
|
+
constructor(name) {
|
|
581
|
+
const id = `flow_${name}`;
|
|
582
|
+
this.node = {
|
|
583
|
+
id,
|
|
584
|
+
name,
|
|
585
|
+
steps: []
|
|
586
|
+
};
|
|
587
|
+
registerFlowNode(this.node);
|
|
588
|
+
}
|
|
589
|
+
startsWith(ask) {
|
|
590
|
+
const node = ask instanceof AskBuilder ? ask.toNode() : ask;
|
|
591
|
+
this.node.steps.push({
|
|
592
|
+
type: "ask",
|
|
593
|
+
node
|
|
594
|
+
});
|
|
595
|
+
return this;
|
|
596
|
+
}
|
|
597
|
+
then(item) {
|
|
598
|
+
if (item instanceof ActBuilder) this.node.steps.push({
|
|
599
|
+
type: "act",
|
|
600
|
+
node: item.toNode()
|
|
601
|
+
});
|
|
602
|
+
else if (item instanceof AskBuilder) this.node.steps.push({
|
|
603
|
+
type: "ask",
|
|
604
|
+
node: item.toNode()
|
|
605
|
+
});
|
|
606
|
+
else if ("validators" in item) this.node.steps.push({
|
|
607
|
+
type: "ask",
|
|
608
|
+
node: item
|
|
609
|
+
});
|
|
610
|
+
else this.node.steps.push({
|
|
611
|
+
type: "act",
|
|
612
|
+
node: item
|
|
613
|
+
});
|
|
614
|
+
return this;
|
|
615
|
+
}
|
|
616
|
+
toNode() {
|
|
617
|
+
return this.node;
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/surface.ts
|
|
623
|
+
var SurfaceBuilder = class {
|
|
624
|
+
node;
|
|
625
|
+
constructor(name) {
|
|
626
|
+
const id = `surface_${name}`;
|
|
627
|
+
this.node = {
|
|
628
|
+
id,
|
|
629
|
+
name,
|
|
630
|
+
items: []
|
|
631
|
+
};
|
|
632
|
+
registerSurfaceNode(this.node);
|
|
633
|
+
}
|
|
634
|
+
contains(...items) {
|
|
635
|
+
for (const item of items) if (item instanceof ActBuilder) this.node.items.push(item.toNode());
|
|
636
|
+
else if (item instanceof AskBuilder) this.node.items.push(item.toNode());
|
|
637
|
+
else this.node.items.push(item);
|
|
638
|
+
return this;
|
|
639
|
+
}
|
|
640
|
+
toNode() {
|
|
641
|
+
return this.node;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/screen.ts
|
|
647
|
+
function screen(name, fn) {
|
|
648
|
+
resetAskRegistry();
|
|
649
|
+
resetActRegistry();
|
|
650
|
+
resetFlowRegistry();
|
|
651
|
+
resetSurfaceRegistry();
|
|
652
|
+
resetResourceRegistry();
|
|
653
|
+
const configs = [];
|
|
654
|
+
const builder = {
|
|
655
|
+
state: {
|
|
656
|
+
text: (n, opts) => createTextState(n, opts?.initial),
|
|
657
|
+
boolean: (n, opts) => createBooleanState(n, opts?.initial),
|
|
658
|
+
choice: (n, opts) => createChoiceState(n, opts)
|
|
659
|
+
},
|
|
660
|
+
ask: (label, state) => new AskBuilder(label, state),
|
|
661
|
+
act: (label) => new ActBuilder(label),
|
|
662
|
+
flow: (n) => new FlowBuilder(n),
|
|
663
|
+
surface: (n) => new SurfaceBuilder(n),
|
|
664
|
+
resource: (n, config) => {
|
|
665
|
+
const id = `resource_${n}`;
|
|
666
|
+
const ref = new ResourceRef(id, n, config.load, config.autoLoad ?? true);
|
|
667
|
+
configs.push({
|
|
668
|
+
id,
|
|
669
|
+
name: n,
|
|
670
|
+
autoLoad: config.autoLoad ?? true,
|
|
671
|
+
loader: config.load,
|
|
672
|
+
ref
|
|
673
|
+
});
|
|
674
|
+
return ref;
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
fn(builder);
|
|
678
|
+
const asks = getAsks();
|
|
679
|
+
const acts = getActs();
|
|
680
|
+
const flows = getFlows();
|
|
681
|
+
const surfaces = getSurfaces();
|
|
682
|
+
return {
|
|
683
|
+
name,
|
|
684
|
+
asks: Array.from(asks.values()),
|
|
685
|
+
acts: Array.from(acts.values()),
|
|
686
|
+
flows: Array.from(flows.values()),
|
|
687
|
+
surfaces: Array.from(surfaces.values()),
|
|
688
|
+
resourceConfigs: configs
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/graph.ts
|
|
694
|
+
function computeDiagnostics(screenDef) {
|
|
695
|
+
const diagnostics = [];
|
|
696
|
+
const primaryActions = screenDef.acts.filter((a) => a.primary);
|
|
697
|
+
if (primaryActions.length > 1) diagnostics.push({
|
|
698
|
+
severity: "warning",
|
|
699
|
+
code: "multiple-primary-actions",
|
|
700
|
+
message: "Screen has multiple primary actions, so default action behavior is ambiguous."
|
|
701
|
+
});
|
|
702
|
+
for (const ask of screenDef.asks) if (ask.kind === "secret" && !ask.isPrivate) diagnostics.push({
|
|
703
|
+
severity: "warning",
|
|
704
|
+
code: "secret-ask-not-private",
|
|
705
|
+
message: "Secret ask should also be marked private.",
|
|
706
|
+
nodeId: ask.id
|
|
707
|
+
});
|
|
708
|
+
for (const act of screenDef.acts) if (act.primary && act.conditions.length > 0 && act.conditions.every((c) => c.message === void 0)) diagnostics.push({
|
|
709
|
+
severity: "info",
|
|
710
|
+
code: "primary-action-without-blocked-reason",
|
|
711
|
+
message: "Primary action can be blocked without an explainable reason.",
|
|
712
|
+
nodeId: act.id
|
|
713
|
+
});
|
|
714
|
+
const surfacedNodeIds = /* @__PURE__ */ new Set();
|
|
715
|
+
for (const surface of screenDef.surfaces) for (const item of surface.items) surfacedNodeIds.add(item.id);
|
|
716
|
+
for (const ask of screenDef.asks) if (!surfacedNodeIds.has(ask.id)) diagnostics.push({
|
|
717
|
+
severity: "warning",
|
|
718
|
+
code: "ask-not-in-surface",
|
|
719
|
+
message: "Ask is defined but not included in any surface.",
|
|
720
|
+
nodeId: ask.id
|
|
721
|
+
});
|
|
722
|
+
for (const act of screenDef.acts) if (!surfacedNodeIds.has(act.id)) diagnostics.push({
|
|
723
|
+
severity: "warning",
|
|
724
|
+
code: "action-not-in-surface",
|
|
725
|
+
message: "Action is defined but not included in any surface.",
|
|
726
|
+
nodeId: act.id
|
|
727
|
+
});
|
|
728
|
+
return diagnostics;
|
|
729
|
+
}
|
|
730
|
+
function inspectScreen(screenDef, runtimeResources) {
|
|
731
|
+
return {
|
|
732
|
+
name: screenDef.name,
|
|
733
|
+
asks: screenDef.asks.map((a) => ({
|
|
734
|
+
id: a.id,
|
|
735
|
+
label: a.label,
|
|
736
|
+
kind: a.kind,
|
|
737
|
+
required: a.required,
|
|
738
|
+
isPrivate: a.isPrivate,
|
|
739
|
+
valid: a.valid.current,
|
|
740
|
+
error: a.error
|
|
741
|
+
})),
|
|
742
|
+
acts: screenDef.acts.map((a) => ({
|
|
743
|
+
id: a.id,
|
|
744
|
+
label: a.label,
|
|
745
|
+
primary: a.primary,
|
|
746
|
+
enabled: a.enabled.current,
|
|
747
|
+
blockedReasons: a.blockedReasons,
|
|
748
|
+
status: a.status,
|
|
749
|
+
statusMessage: a.statusMessage,
|
|
750
|
+
invalidates: a.invalidatedResourceIds
|
|
751
|
+
})),
|
|
752
|
+
flows: screenDef.flows.map((f) => ({
|
|
753
|
+
id: f.id,
|
|
754
|
+
name: f.name,
|
|
755
|
+
stepCount: f.steps.length
|
|
756
|
+
})),
|
|
757
|
+
surfaces: screenDef.surfaces.map((s) => ({
|
|
758
|
+
id: s.id,
|
|
759
|
+
name: s.name,
|
|
760
|
+
itemCount: s.items.length
|
|
761
|
+
})),
|
|
762
|
+
resources: (runtimeResources ?? []).map((r) => ({
|
|
763
|
+
id: r.id,
|
|
764
|
+
name: r.name,
|
|
765
|
+
status: r.status,
|
|
766
|
+
hasValue: r.value !== void 0,
|
|
767
|
+
stale: r.stale.current,
|
|
768
|
+
error: r.status === "failed" ? r.error instanceof Error ? r.error.message : String(r.error) : void 0
|
|
769
|
+
})),
|
|
770
|
+
diagnostics: computeDiagnostics(screenDef)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/runtime.ts
|
|
776
|
+
var ScreenRuntime = class {
|
|
777
|
+
_screen;
|
|
778
|
+
_started = false;
|
|
779
|
+
_disposed = false;
|
|
780
|
+
_unsubscribers = [];
|
|
781
|
+
_services;
|
|
782
|
+
_resourceNodes = [];
|
|
783
|
+
_resourceNodeMap = null;
|
|
784
|
+
autoloadedResources = /* @__PURE__ */ new Set();
|
|
785
|
+
constructor(screen$1, services = {}) {
|
|
786
|
+
this._screen = screen$1;
|
|
787
|
+
this._services = services;
|
|
788
|
+
}
|
|
789
|
+
get screen() {
|
|
790
|
+
return this._screen;
|
|
791
|
+
}
|
|
792
|
+
get graph() {
|
|
793
|
+
return inspectScreen(this._screen, this._resourceNodes);
|
|
794
|
+
}
|
|
795
|
+
get resources() {
|
|
796
|
+
return this._resourceNodes;
|
|
797
|
+
}
|
|
798
|
+
get services() {
|
|
799
|
+
return this._services;
|
|
800
|
+
}
|
|
801
|
+
getExecutionContext() {
|
|
802
|
+
return this._services;
|
|
803
|
+
}
|
|
804
|
+
executeAct(act, context) {
|
|
805
|
+
const ctx = context ?? this.getExecutionContext();
|
|
806
|
+
if (this._resourceNodeMap) return act.execute({
|
|
807
|
+
...ctx,
|
|
808
|
+
[kResourceMap]: this._resourceNodeMap
|
|
809
|
+
});
|
|
810
|
+
return act.execute(ctx);
|
|
811
|
+
}
|
|
812
|
+
async start() {
|
|
813
|
+
if (this._started) return;
|
|
814
|
+
this._started = true;
|
|
815
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
816
|
+
for (const config of this._screen.resourceConfigs) {
|
|
817
|
+
const node = createResourceNode(config.id, config.name, config.loader, false);
|
|
818
|
+
this._resourceNodes.push(node);
|
|
819
|
+
nodeMap.set(config.id, node);
|
|
820
|
+
}
|
|
821
|
+
this._resourceNodeMap = nodeMap;
|
|
822
|
+
for (const config of this._screen.resourceConfigs) {
|
|
823
|
+
const node = nodeMap.get(config.id);
|
|
824
|
+
if (node && config.ref) config.ref._connect(node);
|
|
825
|
+
}
|
|
826
|
+
const pureServices = this._services;
|
|
827
|
+
const toLoad = this._resourceNodes.filter((r) => {
|
|
828
|
+
const config = this._screen.resourceConfigs.find((c) => c.id === r.id);
|
|
829
|
+
return (config?.autoLoad ?? false) && !this.autoloadedResources.has(r.id);
|
|
830
|
+
});
|
|
831
|
+
for (const r of toLoad) this.autoloadedResources.add(r.id);
|
|
832
|
+
if (toLoad.length > 0) await Promise.all(toLoad.map((r) => r.load(pureServices)));
|
|
833
|
+
}
|
|
834
|
+
dispose() {
|
|
835
|
+
if (this._disposed) return;
|
|
836
|
+
this._disposed = true;
|
|
837
|
+
for (const unsub of this._unsubscribers) unsub();
|
|
838
|
+
this._unsubscribers = [];
|
|
839
|
+
for (const config of this._screen.resourceConfigs) if (config.ref && this._resourceNodeMap) {
|
|
840
|
+
const node = this._resourceNodeMap.get(config.id);
|
|
841
|
+
if (node) config.ref._disconnect(node);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/** @internal */
|
|
845
|
+
_addUnsubscriber(fn) {
|
|
846
|
+
this._unsubscribers.push(fn);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
function createScreenRuntime(screen$1, options) {
|
|
850
|
+
return new ScreenRuntime(screen$1, options?.services);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
//#endregion
|
|
854
|
+
export { ResourceRef, createResourceNode, createScreenRuntime, getActs, getAsks, getFlows, getResources, getSurfaces, inspectScreen, isCondition, resetActRegistry, resetAskRegistry, resetFlowRegistry, resetResourceRegistry, resetSurfaceRegistry, screen };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AnyAskNode } from "./ask.js";
|
|
2
|
+
import type { ActNode, DefaultScreenServices } from "./act.js";
|
|
3
|
+
import type { FlowNode } from "./flow.js";
|
|
4
|
+
import type { SurfaceNode } from "./surface.js";
|
|
5
|
+
import type { AnyResourceNode } from "./resource.js";
|
|
6
|
+
export declare function registerAskNode(node: AnyAskNode): void;
|
|
7
|
+
export declare function unregisterAskNode(id: string): void;
|
|
8
|
+
export declare function registerActNode<TServices extends object = DefaultScreenServices>(node: ActNode<TServices>): void;
|
|
9
|
+
export declare function unregisterActNode(id: string): void;
|
|
10
|
+
export declare function registerFlowNode(node: FlowNode): void;
|
|
11
|
+
export declare function unregisterFlowNode(id: string): void;
|
|
12
|
+
export declare function registerSurfaceNode(node: SurfaceNode): void;
|
|
13
|
+
export declare function unregisterSurfaceNode(id: string): void;
|
|
14
|
+
export declare function registerResourceNode(node: AnyResourceNode): void;
|
|
15
|
+
export declare function unregisterResourceNode(id: string): void;
|
|
16
|
+
export declare function resetAskRegistry(): void;
|
|
17
|
+
export declare function resetActRegistry(): void;
|
|
18
|
+
export declare function resetFlowRegistry(): void;
|
|
19
|
+
export declare function resetSurfaceRegistry(): void;
|
|
20
|
+
export declare function resetResourceRegistry(): void;
|
|
21
|
+
export declare function getAsks(): Map<string, AnyAskNode>;
|
|
22
|
+
export declare function getActs(): Map<string, ActNode<any>>;
|
|
23
|
+
export declare function getFlows(): Map<string, FlowNode>;
|
|
24
|
+
export declare function getSurfaces(): Map<string, SurfaceNode>;
|
|
25
|
+
export declare function getResources(): Map<string, AnyResourceNode>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Condition } from "./signal.js";
|
|
2
|
+
import type { ActionExecutionContext, DefaultScreenServices } from "./act.js";
|
|
3
|
+
export type ResourceStatus = "idle" | "pending" | "ready" | "failed";
|
|
4
|
+
export type ResourceLoadContext<TServices extends object = DefaultScreenServices> = ActionExecutionContext<TServices>;
|
|
5
|
+
type ResourceLoader<TValue, TServices extends object> = (() => TValue | Promise<TValue>) | ((context: ResourceLoadContext<TServices>) => TValue | Promise<TValue>);
|
|
6
|
+
export type ResourceNode<TValue, TServices extends object = DefaultScreenServices> = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
autoLoad: boolean;
|
|
10
|
+
status: ResourceStatus;
|
|
11
|
+
value: TValue | undefined;
|
|
12
|
+
error: unknown | undefined;
|
|
13
|
+
ready: Condition;
|
|
14
|
+
pending: Condition;
|
|
15
|
+
failed: Condition;
|
|
16
|
+
stale: Condition;
|
|
17
|
+
load: (context?: ResourceLoadContext<TServices>) => Promise<void>;
|
|
18
|
+
reload: (context?: ResourceLoadContext<TServices>) => Promise<void>;
|
|
19
|
+
invalidate: () => void;
|
|
20
|
+
subscribe: (fn: () => void) => () => void;
|
|
21
|
+
};
|
|
22
|
+
export type AnyResourceNode = ResourceNode<unknown, any>;
|
|
23
|
+
export type ResourceConfig<TValue = unknown, TServices extends object = DefaultScreenServices> = {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
autoLoad: boolean;
|
|
27
|
+
loader: ResourceLoader<TValue, TServices>;
|
|
28
|
+
ref?: ResourceRef<TValue, TServices>;
|
|
29
|
+
};
|
|
30
|
+
export declare function createResourceConfig<TValue, TServices extends object = DefaultScreenServices>(id: string, name: string, loader: ResourceLoader<TValue, TServices>, autoLoad?: boolean): ResourceConfig<TValue, TServices>;
|
|
31
|
+
export declare function createResourceNode<TValue, TServices extends object = DefaultScreenServices>(id: string, name: string, loader: ResourceLoader<TValue, TServices>, autoLoad?: boolean): ResourceNode<TValue, TServices>;
|
|
32
|
+
export declare class ResourceRef<TValue, TServices extends object = DefaultScreenServices> {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly autoLoad: boolean;
|
|
36
|
+
readonly loader: ResourceLoader<TValue, TServices>;
|
|
37
|
+
private _connected;
|
|
38
|
+
private _connSignal;
|
|
39
|
+
private _nodeSub;
|
|
40
|
+
private _readyCache;
|
|
41
|
+
private _pendingCache;
|
|
42
|
+
private _failedCache;
|
|
43
|
+
private _staleCache;
|
|
44
|
+
constructor(id: string, name: string, loader: ResourceLoader<TValue, TServices>, autoLoad: boolean);
|
|
45
|
+
get status(): ResourceStatus;
|
|
46
|
+
get value(): TValue | undefined;
|
|
47
|
+
get error(): unknown | undefined;
|
|
48
|
+
get ready(): Condition;
|
|
49
|
+
get pending(): Condition;
|
|
50
|
+
get failed(): Condition;
|
|
51
|
+
get stale(): Condition;
|
|
52
|
+
load(context?: ResourceLoadContext<TServices>): Promise<void>;
|
|
53
|
+
reload(context?: ResourceLoadContext<TServices>): Promise<void>;
|
|
54
|
+
invalidate(): void;
|
|
55
|
+
subscribe(fn: () => void): () => void;
|
|
56
|
+
_connect(node: ResourceNode<TValue, TServices>): void;
|
|
57
|
+
_disconnect(expectedNode?: ResourceNode<TValue, TServices>): void;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ScreenDefinition } from "./screen.js";
|
|
2
|
+
import { type InspectedScreen } from "./graph.js";
|
|
3
|
+
import type { AnyResourceNode } from "./resource.js";
|
|
4
|
+
import type { ActionExecutionContext, DefaultScreenServices, ActNode } from "./act.js";
|
|
5
|
+
export declare class ScreenRuntime<TServices extends object = DefaultScreenServices> {
|
|
6
|
+
private _screen;
|
|
7
|
+
private _started;
|
|
8
|
+
private _disposed;
|
|
9
|
+
private _unsubscribers;
|
|
10
|
+
private _services;
|
|
11
|
+
private _resourceNodes;
|
|
12
|
+
private _resourceNodeMap;
|
|
13
|
+
private autoloadedResources;
|
|
14
|
+
constructor(screen: ScreenDefinition<TServices>, services?: TServices);
|
|
15
|
+
get screen(): ScreenDefinition<TServices>;
|
|
16
|
+
get graph(): InspectedScreen;
|
|
17
|
+
get resources(): AnyResourceNode[];
|
|
18
|
+
get services(): TServices;
|
|
19
|
+
getExecutionContext(): ActionExecutionContext<TServices>;
|
|
20
|
+
executeAct(act: ActNode<TServices>, context?: ActionExecutionContext<TServices>): Promise<void>;
|
|
21
|
+
start(): Promise<void>;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
/** @internal */
|
|
24
|
+
_addUnsubscriber(fn: () => void): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function createScreenRuntime<TServices extends object = DefaultScreenServices>(screen: ScreenDefinition<TServices>, options?: {
|
|
27
|
+
services?: TServices;
|
|
28
|
+
}): ScreenRuntime<TServices>;
|
package/dist/screen.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AnyAskNode } from "./ask.js";
|
|
2
|
+
import type { ActNode, DefaultScreenServices } from "./act.js";
|
|
3
|
+
import type { FlowNode } from "./flow.js";
|
|
4
|
+
import type { SurfaceNode } from "./surface.js";
|
|
5
|
+
import type { ResourceConfig, ResourceLoadContext } from "./resource.js";
|
|
6
|
+
import { ResourceRef } from "./resource.js";
|
|
7
|
+
import { type TextState, type BooleanState, type ChoiceState } from "./state.js";
|
|
8
|
+
import { AskBuilder } from "./ask.js";
|
|
9
|
+
import { ActBuilder } from "./act.js";
|
|
10
|
+
import { FlowBuilder } from "./flow.js";
|
|
11
|
+
import { SurfaceBuilder } from "./surface.js";
|
|
12
|
+
export type ScreenBuilder<TServices extends object = DefaultScreenServices> = {
|
|
13
|
+
state: {
|
|
14
|
+
text: (name: string, opts?: {
|
|
15
|
+
initial?: string;
|
|
16
|
+
}) => TextState;
|
|
17
|
+
boolean: (name: string, opts?: {
|
|
18
|
+
initial?: boolean;
|
|
19
|
+
}) => BooleanState;
|
|
20
|
+
choice: <T extends string>(name: string, opts: {
|
|
21
|
+
initial: T;
|
|
22
|
+
options: readonly T[];
|
|
23
|
+
}) => ChoiceState<T>;
|
|
24
|
+
};
|
|
25
|
+
ask: <T>(label: string, state: {
|
|
26
|
+
value: T;
|
|
27
|
+
}) => AskBuilder<T>;
|
|
28
|
+
act: (label: string) => ActBuilder<TServices>;
|
|
29
|
+
flow: (name: string) => FlowBuilder;
|
|
30
|
+
surface: (name: string) => SurfaceBuilder;
|
|
31
|
+
resource: <T>(name: string, config: {
|
|
32
|
+
load: (() => Promise<T>) | ((context: ResourceLoadContext<TServices>) => Promise<T>);
|
|
33
|
+
autoLoad?: boolean;
|
|
34
|
+
}) => ResourceRef<T, TServices>;
|
|
35
|
+
};
|
|
36
|
+
export type ScreenDefinition<TServices extends object = DefaultScreenServices> = {
|
|
37
|
+
name: string;
|
|
38
|
+
asks: AnyAskNode[];
|
|
39
|
+
acts: ActNode<TServices>[];
|
|
40
|
+
flows: FlowNode[];
|
|
41
|
+
surfaces: SurfaceNode[];
|
|
42
|
+
resourceConfigs: ResourceConfig[];
|
|
43
|
+
};
|
|
44
|
+
export declare function screen<TServices extends object = DefaultScreenServices>(name: string, fn: ($: ScreenBuilder<TServices>) => void): ScreenDefinition<TServices>;
|
package/dist/signal.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Condition = {
|
|
2
|
+
readonly current: boolean;
|
|
3
|
+
readonly reason?: string;
|
|
4
|
+
subscribe(fn: () => void): () => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function isCondition(value: unknown): value is Condition;
|
|
7
|
+
export declare function createCondition(compute: () => boolean, subscribeToChanges: (onChange: () => void) => () => void, reason?: string): Condition;
|
|
8
|
+
export type Signal<T> = {
|
|
9
|
+
get(): T;
|
|
10
|
+
set(value: T): void;
|
|
11
|
+
subscribe(fn: (value: T) => void): () => void;
|
|
12
|
+
};
|
|
13
|
+
export declare function signal<T>(initial: T): Signal<T>;
|
|
14
|
+
export type Computed<T> = {
|
|
15
|
+
get(): T;
|
|
16
|
+
subscribe(fn: (value: T) => void): () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function derive<T>(dependencies: Array<{
|
|
19
|
+
subscribe: (fn: (...args: unknown[]) => void) => () => void;
|
|
20
|
+
get(): unknown;
|
|
21
|
+
}>, compute: () => T): Computed<T>;
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Condition } from "./signal.js";
|
|
2
|
+
export interface TextState {
|
|
3
|
+
value: string;
|
|
4
|
+
valid: Condition;
|
|
5
|
+
set(value: string): void;
|
|
6
|
+
onChange(fn: (value: string) => void): () => void;
|
|
7
|
+
clear(): void;
|
|
8
|
+
}
|
|
9
|
+
export interface BooleanState {
|
|
10
|
+
value: boolean;
|
|
11
|
+
valid: Condition;
|
|
12
|
+
set(value: boolean): void;
|
|
13
|
+
toggle(): void;
|
|
14
|
+
onChange(fn: (value: boolean) => void): () => void;
|
|
15
|
+
}
|
|
16
|
+
export interface ChoiceState<T extends string> {
|
|
17
|
+
value: T;
|
|
18
|
+
valid: Condition;
|
|
19
|
+
set(value: T): void;
|
|
20
|
+
options: readonly T[];
|
|
21
|
+
onChange(fn: (value: T) => void): () => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createTextState(_name: string, initial?: string): TextState;
|
|
24
|
+
export declare function createBooleanState(_name: string, initial?: boolean): BooleanState;
|
|
25
|
+
export declare function createChoiceState<T extends string>(_name: string, opts: {
|
|
26
|
+
initial: T;
|
|
27
|
+
options: readonly T[];
|
|
28
|
+
}): ChoiceState<T>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AnyAskNode } from "./ask.js";
|
|
2
|
+
import type { ActNode } from "./act.js";
|
|
3
|
+
import { AskBuilder } from "./ask.js";
|
|
4
|
+
import { ActBuilder } from "./act.js";
|
|
5
|
+
export type SurfaceNode = {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
items: Array<AnyAskNode | ActNode>;
|
|
9
|
+
};
|
|
10
|
+
export declare class SurfaceBuilder {
|
|
11
|
+
private node;
|
|
12
|
+
constructor(name: string);
|
|
13
|
+
contains(...items: Array<AnyAskNode | ActNode | AskBuilder<any> | ActBuilder<any>>): this;
|
|
14
|
+
toNode(): SurfaceNode;
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intent-framework/core",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.1.0-alpha.0",
|
|
7
|
+
"description": "Platformless semantic graph and runtime for Intent applications",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/intent-framework/intent.git",
|
|
12
|
+
"directory": "packages/core"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"tsdown": "^0.3.0",
|
|
30
|
+
"typescript": "^5.7.0",
|
|
31
|
+
"vitest": "^3.0.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsdown",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"typecheck": "tsc -b",
|
|
37
|
+
"lint": "tsc -b"
|
|
38
|
+
}
|
|
39
|
+
}
|