@intent-framework/core 0.1.0-alpha.0 → 0.1.0-alpha.10

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 ADDED
@@ -0,0 +1,67 @@
1
+ # @intent-framework/core
2
+
3
+ Platformless semantic graph and runtime for Intent applications.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add @intent-framework/core
9
+ ```
10
+
11
+ ```sh
12
+ npm install @intent-framework/core
13
+ ```
14
+
15
+ ## What it provides
16
+
17
+ - `screen()` — define a semantic interaction space
18
+ - `$.state.text()` / `$.state.boolean()` / `$.state.choice()` — reactive state
19
+ - `$.ask()` — user-facing question with validation
20
+ - `$.act()` — executable action with conditions, lifecycle, and feedback
21
+ - `$.resource()` — async state with load/reload lifecycle
22
+ - `$.surface()` — named containment surface
23
+ - `createScreenRuntime()` — runtime that owns screen state and resources
24
+ - `inspectScreen()` — semantic graph snapshot with diagnostics
25
+ - Condition and signal primitives
26
+
27
+ ## Minimal example
28
+
29
+ ```ts
30
+ import { screen, inspectScreen } from "@intent-framework/core"
31
+
32
+ const InviteMember = screen("InviteMember", $ => {
33
+ const email = $.state.text("email")
34
+
35
+ const emailAsk = $.ask("Email", email)
36
+ .required("Email is required")
37
+ .validate(value => value.includes("@") ? true : "Enter a valid email")
38
+
39
+ const invite = $.act("Invite member")
40
+ .primary()
41
+ .when(emailAsk.valid, "Enter a valid email first")
42
+ .does(() => {
43
+ console.log("invite", email.value)
44
+ })
45
+
46
+ $.surface("main").contains(emailAsk, invite)
47
+ })
48
+
49
+ const graph = inspectScreen(InviteMember)
50
+ console.log(graph.diagnostics)
51
+ ```
52
+
53
+ ## Where this fits
54
+
55
+ Core defines the product graph. It has no DOM, React, Node, or framework dependencies. Renderers (`@intent-framework/dom`), the router (`@intent-framework/router`), and testing (`@intent-framework/testing`) all build on core.
56
+
57
+ ## Learn more
58
+
59
+ - [Root README](../../README.md) — project overview and philosophy
60
+ - [Quickstart](../../docs/Quickstart.md) — step-by-step guide
61
+ - [Inspect Screen and Diagnostics Guide](../../docs/Inspect-Screen.md) — graph inspection and diagnostics
62
+ - [Resources Guide](../../docs/Resources.md) — resource lifecycle and runtime scoping
63
+ - [MVP Checkpoint](../../docs/MVP-Checkpoint.md) — current implementation boundaries
64
+
65
+ ## Status
66
+
67
+ Experimental alpha. APIs may change. Not recommended for production use.
package/dist/graph.d.ts CHANGED
@@ -2,16 +2,24 @@ import type { ScreenDefinition } from "./screen.js";
2
2
  import type { DefaultScreenServices } from "./act.js";
3
3
  import type { AnyResourceNode } from "./resource.js";
4
4
  export type DiagnosticSeverity = "info" | "warning" | "error";
5
+ export type FlowDiagnosticMeta = {
6
+ flowNodeId: string;
7
+ flowSemanticNodeId?: string;
8
+ };
5
9
  export type GraphDiagnostic = {
6
10
  severity: DiagnosticSeverity;
7
11
  code: string;
8
12
  message: string;
9
13
  nodeId?: string;
14
+ semanticNodeId?: string;
15
+ flow?: FlowDiagnosticMeta;
10
16
  };
11
17
  export type InspectedScreen = {
12
18
  name: string;
19
+ semanticId: string;
13
20
  asks: Array<{
14
21
  id: string;
22
+ semanticId: string;
15
23
  label: string;
16
24
  kind: string;
17
25
  required: boolean;
@@ -21,6 +29,7 @@ export type InspectedScreen = {
21
29
  }>;
22
30
  acts: Array<{
23
31
  id: string;
32
+ semanticId: string;
24
33
  label: string;
25
34
  primary: boolean;
26
35
  enabled: boolean;
@@ -31,16 +40,19 @@ export type InspectedScreen = {
31
40
  }>;
32
41
  flows: Array<{
33
42
  id: string;
43
+ semanticId: string;
34
44
  name: string;
35
45
  stepCount: number;
36
46
  }>;
37
47
  surfaces: Array<{
38
48
  id: string;
49
+ semanticId: string;
39
50
  name: string;
40
51
  itemCount: number;
41
52
  }>;
42
53
  resources: Array<{
43
54
  id: string;
55
+ semanticId: string;
44
56
  name: string;
45
57
  status: string;
46
58
  hasValue: boolean;
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  export { screen } from "./screen.js";
2
2
  export type { ScreenDefinition, ScreenBuilder } from "./screen.js";
3
3
  export { inspectScreen } from "./graph.js";
4
- export type { InspectedScreen, GraphDiagnostic, DiagnosticSeverity } from "./graph.js";
4
+ export type { InspectedScreen, GraphDiagnostic, DiagnosticSeverity, FlowDiagnosticMeta } from "./graph.js";
5
5
  export type { TextState, BooleanState, ChoiceState } from "./state.js";
6
6
  export type { AskNode, AnyAskNode, AskKind, AskBuilder } from "./ask.js";
7
7
  export type { ActNode, ActCondition, ActStatus, FeedbackConfig, ActBuilder, NavigationService, ActionExecutionContext, DefaultScreenServices } from "./act.js";
8
8
  export type { FlowNode, FlowStep, FlowBuilder } from "./flow.js";
9
9
  export type { SurfaceNode, SurfaceBuilder } from "./surface.js";
10
- export type { ResourceNode, ResourceConfig, ResourceLoadContext, ResourceStatus, AnyResourceNode } from "./resource.js";
10
+ export type { ResourceNode, ResourceConfig, ResourceCacheOptions, ResourceLoadContext, ResourceStatus, ResourceKey, AnyResourceNode } from "./resource.js";
11
11
  export { ResourceRef, createResourceNode } from "./resource.js";
12
12
  export { createScreenRuntime } from "./runtime.js";
13
13
  export type { ScreenRuntime } from "./runtime.js";
package/dist/index.js CHANGED
@@ -37,9 +37,25 @@ function signal(initial) {
37
37
 
38
38
  //#endregion
39
39
  //#region src/resource.ts
40
- function createResourceNode(id, name, loader, autoLoad = true) {
40
+ function createResourceNode(id, name, loader, autoLoad = true, cache) {
41
41
  const statusSignal = signal(0);
42
42
  const staleSignal = signal(0);
43
+ const hasKey = typeof cache?.key === "function";
44
+ const DEFAULT_KEY = "";
45
+ function createEntry() {
46
+ return {
47
+ value: void 0,
48
+ status: "idle",
49
+ error: void 0,
50
+ stale: false,
51
+ staleTimer: null,
52
+ cacheTimer: null,
53
+ inFlightPromise: null
54
+ };
55
+ }
56
+ const entries = /* @__PURE__ */ new Map();
57
+ let _activeKey = DEFAULT_KEY;
58
+ if (!hasKey) entries.set(DEFAULT_KEY, createEntry());
43
59
  let currentStatus = "idle";
44
60
  let currentValue = void 0;
45
61
  let currentError = void 0;
@@ -47,10 +63,94 @@ function createResourceNode(id, name, loader, autoLoad = true) {
47
63
  let lastContext = void 0;
48
64
  const notify = () => statusSignal.set(statusSignal.get() + 1);
49
65
  const staleNotify = () => staleSignal.set(staleSignal.get() + 1);
66
+ const shouldDeduplicate = cache ? cache.deduplicate !== false : false;
50
67
  let _ready;
51
68
  let _pending;
52
69
  let _failed;
53
70
  let _stale;
71
+ function getActiveEntry() {
72
+ let entry = entries.get(_activeKey);
73
+ if (!entry) {
74
+ entry = createEntry();
75
+ entries.set(_activeKey, entry);
76
+ }
77
+ return entry;
78
+ }
79
+ function syncFromEntry(entry) {
80
+ currentStatus = entry.status;
81
+ currentValue = entry.value;
82
+ currentError = entry.error;
83
+ if (currentStale !== entry.stale) {
84
+ currentStale = entry.stale;
85
+ staleNotify();
86
+ }
87
+ notify();
88
+ }
89
+ function syncFromActiveEntry() {
90
+ syncFromEntry(getActiveEntry());
91
+ }
92
+ function encodeResourceKey(key) {
93
+ if (Array.isArray(key)) return ["array", key.map(encodeResourceKey)];
94
+ if (key === null) return ["null"];
95
+ if (key === void 0) return ["undefined"];
96
+ if (typeof key === "string") return ["string", key];
97
+ if (typeof key === "boolean") return ["boolean", key];
98
+ if (typeof key === "number") {
99
+ if (Number.isNaN(key)) return ["number", "NaN"];
100
+ if (key === Infinity) return ["number", "Infinity"];
101
+ if (key === -Infinity) return ["number", "-Infinity"];
102
+ if (Object.is(key, -0)) return ["number", "-0"];
103
+ return ["number", key];
104
+ }
105
+ const exhaustive = key;
106
+ return exhaustive;
107
+ }
108
+ function normalizeKey(key) {
109
+ return JSON.stringify(encodeResourceKey(key));
110
+ }
111
+ function resolveKey(ctx) {
112
+ if (!hasKey) return DEFAULT_KEY;
113
+ const context = ctx ?? lastContext ?? {};
114
+ return normalizeKey(cache.key(context));
115
+ }
116
+ function _clearEntryStaleTimer(entry) {
117
+ if (entry.staleTimer != null) {
118
+ clearTimeout(entry.staleTimer);
119
+ entry.staleTimer = null;
120
+ }
121
+ }
122
+ function _startEntryStaleTimer(entry, key) {
123
+ _clearEntryStaleTimer(entry);
124
+ if (cache?.staleTime != null && isFinite(cache.staleTime)) entry.staleTimer = setTimeout(() => {
125
+ if (!entry.stale) {
126
+ entry.stale = true;
127
+ _startEntryCacheTimer(entry, key);
128
+ if (entry === getActiveEntry()) {
129
+ currentStale = true;
130
+ staleNotify();
131
+ }
132
+ }
133
+ }, cache.staleTime);
134
+ }
135
+ function _clearEntryCacheTimer(entry) {
136
+ if (entry.cacheTimer != null) {
137
+ clearTimeout(entry.cacheTimer);
138
+ entry.cacheTimer = null;
139
+ }
140
+ }
141
+ function _startEntryCacheTimer(entry, key) {
142
+ _clearEntryCacheTimer(entry);
143
+ if (cache?.cacheTime != null && isFinite(cache.cacheTime)) entry.cacheTimer = setTimeout(() => {
144
+ if (entry === getActiveEntry()) {
145
+ entry.value = void 0;
146
+ entry.error = void 0;
147
+ entry.status = "idle";
148
+ entry.stale = false;
149
+ _clearEntryCacheTimer(entry);
150
+ syncFromActiveEntry();
151
+ } else entries.delete(key);
152
+ }, cache.cacheTime);
153
+ }
54
154
  function getReady() {
55
155
  if (!_ready) _ready = createCondition(() => currentStatus === "ready", (notify$1) => statusSignal.subscribe(() => notify$1()));
56
156
  return _ready;
@@ -67,34 +167,68 @@ function createResourceNode(id, name, loader, autoLoad = true) {
67
167
  if (!_stale) _stale = createCondition(() => currentStale, (notify$1) => staleSignal.subscribe(() => notify$1()));
68
168
  return _stale;
69
169
  }
70
- async function executeLoad(context) {
71
- currentStale = false;
72
- staleNotify();
73
- currentStatus = "pending";
74
- currentValue = void 0;
75
- currentError = void 0;
76
- notify();
170
+ function executeLoad(context) {
171
+ const key = resolveKey(context);
172
+ const prevActiveKey = _activeKey;
173
+ _activeKey = key;
174
+ let entry = entries.get(key);
175
+ if (!entry) {
176
+ entry = createEntry();
177
+ entries.set(key, entry);
178
+ }
179
+ _clearEntryCacheTimer(entry);
180
+ if (shouldDeduplicate && entry.inFlightPromise) {
181
+ if (key !== prevActiveKey) syncFromEntry(entry);
182
+ return entry.inFlightPromise;
183
+ }
77
184
  if (context !== void 0) lastContext = context;
78
185
  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
- }
186
+ entry.stale = false;
187
+ entry.status = "pending";
188
+ entry.value = void 0;
189
+ entry.error = void 0;
190
+ syncFromActiveEntry();
191
+ const promise = (async () => {
192
+ try {
193
+ const result = await Promise.resolve(loader(loadContext));
194
+ if (entries.get(key) !== entry) return;
195
+ entry.value = result;
196
+ entry.status = "ready";
197
+ entry.stale = false;
198
+ _clearEntryCacheTimer(entry);
199
+ if (entry === getActiveEntry()) syncFromEntry(entry);
200
+ staleNotify();
201
+ _startEntryStaleTimer(entry, key);
202
+ } catch (e) {
203
+ if (entries.get(key) !== entry) return;
204
+ entry.error = e;
205
+ entry.status = "failed";
206
+ entry.stale = false;
207
+ if (entry === getActiveEntry()) syncFromEntry(entry);
208
+ staleNotify();
209
+ } finally {
210
+ entry.inFlightPromise = null;
211
+ }
212
+ })();
213
+ entry.inFlightPromise = promise;
214
+ return promise;
93
215
  }
94
216
  function invalidate() {
95
- if (!currentStale) {
96
- currentStale = true;
97
- staleNotify();
217
+ const entry = getActiveEntry();
218
+ if (!entry.stale) {
219
+ entry.stale = true;
220
+ _startEntryCacheTimer(entry, _activeKey);
221
+ if (entry === getActiveEntry()) {
222
+ currentStale = true;
223
+ staleNotify();
224
+ }
225
+ }
226
+ }
227
+ function dispose() {
228
+ for (const entry of entries.values()) {
229
+ _clearEntryStaleTimer(entry);
230
+ _clearEntryCacheTimer(entry);
231
+ entry.inFlightPromise = null;
98
232
  }
99
233
  }
100
234
  const node = {
@@ -127,7 +261,8 @@ function createResourceNode(id, name, loader, autoLoad = true) {
127
261
  invalidate,
128
262
  subscribe(fn) {
129
263
  return statusSignal.subscribe(fn);
130
- }
264
+ },
265
+ dispose
131
266
  };
132
267
  return node;
133
268
  }
@@ -351,6 +486,11 @@ function getSurfaces() {
351
486
  function getResources() {
352
487
  return resourceMap;
353
488
  }
489
+ function nextSuffix(baseId, exists) {
490
+ let counter = 2;
491
+ while (exists(`${baseId}-${counter}`)) counter++;
492
+ return `${baseId}-${counter}`;
493
+ }
354
494
 
355
495
  //#endregion
356
496
  //#region src/ask.ts
@@ -408,7 +548,9 @@ function computeAskError(node) {
408
548
  var AskBuilder = class {
409
549
  node;
410
550
  constructor(label, stateRef) {
411
- const id = `ask_${label.toLowerCase().replace(/\s+/g, "_")}`;
551
+ const baseId = `ask_${label.toLowerCase().replace(/\s+/g, "_")}`;
552
+ const existing = getAsks();
553
+ const id = existing.has(baseId) ? nextSuffix(baseId, (id$1) => existing.has(id$1)) : baseId;
412
554
  const onChange = stateRef.onChange;
413
555
  const subscribeToState = onChange ? (fn) => onChange((_v) => fn()) : void 0;
414
556
  this.node = createAskNode(id, label, stateRef, subscribeToState);
@@ -520,7 +662,9 @@ async function executeAct(node, context, notify) {
520
662
  var ActBuilder = class {
521
663
  node;
522
664
  constructor(label) {
523
- const id = `act_${label.toLowerCase().replace(/\s+/g, "_")}`;
665
+ const baseId = `act_${label.toLowerCase().replace(/\s+/g, "_")}`;
666
+ const existing = getActs();
667
+ const id = existing.has(baseId) ? nextSuffix(baseId, (id$1) => existing.has(id$1)) : baseId;
524
668
  this.node = createActNode(id, label, [], null, void 0, false);
525
669
  registerActNode(this.node);
526
670
  }
@@ -578,7 +722,9 @@ var ActBuilder = class {
578
722
  var FlowBuilder = class {
579
723
  node;
580
724
  constructor(name) {
581
- const id = `flow_${name}`;
725
+ const baseId = `flow_${name}`;
726
+ const existing = getFlows();
727
+ const id = existing.has(baseId) ? nextSuffix(baseId, (id$1) => existing.has(id$1)) : baseId;
582
728
  this.node = {
583
729
  id,
584
730
  name,
@@ -623,7 +769,9 @@ var FlowBuilder = class {
623
769
  var SurfaceBuilder = class {
624
770
  node;
625
771
  constructor(name) {
626
- const id = `surface_${name}`;
772
+ const baseId = `surface_${name}`;
773
+ const existing = getSurfaces();
774
+ const id = existing.has(baseId) ? nextSuffix(baseId, (id$1) => existing.has(id$1)) : baseId;
627
775
  this.node = {
628
776
  id,
629
777
  name,
@@ -662,13 +810,15 @@ function screen(name, fn) {
662
810
  flow: (n) => new FlowBuilder(n),
663
811
  surface: (n) => new SurfaceBuilder(n),
664
812
  resource: (n, config) => {
665
- const id = `resource_${n}`;
813
+ const baseId = `resource_${n}`;
814
+ const id = configs.some((c) => c.id === baseId) ? nextSuffix(baseId, (id$1) => configs.some((c) => c.id === id$1)) : baseId;
666
815
  const ref = new ResourceRef(id, n, config.load, config.autoLoad ?? true);
667
816
  configs.push({
668
817
  id,
669
818
  name: n,
670
819
  autoLoad: config.autoLoad ?? true,
671
820
  loader: config.load,
821
+ cache: config.cache,
672
822
  ref
673
823
  });
674
824
  return ref;
@@ -691,6 +841,28 @@ function screen(name, fn) {
691
841
 
692
842
  //#endregion
693
843
  //#region src/graph.ts
844
+ const NODE_KINDS = {
845
+ ask: "ask",
846
+ act: "action",
847
+ flow: "flow",
848
+ surface: "surface",
849
+ resource: "resource"
850
+ };
851
+ function slugify(text) {
852
+ return text.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
853
+ }
854
+ function createSemanticIdFactory(kind) {
855
+ const prefix = NODE_KINDS[kind] ?? kind;
856
+ const used = /* @__PURE__ */ new Map();
857
+ let unnamed = 0;
858
+ return (source) => {
859
+ const slug = slugify(source);
860
+ const base = slug.length > 0 ? slug : String(++unnamed);
861
+ const count = used.get(base) ?? 0;
862
+ used.set(base, count + 1);
863
+ return count === 0 ? `${prefix}:${base}` : `${prefix}:${base}-${count + 1}`;
864
+ };
865
+ }
694
866
  function computeDiagnostics(screenDef) {
695
867
  const diagnostics = [];
696
868
  const primaryActions = screenDef.acts.filter((a) => a.primary);
@@ -725,13 +897,66 @@ function computeDiagnostics(screenDef) {
725
897
  message: "Action is defined but not included in any surface.",
726
898
  nodeId: act.id
727
899
  });
900
+ if (screenDef.flows.length > 0) {
901
+ const flowNodeIds = /* @__PURE__ */ new Set();
902
+ for (const flow of screenDef.flows) for (const step of flow.steps) flowNodeIds.add(step.node.id);
903
+ for (const flow of screenDef.flows) for (const step of flow.steps) if (!surfacedNodeIds.has(step.node.id)) diagnostics.push({
904
+ severity: "warning",
905
+ code: "flow-step-not-surfaced",
906
+ message: `"${step.node.label}" is a flow step but not included in any surface.`,
907
+ nodeId: step.node.id,
908
+ flow: { flowNodeId: flow.id }
909
+ });
910
+ for (const flow of screenDef.flows) if (flow.steps.length > 0) {
911
+ const hasSurfacedStep = flow.steps.some((step) => surfacedNodeIds.has(step.node.id));
912
+ if (!hasSurfacedStep) diagnostics.push({
913
+ severity: "warning",
914
+ code: "orphaned-flow",
915
+ message: `"${flow.name}" has no surfaced steps.`,
916
+ flow: { flowNodeId: flow.id }
917
+ });
918
+ }
919
+ for (const ask of screenDef.asks) if (surfacedNodeIds.has(ask.id) && !flowNodeIds.has(ask.id)) diagnostics.push({
920
+ severity: "info",
921
+ code: "surfaced-node-not-in-any-flow",
922
+ message: `"${ask.label}" is surfaced but not referenced in any flow.`,
923
+ nodeId: ask.id
924
+ });
925
+ for (const act of screenDef.acts) if (surfacedNodeIds.has(act.id) && !flowNodeIds.has(act.id)) diagnostics.push({
926
+ severity: "info",
927
+ code: "surfaced-node-not-in-any-flow",
928
+ message: `"${act.label}" is surfaced but not referenced in any flow.`,
929
+ nodeId: act.id
930
+ });
931
+ }
728
932
  return diagnostics;
729
933
  }
730
934
  function inspectScreen(screenDef, runtimeResources) {
935
+ const diagnostics = computeDiagnostics(screenDef);
936
+ const askIds = createSemanticIdFactory("ask");
937
+ const actIds = createSemanticIdFactory("act");
938
+ const flowIds = createSemanticIdFactory("flow");
939
+ const surfaceIds = createSemanticIdFactory("surface");
940
+ const resourceIds = createSemanticIdFactory("resource");
941
+ const idToSemantic = /* @__PURE__ */ new Map();
942
+ for (const a of screenDef.asks) idToSemantic.set(a.id, askIds(a.label));
943
+ for (const a of screenDef.acts) idToSemantic.set(a.id, actIds(a.label));
944
+ const idToFlowSemantic = /* @__PURE__ */ new Map();
945
+ for (const f of screenDef.flows) idToFlowSemantic.set(f.id, flowIds(f.name));
946
+ const augmentedDiagnostics = diagnostics.map((d) => ({
947
+ ...d,
948
+ semanticNodeId: d.nodeId ? idToSemantic.get(d.nodeId) : void 0,
949
+ flow: d.flow ? {
950
+ ...d.flow,
951
+ flowSemanticNodeId: idToFlowSemantic.get(d.flow.flowNodeId)
952
+ } : void 0
953
+ }));
731
954
  return {
732
955
  name: screenDef.name,
956
+ semanticId: `screen:${slugify(screenDef.name)}`,
733
957
  asks: screenDef.asks.map((a) => ({
734
958
  id: a.id,
959
+ semanticId: idToSemantic.get(a.id),
735
960
  label: a.label,
736
961
  kind: a.kind,
737
962
  required: a.required,
@@ -741,6 +966,7 @@ function inspectScreen(screenDef, runtimeResources) {
741
966
  })),
742
967
  acts: screenDef.acts.map((a) => ({
743
968
  id: a.id,
969
+ semanticId: idToSemantic.get(a.id),
744
970
  label: a.label,
745
971
  primary: a.primary,
746
972
  enabled: a.enabled.current,
@@ -751,23 +977,26 @@ function inspectScreen(screenDef, runtimeResources) {
751
977
  })),
752
978
  flows: screenDef.flows.map((f) => ({
753
979
  id: f.id,
980
+ semanticId: idToFlowSemantic.get(f.id),
754
981
  name: f.name,
755
982
  stepCount: f.steps.length
756
983
  })),
757
984
  surfaces: screenDef.surfaces.map((s) => ({
758
985
  id: s.id,
986
+ semanticId: surfaceIds(s.name),
759
987
  name: s.name,
760
988
  itemCount: s.items.length
761
989
  })),
762
990
  resources: (runtimeResources ?? []).map((r) => ({
763
991
  id: r.id,
992
+ semanticId: resourceIds(r.name),
764
993
  name: r.name,
765
994
  status: r.status,
766
995
  hasValue: r.value !== void 0,
767
996
  stale: r.stale.current,
768
997
  error: r.status === "failed" ? r.error instanceof Error ? r.error.message : String(r.error) : void 0
769
998
  })),
770
- diagnostics: computeDiagnostics(screenDef)
999
+ diagnostics: augmentedDiagnostics
771
1000
  };
772
1001
  }
773
1002
 
@@ -814,7 +1043,7 @@ var ScreenRuntime = class {
814
1043
  this._started = true;
815
1044
  const nodeMap = /* @__PURE__ */ new Map();
816
1045
  for (const config of this._screen.resourceConfigs) {
817
- const node = createResourceNode(config.id, config.name, config.loader, false);
1046
+ const node = createResourceNode(config.id, config.name, config.loader, false, config.cache);
818
1047
  this._resourceNodes.push(node);
819
1048
  nodeMap.set(config.id, node);
820
1049
  }
@@ -836,6 +1065,7 @@ var ScreenRuntime = class {
836
1065
  this._disposed = true;
837
1066
  for (const unsub of this._unsubscribers) unsub();
838
1067
  this._unsubscribers = [];
1068
+ for (const node of this._resourceNodes) node.dispose();
839
1069
  for (const config of this._screen.resourceConfigs) if (config.ref && this._resourceNodeMap) {
840
1070
  const node = this._resourceNodeMap.get(config.id);
841
1071
  if (node) config.ref._disconnect(node);
@@ -23,3 +23,4 @@ export declare function getActs(): Map<string, ActNode<any>>;
23
23
  export declare function getFlows(): Map<string, FlowNode>;
24
24
  export declare function getSurfaces(): Map<string, SurfaceNode>;
25
25
  export declare function getResources(): Map<string, AnyResourceNode>;
26
+ export declare function nextSuffix(baseId: string, exists: (id: string) => boolean): string;
@@ -1,6 +1,7 @@
1
1
  import { type Condition } from "./signal.js";
2
2
  import type { ActionExecutionContext, DefaultScreenServices } from "./act.js";
3
3
  export type ResourceStatus = "idle" | "pending" | "ready" | "failed";
4
+ export type ResourceKey = string | number | boolean | null | undefined | ResourceKey[];
4
5
  export type ResourceLoadContext<TServices extends object = DefaultScreenServices> = ActionExecutionContext<TServices>;
5
6
  type ResourceLoader<TValue, TServices extends object> = (() => TValue | Promise<TValue>) | ((context: ResourceLoadContext<TServices>) => TValue | Promise<TValue>);
6
7
  export type ResourceNode<TValue, TServices extends object = DefaultScreenServices> = {
@@ -18,17 +19,25 @@ export type ResourceNode<TValue, TServices extends object = DefaultScreenService
18
19
  reload: (context?: ResourceLoadContext<TServices>) => Promise<void>;
19
20
  invalidate: () => void;
20
21
  subscribe: (fn: () => void) => () => void;
22
+ dispose: () => void;
21
23
  };
22
24
  export type AnyResourceNode = ResourceNode<unknown, any>;
25
+ export type ResourceCacheOptions<TServices extends object = DefaultScreenServices> = {
26
+ key?: (context: ResourceLoadContext<TServices>) => ResourceKey;
27
+ staleTime?: number;
28
+ cacheTime?: number;
29
+ deduplicate?: boolean;
30
+ };
23
31
  export type ResourceConfig<TValue = unknown, TServices extends object = DefaultScreenServices> = {
24
32
  id: string;
25
33
  name: string;
26
34
  autoLoad: boolean;
27
35
  loader: ResourceLoader<TValue, TServices>;
36
+ cache?: ResourceCacheOptions<TServices>;
28
37
  ref?: ResourceRef<TValue, TServices>;
29
38
  };
30
39
  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>;
40
+ export declare function createResourceNode<TValue, TServices extends object = DefaultScreenServices>(id: string, name: string, loader: ResourceLoader<TValue, TServices>, autoLoad?: boolean, cache?: ResourceCacheOptions<TServices>): ResourceNode<TValue, TServices>;
32
41
  export declare class ResourceRef<TValue, TServices extends object = DefaultScreenServices> {
33
42
  readonly id: string;
34
43
  readonly name: string;
package/dist/screen.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { AnyAskNode } from "./ask.js";
2
2
  import type { ActNode, DefaultScreenServices } from "./act.js";
3
3
  import type { FlowNode } from "./flow.js";
4
4
  import type { SurfaceNode } from "./surface.js";
5
- import type { ResourceConfig, ResourceLoadContext } from "./resource.js";
5
+ import type { ResourceCacheOptions, ResourceConfig, ResourceLoadContext } from "./resource.js";
6
6
  import { ResourceRef } from "./resource.js";
7
7
  import { type TextState, type BooleanState, type ChoiceState } from "./state.js";
8
8
  import { AskBuilder } from "./ask.js";
@@ -31,6 +31,7 @@ export type ScreenBuilder<TServices extends object = DefaultScreenServices> = {
31
31
  resource: <T>(name: string, config: {
32
32
  load: (() => Promise<T>) | ((context: ResourceLoadContext<TServices>) => Promise<T>);
33
33
  autoLoad?: boolean;
34
+ cache?: ResourceCacheOptions<TServices>;
34
35
  }) => ResourceRef<T, TServices>;
35
36
  };
36
37
  export type ScreenDefinition<TServices extends object = DefaultScreenServices> = {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.0-alpha.0",
6
+ "version": "0.1.0-alpha.10",
7
7
  "description": "Platformless semantic graph and runtime for Intent applications",
8
8
  "license": "MIT",
9
9
  "repository": {