@intent-framework/core 0.1.0-alpha.7 → 0.1.0-alpha.9

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/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ 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,24 @@ 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
+ inFlightPromise: null
53
+ };
54
+ }
55
+ const entries = /* @__PURE__ */ new Map();
56
+ let _activeKey = DEFAULT_KEY;
57
+ if (!hasKey) entries.set(DEFAULT_KEY, createEntry());
43
58
  let currentStatus = "idle";
44
59
  let currentValue = void 0;
45
60
  let currentError = void 0;
@@ -47,10 +62,74 @@ function createResourceNode(id, name, loader, autoLoad = true) {
47
62
  let lastContext = void 0;
48
63
  const notify = () => statusSignal.set(statusSignal.get() + 1);
49
64
  const staleNotify = () => staleSignal.set(staleSignal.get() + 1);
65
+ const shouldDeduplicate = cache ? cache.deduplicate !== false : false;
50
66
  let _ready;
51
67
  let _pending;
52
68
  let _failed;
53
69
  let _stale;
70
+ function getActiveEntry() {
71
+ let entry = entries.get(_activeKey);
72
+ if (!entry) {
73
+ entry = createEntry();
74
+ entries.set(_activeKey, entry);
75
+ }
76
+ return entry;
77
+ }
78
+ function syncFromEntry(entry) {
79
+ currentStatus = entry.status;
80
+ currentValue = entry.value;
81
+ currentError = entry.error;
82
+ if (currentStale !== entry.stale) {
83
+ currentStale = entry.stale;
84
+ staleNotify();
85
+ }
86
+ notify();
87
+ }
88
+ function syncFromActiveEntry() {
89
+ syncFromEntry(getActiveEntry());
90
+ }
91
+ function encodeResourceKey(key) {
92
+ if (Array.isArray(key)) return ["array", key.map(encodeResourceKey)];
93
+ if (key === null) return ["null"];
94
+ if (key === void 0) return ["undefined"];
95
+ if (typeof key === "string") return ["string", key];
96
+ if (typeof key === "boolean") return ["boolean", key];
97
+ if (typeof key === "number") {
98
+ if (Number.isNaN(key)) return ["number", "NaN"];
99
+ if (key === Infinity) return ["number", "Infinity"];
100
+ if (key === -Infinity) return ["number", "-Infinity"];
101
+ if (Object.is(key, -0)) return ["number", "-0"];
102
+ return ["number", key];
103
+ }
104
+ const exhaustive = key;
105
+ return exhaustive;
106
+ }
107
+ function normalizeKey(key) {
108
+ return JSON.stringify(encodeResourceKey(key));
109
+ }
110
+ function resolveKey(ctx) {
111
+ if (!hasKey) return DEFAULT_KEY;
112
+ const context = ctx ?? lastContext ?? {};
113
+ return normalizeKey(cache.key(context));
114
+ }
115
+ function _clearEntryStaleTimer(entry) {
116
+ if (entry.staleTimer != null) {
117
+ clearTimeout(entry.staleTimer);
118
+ entry.staleTimer = null;
119
+ }
120
+ }
121
+ function _startEntryStaleTimer(entry) {
122
+ _clearEntryStaleTimer(entry);
123
+ if (cache?.staleTime != null && isFinite(cache.staleTime)) entry.staleTimer = setTimeout(() => {
124
+ if (!entry.stale) {
125
+ entry.stale = true;
126
+ if (entry === getActiveEntry()) {
127
+ currentStale = true;
128
+ staleNotify();
129
+ }
130
+ }
131
+ }, cache.staleTime);
132
+ }
54
133
  function getReady() {
55
134
  if (!_ready) _ready = createCondition(() => currentStatus === "ready", (notify$1) => statusSignal.subscribe(() => notify$1()));
56
135
  return _ready;
@@ -67,34 +146,58 @@ function createResourceNode(id, name, loader, autoLoad = true) {
67
146
  if (!_stale) _stale = createCondition(() => currentStale, (notify$1) => staleSignal.subscribe(() => notify$1()));
68
147
  return _stale;
69
148
  }
70
- async function executeLoad(context) {
71
- currentStale = false;
72
- staleNotify();
73
- currentStatus = "pending";
74
- currentValue = void 0;
75
- currentError = void 0;
76
- notify();
149
+ function executeLoad(context) {
150
+ const key = resolveKey(context);
151
+ _activeKey = key;
152
+ let entry = entries.get(key);
153
+ if (!entry) {
154
+ entry = createEntry();
155
+ entries.set(key, entry);
156
+ }
157
+ if (shouldDeduplicate && entry.inFlightPromise) return entry.inFlightPromise;
77
158
  if (context !== void 0) lastContext = context;
78
159
  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
- }
160
+ entry.stale = false;
161
+ entry.status = "pending";
162
+ entry.value = void 0;
163
+ entry.error = void 0;
164
+ syncFromActiveEntry();
165
+ const promise = (async () => {
166
+ try {
167
+ const result = await Promise.resolve(loader(loadContext));
168
+ entry.value = result;
169
+ entry.status = "ready";
170
+ entry.stale = false;
171
+ if (entry === getActiveEntry()) syncFromEntry(entry);
172
+ staleNotify();
173
+ _startEntryStaleTimer(entry);
174
+ } catch (e) {
175
+ entry.error = e;
176
+ entry.status = "failed";
177
+ entry.stale = false;
178
+ if (entry === getActiveEntry()) syncFromEntry(entry);
179
+ staleNotify();
180
+ } finally {
181
+ entry.inFlightPromise = null;
182
+ }
183
+ })();
184
+ entry.inFlightPromise = promise;
185
+ return promise;
93
186
  }
94
187
  function invalidate() {
95
- if (!currentStale) {
96
- currentStale = true;
97
- staleNotify();
188
+ const entry = getActiveEntry();
189
+ if (!entry.stale) {
190
+ entry.stale = true;
191
+ if (entry === getActiveEntry()) {
192
+ currentStale = true;
193
+ staleNotify();
194
+ }
195
+ }
196
+ }
197
+ function dispose() {
198
+ for (const entry of entries.values()) {
199
+ _clearEntryStaleTimer(entry);
200
+ entry.inFlightPromise = null;
98
201
  }
99
202
  }
100
203
  const node = {
@@ -127,7 +230,8 @@ function createResourceNode(id, name, loader, autoLoad = true) {
127
230
  invalidate,
128
231
  subscribe(fn) {
129
232
  return statusSignal.subscribe(fn);
130
- }
233
+ },
234
+ dispose
131
235
  };
132
236
  return node;
133
237
  }
@@ -683,6 +787,7 @@ function screen(name, fn) {
683
787
  name: n,
684
788
  autoLoad: config.autoLoad ?? true,
685
789
  loader: config.load,
790
+ cache: config.cache,
686
791
  ref
687
792
  });
688
793
  return ref;
@@ -907,7 +1012,7 @@ var ScreenRuntime = class {
907
1012
  this._started = true;
908
1013
  const nodeMap = /* @__PURE__ */ new Map();
909
1014
  for (const config of this._screen.resourceConfigs) {
910
- const node = createResourceNode(config.id, config.name, config.loader, false);
1015
+ const node = createResourceNode(config.id, config.name, config.loader, false, config.cache);
911
1016
  this._resourceNodes.push(node);
912
1017
  nodeMap.set(config.id, node);
913
1018
  }
@@ -929,6 +1034,7 @@ var ScreenRuntime = class {
929
1034
  this._disposed = true;
930
1035
  for (const unsub of this._unsubscribers) unsub();
931
1036
  this._unsubscribers = [];
1037
+ for (const node of this._resourceNodes) node.dispose();
932
1038
  for (const config of this._screen.resourceConfigs) if (config.ref && this._resourceNodeMap) {
933
1039
  const node = this._resourceNodeMap.get(config.id);
934
1040
  if (node) config.ref._disconnect(node);
@@ -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,24 @@ 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
+ deduplicate?: boolean;
29
+ };
23
30
  export type ResourceConfig<TValue = unknown, TServices extends object = DefaultScreenServices> = {
24
31
  id: string;
25
32
  name: string;
26
33
  autoLoad: boolean;
27
34
  loader: ResourceLoader<TValue, TServices>;
35
+ cache?: ResourceCacheOptions<TServices>;
28
36
  ref?: ResourceRef<TValue, TServices>;
29
37
  };
30
38
  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>;
39
+ 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
40
  export declare class ResourceRef<TValue, TServices extends object = DefaultScreenServices> {
33
41
  readonly id: string;
34
42
  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.7",
6
+ "version": "0.1.0-alpha.9",
7
7
  "description": "Platformless semantic graph and runtime for Intent applications",
8
8
  "license": "MIT",
9
9
  "repository": {