akanjs 3.0.0-alpha.43 → 3.0.0-alpha.45

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.
@@ -81,6 +81,7 @@ export class FetchClient {
81
81
  readonly handler: Record<string, FetchHandler>;
82
82
  readonly slice: Record<string, SliceMeta> = {};
83
83
  readonly sortKeyMap = new Map<string, string[]>();
84
+ readonly #originWs = new Map<string, WsClient>();
84
85
  readonly #handlerStore: Record<string, FetchHandler> = {};
85
86
  readonly #handlerFactory = new Map<string, FetchHandlerFactory>();
86
87
  #sharedRegistryAppliedVersion = 0;
@@ -95,8 +96,7 @@ export class FetchClient {
95
96
  ) {
96
97
  this.origin = origin;
97
98
  this.http = new HttpClient(origin, ErrorCls);
98
- const wsUri = `${origin.replace("http://", "ws://").replace("https://", "wss://")}/ws`;
99
- this.ws = new WsClient(wsUri, ErrorCls);
99
+ this.ws = new WsClient(FetchClient.#makeWsUri(origin), ErrorCls);
100
100
  Object.assign(this.#handlerStore, handler);
101
101
  this.handler = this.#makeHandlerProxy();
102
102
  this.applySignal(serializedSignal);
@@ -158,6 +158,7 @@ export class FetchClient {
158
158
  this.ErrorCls = ErrorCls;
159
159
  this.http.setErrorConstructor(ErrorCls);
160
160
  this.ws.setErrorConstructor(ErrorCls);
161
+ for (const ws of this.#originWs.values()) ws.setErrorConstructor(ErrorCls);
161
162
  }
162
163
  applySignal(serializedSignal: { [key: string]: SerializedSignal }, { share = true }: { share?: boolean } = {}) {
163
164
  if (share && Object.keys(serializedSignal).length > 0) {
@@ -232,6 +233,8 @@ export class FetchClient {
232
233
  }
233
234
  disconnect() {
234
235
  this.ws.destroy();
236
+ for (const ws of this.#originWs.values()) ws.destroy();
237
+ this.#originWs.clear();
235
238
  }
236
239
  clone({ origin, connect = true, jwt }: { origin?: string; connect?: boolean; jwt?: string } = {}) {
237
240
  const instance = new FetchClient(origin ?? this.origin, {}, this.serializedSignal, this.ErrorCls);
@@ -245,6 +248,20 @@ export class FetchClient {
245
248
  setJwt(jwt: string | null) {
246
249
  this.jwt = jwt;
247
250
  this.ws.setJwt(jwt);
251
+ for (const ws of this.#originWs.values()) ws.setJwt(jwt);
252
+ }
253
+
254
+ #resolveWs(origin?: string) {
255
+ if (!origin) return this.ws;
256
+ const target = origin.replace(/\/+$/, "");
257
+ if (target === this.origin.replace(/\/+$/, "")) return this.ws;
258
+ const cached = this.#originWs.get(target);
259
+ if (cached) return cached;
260
+ const ws = new WsClient(FetchClient.#makeWsUri(target), this.ErrorCls);
261
+ this.#originWs.set(target, ws);
262
+ ws.setJwt(this.jwt);
263
+ ws.connect();
264
+ return ws;
248
265
  }
249
266
  #makeAuthHeaders(option?: FetchPolicy): Record<string, string> {
250
267
  if (option?.token) return { Authorization: `Bearer ${option.token}` };
@@ -338,13 +355,13 @@ export class FetchClient {
338
355
  handleEvent(parsedReturn);
339
356
  };
340
357
  wrappedListeners.set(handleEvent, wrapped);
341
- this.ws.subscribe({
358
+ const ws = this.#resolveWs(fetchPolicy?.origin);
359
+ ws.subscribe({
342
360
  key,
343
361
  data,
344
362
  handleEvent: wrapped,
345
363
  });
346
- return () =>
347
- this.ws.unsubscribe({ key, data, handleEvent: wrappedListeners.get(handleEvent) ?? handleEvent });
364
+ return () => ws.unsubscribe({ key, data, handleEvent: wrappedListeners.get(handleEvent) ?? handleEvent });
348
365
  };
349
366
  });
350
367
  return;
@@ -356,8 +373,9 @@ export class FetchClient {
356
373
  const serializerMap = this.#makeArgSerializer(endpoint.args);
357
374
  return (...argData: unknown[]) => {
358
375
  const args = argData.slice(0, msgArgLength);
376
+ const fetchPolicy = argData[msgArgLength] as FetchPolicy | undefined;
359
377
  const data = msgArgs.map((arg, idx) => serializerMap.get(arg.name)?.(args[idx]) ?? null);
360
- this.ws.emit(key, data);
378
+ this.#resolveWs(fetchPolicy?.origin).emit(key, data);
361
379
  };
362
380
  });
363
381
  this.#setHandlerFactory(`listen${capitalize(key)}`, () => {
@@ -369,8 +387,9 @@ export class FetchClient {
369
387
  handleEvent(parsedReturn);
370
388
  };
371
389
  wrappedListeners.set(handleEvent, wrapped);
372
- this.ws.on(key, wrapped);
373
- return () => this.ws.off(key, wrappedListeners.get(handleEvent) ?? handleEvent);
390
+ const ws = this.#resolveWs(fetchPolicy.origin);
391
+ ws.on(key, wrapped);
392
+ return () => ws.off(key, wrappedListeners.get(handleEvent) ?? handleEvent);
374
393
  }) as FetchHandler;
375
394
  });
376
395
  return;
@@ -380,6 +399,10 @@ export class FetchClient {
380
399
  break;
381
400
  }
382
401
  }
402
+ static #makeWsUri(origin: string) {
403
+ return `${origin.replace("http://", "ws://").replace("https://", "wss://")}/ws`;
404
+ }
405
+
383
406
  static paginationArgs: SerializedArg[] = [
384
407
  { type: "search", name: "skip", refName: "Int" },
385
408
  { type: "search", name: "limit", refName: "Int" },
@@ -41,6 +41,7 @@ export class WsClient {
41
41
  #listenerMap = new Map<string, Set<Listener>>();
42
42
  #destroyed = false;
43
43
  #connectRequested = false;
44
+ #outbox: string[] = [];
44
45
  #unconnectedWarnTimers = new Map<string, ReturnType<typeof setTimeout>>();
45
46
  #jwt: string | null = null;
46
47
  connected = false;
@@ -94,6 +95,9 @@ export class WsClient {
94
95
  const data: WebsocketReqData = { key: option.key, data: option.data, subscribe: true };
95
96
  this.#ws?.send(JSON.stringify(data));
96
97
  });
98
+ const queued = this.#outbox;
99
+ this.#outbox = [];
100
+ for (const frame of queued) this.#ws?.send(frame);
97
101
  };
98
102
  this.#ws.onmessage = (e) => {
99
103
  try {
@@ -197,6 +201,7 @@ export class WsClient {
197
201
  }
198
202
  for (const timer of this.#unconnectedWarnTimers.values()) clearTimeout(timer);
199
203
  this.#unconnectedWarnTimers.clear();
204
+ this.#outbox = [];
200
205
  this.#ws?.close();
201
206
  this.#ws = null;
202
207
  }
@@ -240,28 +245,31 @@ export class WsClient {
240
245
  `[akanjs] WebSocket is not connected. Call fetch.instance.connect(), or drop the root layout "wsConnect = false", before ${action} "${key}".`,
241
246
  );
242
247
  }
243
- #warnUnconnectedSubscribe(key: string) {
244
- if (this.#connectRequested || this.#unconnectedWarnTimers.has(key)) return;
248
+ #warnUnconnected(action: "emit" | "subscribe", key: string) {
249
+ const timerKey = `${action}:${key}`;
250
+ if (this.#connectRequested || this.#unconnectedWarnTimers.has(timerKey)) return;
245
251
  const timer = setTimeout(() => {
246
- this.#unconnectedWarnTimers.delete(key);
252
+ this.#unconnectedWarnTimers.delete(timerKey);
247
253
  if (this.#connectRequested || this.#destroyed) return;
248
- this.#warnNotConnected("subscribe", key);
254
+ this.#warnNotConnected(action, key);
249
255
  }, 0);
250
- this.#unconnectedWarnTimers.set(key, timer);
256
+ this.#unconnectedWarnTimers.set(timerKey, timer);
251
257
  }
252
258
  emit(key: string, data: WsRequestPayload) {
259
+ const payload: WebsocketReqData = { key, data: Array.isArray(data) ? data : [data] };
260
+ const frame = JSON.stringify(payload);
261
+
253
262
  if (this.#ws?.readyState !== WebSocket.OPEN) {
254
- this.logger.warn("WebSocket not connected");
255
- this.#warnNotConnected("emit", key);
263
+ this.#outbox.push(frame);
264
+ this.#warnUnconnected("emit", key);
256
265
  return this;
257
266
  }
258
- const payload: WebsocketReqData = { key, data: Array.isArray(data) ? data : [data] };
259
- this.#ws.send(JSON.stringify(payload));
267
+ this.#ws.send(frame);
260
268
  return this;
261
269
  }
262
270
  subscribe(option: { key: string; data: unknown[]; handleEvent: (data: unknown) => void }) {
263
271
  const roomId = WsClient.makeRoomId(option.key, option.data);
264
- if (!this.#ws) this.#warnUnconnectedSubscribe(option.key);
272
+ if (!this.#ws) this.#warnUnconnected("subscribe", option.key);
265
273
  if (!this.#roomSubscribeMap.has(roomId)) {
266
274
  this.#roomSubscribeMap.set(roomId, { key: option.key, data: option.data, listener: new Set() });
267
275
  if (this.#ws?.readyState === WebSocket.OPEN) {
@@ -44,7 +44,7 @@ type QueryOrMutationFetchFn<E, SlceCls extends SliceCls | never> = (
44
44
  /** Typed off `PromptResult` rather than the endpoint's return ref, which is the `Any` carrier a prompt rides on. */
45
45
  type PromptFetchFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => Promise<PromptMessage[]>;
46
46
 
47
- type MessageEmitFn<E> = (...args: EndpInfoArgs<E>) => void;
47
+ type MessageEmitFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => void;
48
48
 
49
49
  type MessageListenFn<E, SlceCls extends SliceCls | never> = (
50
50
  handleEvent: (data: EndpInfoReturns<E, SlceCls>) => PromiseOrObject<void>,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.43",
3
+ "version": "3.0.0-alpha.45",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -18,6 +18,7 @@ import {
18
18
  type DocumentUpdateOptions,
19
19
  documentQueryHelper,
20
20
  encodeDocumentValue,
21
+ isDocumentId,
21
22
  isDocumentUpdateNode,
22
23
  NoDocumentError,
23
24
  resolveDocumentUpdate,
@@ -1033,6 +1034,7 @@ export class SqlDocumentStore {
1033
1034
  async create(data: DocumentRecord, { runSaveHooks = true }: WriteHookOptions = {}) {
1034
1035
  const now = Date.now();
1035
1036
  const id = data.id ?? createDocumentId(now);
1037
+ if (!isDocumentId(id)) throw new Error(`Invalid ID value: ${id}`);
1036
1038
  const doc = this.hydrate(
1037
1039
  this.prepareDocument({
1038
1040
  ...data,
@@ -4,7 +4,6 @@ import { AgentBridge } from "./AgentBridge";
4
4
  import { ScreenReader } from "./ScreenReader";
5
5
  import { ScreenSettle } from "./ScreenSettle";
6
6
  import { ScreenTarget } from "./ScreenTarget";
7
- import { StateWait } from "./StateWait";
8
7
 
9
8
  /**
10
9
  * The tools every akan screen has whatever it declares: where it can go, what it is rendering, what one of the
@@ -39,7 +38,6 @@ export class StoreSurfaceSource implements SurfaceSource {
39
38
  StoreSurfaceSource.#goBack(),
40
39
  StoreSurfaceSource.#readScreen(viewKey),
41
40
  this.#readState(viewKey),
42
- this.#waitFor(viewKey),
43
41
  StoreSurfaceSource.#highlight(viewKey),
44
42
  ];
45
43
  this.#builtins.set(viewKey, builtins);
@@ -192,65 +190,6 @@ export class StoreSurfaceSource implements SurfaceSource {
192
190
  };
193
191
  }
194
192
 
195
- /**
196
- * The wait that costs no model turns.
197
- *
198
- * An agent that started something slow has one way to learn it finished: ask again, and again, a full round trip
199
- * per look. That burns the turn budget in seconds and reads to the user as a loop. This parks the call instead —
200
- * the session already awaits `run`, so the turn simply takes as long as the work does, and the change report
201
- * that follows carries whatever landed while it waited.
202
- *
203
- * It watches a published state key rather than sleeping for a fixed span. A bare sleep would only make the
204
- * polling slower, and a screen that can report progress at all reports it into the store; a screen that reports
205
- * none publishes a key with `st.expose`, which is worth doing anyway, since the key is then readable too.
206
- */
207
- #waitFor(viewKey: string): ToolEntry {
208
- return {
209
- name: "waitFor",
210
- description:
211
- "Wait here until one of this page's state keys settles, instead of asking again and again. Use it after starting something slow — a generation, an upload, a long job — whenever a state key reports how it is going: the turn pauses with no model round trip and resumes the moment the key moves. Keys are listed in the state context block. Returns what the key holds when the wait ends.",
212
- parameters: {
213
- type: "object",
214
- properties: {
215
- key: { type: "string", description: "The state key to watch, spelled as the state context block lists it." },
216
- equals: {
217
- type: "string",
218
- description:
219
- "Wait until the key reads exactly this, compared as text. Omit to wait until it changes from whatever it holds now.",
220
- },
221
- timeoutSeconds: {
222
- type: "number",
223
- description: `How long to wait before answering with the key's current value. Default ${StateWait.defaultSeconds}, maximum ${StateWait.maxSeconds}. Running out is not a failure — call waitFor again to keep waiting.`,
224
- },
225
- },
226
- required: ["key"],
227
- additionalProperties: false,
228
- },
229
-
230
- effect: "state",
231
- run: async (args) => {
232
- this.#bridge ??= AgentBridge.of();
233
- const key = typeof args.key === "string" ? args.key.trim() : "";
234
- if (!key) throw new Error("waitFor needs a key to watch.");
235
- const live = this.#bridge.readableKeys(viewKey);
236
- if (!live.includes(key))
237
- throw new Error(
238
- `No state key named ${key} is read by this screen. ${
239
- live.length
240
- ? `Keys here: ${live.join(", ")}.`
241
- : "This screen reads no state keys, so there is nothing to wait for."
242
- }`,
243
- );
244
- return await new StateWait(this.#bridge, {
245
- key,
246
- viewKey,
247
- equals: typeof args.equals === "string" ? args.equals : null,
248
- seconds: args.timeoutSeconds,
249
- }).run();
250
- },
251
- };
252
- }
253
-
254
193
  /**
255
194
  * The ring goes on once the scroll lands, not when it starts: a smooth scroll across a long page takes most of a
256
195
  * second, and a flash begun at the top is already fading by the time the user's eye arrives. Settles on the
@@ -7,7 +7,6 @@ export * from "./AgentPrompts";
7
7
  export * from "./ScreenReader";
8
8
  export * from "./ScreenSettle";
9
9
  export * from "./ScreenTarget";
10
- export * from "./StateWait";
11
10
  export * from "./StoreCatalogue";
12
11
  export * from "./StoreSurfaceSource";
13
12
  export * from "./storeSurface";
@@ -7,7 +7,7 @@ type EndpInfoReturns<E, SlceCls extends SliceCls | never> = EndpointClientReturn
7
7
  type QueryOrMutationFetchFn<E, SlceCls extends SliceCls | never> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => Promise<EndpInfoReturns<E, SlceCls>>;
8
8
  /** Typed off `PromptResult` rather than the endpoint's return ref, which is the `Any` carrier a prompt rides on. */
9
9
  type PromptFetchFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => Promise<PromptMessage[]>;
10
- type MessageEmitFn<E> = (...args: EndpInfoArgs<E>) => void;
10
+ type MessageEmitFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => void;
11
11
  type MessageListenFn<E, SlceCls extends SliceCls | never> = (handleEvent: (data: EndpInfoReturns<E, SlceCls>) => PromiseOrObject<void>, options?: FetchPolicy) => () => void;
12
12
  type PubsubSubscribeFn<E, SlceCls extends SliceCls | never> = (...args: [
13
13
  ...EndpInfoArgs<E>,
@@ -5,7 +5,6 @@ export * from "./AgentPrompts.d.ts";
5
5
  export * from "./ScreenReader.d.ts";
6
6
  export * from "./ScreenSettle.d.ts";
7
7
  export * from "./ScreenTarget.d.ts";
8
- export * from "./StateWait.d.ts";
9
8
  export * from "./StoreCatalogue.d.ts";
10
9
  export * from "./StoreSurfaceSource.d.ts";
11
10
  export * from "./storeSurface.d.ts";
@@ -18,8 +18,8 @@ const asked = (messages: readonly ChatMessage[], cap: number) =>
18
18
  export const useDraftRecall = (messages: readonly ChatMessage[]) => {
19
19
  const cap = 30;
20
20
  const sent = useRef<string[] | null>(null);
21
-
22
- const walked = (sent.current ??= asked(messages, cap));
21
+ if (!sent.current) sent.current = asked(messages, cap);
22
+ const walked = sent.current;
23
23
  const stashed = useRef("");
24
24
  const [at, setAt] = useState(0);
25
25
  return {
@@ -1,139 +0,0 @@
1
- import { AgentAbort, AgentProgress } from "../../vendor/use-agentic";
2
-
3
- /** The reading half of the store an in-page wait needs. `AgentBridge` is one; a test can be another. */
4
- export interface StateSource {
5
- read(key: string, viewKey?: string): unknown;
6
- subscribe(listener: () => void): () => void;
7
- }
8
-
9
- export interface StateWaitOptions {
10
- key: string;
11
- viewKey?: string;
12
- /** Settle when the key reads exactly this. Null waits for it to change from whatever it holds now. */
13
- equals?: string | null;
14
- /** Straight off the tool argument, so it may be anything; `StateWait.seconds` is what makes it a number. */
15
- seconds?: unknown;
16
- }
17
-
18
- /**
19
- * One `waitFor` call: park until a published state key settles, or until the timeout says how it is going.
20
- *
21
- * Two clocks, because neither covers the other. The store's own subscription catches the value changing, which is
22
- * the whole point of the tool and has to land immediately. The tick catches what the store never announces —
23
- * `retainLive` / `releaseLive` mutate the live-key map without notifying any listener, so a page navigated away
24
- * from mid-wait would otherwise hold the turn until the timeout — and the countdown row needs a tick anyway.
25
- *
26
- * Nothing here throws on a wait that ran out. A key still reading `generating` after two minutes is an answer, not
27
- * a failure, and the model is the one that decides whether to wait again.
28
- */
29
- export class StateWait {
30
- static readonly tickMs = 1000;
31
- static readonly defaultSeconds = 120;
32
- static readonly maxSeconds = 600;
33
-
34
- /** Clamped rather than refused: a model that asks for an hour gets the longest wait on offer and reads how long. */
35
- static seconds(value: unknown): number {
36
- if (typeof value !== "number" || !Number.isFinite(value)) return StateWait.defaultSeconds;
37
- return Math.min(Math.max(Math.round(value), 1), StateWait.maxSeconds);
38
- }
39
-
40
- /** A string is itself; everything else is JSON, so a number, a boolean and null each compare as they are written. */
41
- static print(value: unknown): string {
42
- if (typeof value === "string") return value;
43
- try {
44
- return JSON.stringify(value) ?? String(value);
45
- } catch {
46
- return String(value);
47
- }
48
- }
49
-
50
- readonly #source: StateSource;
51
- readonly #key: string;
52
- readonly #viewKey: string;
53
- readonly #equals: string | null;
54
- readonly #seconds: number;
55
- #was = "";
56
- #now = "";
57
- #elapsed = 0;
58
- #settled: boolean = false;
59
- #ticker: ReturnType<typeof setInterval> | undefined;
60
- #unsubscribe: (() => void) | undefined;
61
- #signal: AbortSignal | null = null;
62
- #resolve: ((message: string) => void) | null = null;
63
- #reject: ((error: Error) => void) | null = null;
64
-
65
- constructor(source: StateSource, { key, viewKey = "", equals = null, seconds }: StateWaitOptions) {
66
- this.#source = source;
67
- this.#key = key;
68
- this.#viewKey = viewKey;
69
- this.#equals = equals;
70
- this.#seconds = StateWait.seconds(seconds);
71
- }
72
-
73
- run(): Promise<string> {
74
- this.#was = StateWait.print(this.#source.read(this.#key, this.#viewKey));
75
- this.#now = this.#was;
76
- if (this.#equals !== null && this.#was === this.#equals)
77
- return Promise.resolve(`${this.#key} is already ${this.#was}.`);
78
- return new Promise<string>((resolve, reject) => {
79
- this.#resolve = resolve;
80
- this.#reject = reject;
81
- this.#signal = AgentAbort.current;
82
- if (this.#signal?.aborted) {
83
- this.#abort();
84
- return;
85
- }
86
- this.#signal?.addEventListener("abort", this.#abort);
87
- this.#unsubscribe = this.#source.subscribe(this.#check);
88
- this.#ticker = setInterval(this.#tick, StateWait.tickMs);
89
- AgentProgress.report("", { done: 0, total: this.#seconds });
90
- });
91
- }
92
-
93
- /** The session races every call against the same signal; honouring it here is what stops the timer. */
94
- #abort = () => {
95
- this.#stop();
96
- this.#reject?.(new Error("The user aborted the turn."));
97
- };
98
-
99
- #check = () => {
100
- if (this.#settled) return;
101
- let now: string;
102
- try {
103
- now = StateWait.print(this.#source.read(this.#key, this.#viewKey));
104
- } catch (error) {
105
-
106
- this.#end(`Stopped waiting: ${error instanceof Error ? error.message : String(error)}`);
107
- return;
108
- }
109
- this.#now = now;
110
- if (this.#equals === null ? now !== this.#was : now === this.#equals) this.#end(`${this.#key} is now ${now}.`);
111
- };
112
-
113
- #tick = () => {
114
- this.#check();
115
- if (this.#settled) return;
116
- this.#elapsed += 1;
117
- if (this.#elapsed >= this.#seconds) {
118
- this.#end(
119
- `${this.#key} is still ${this.#now} after ${this.#seconds}s. Call waitFor again to keep waiting, or tell the user it is taking longer than expected.`,
120
- );
121
- return;
122
- }
123
- AgentProgress.report("", { done: this.#elapsed, total: this.#seconds });
124
- };
125
-
126
- #end(message: string) {
127
- this.#stop();
128
- this.#resolve?.(message);
129
- }
130
-
131
- #stop() {
132
- this.#settled = true;
133
- clearInterval(this.#ticker);
134
- this.#ticker = undefined;
135
- this.#unsubscribe?.();
136
- this.#unsubscribe = undefined;
137
- this.#signal?.removeEventListener("abort", this.#abort);
138
- }
139
- }
@@ -1,36 +0,0 @@
1
- /** The reading half of the store an in-page wait needs. `AgentBridge` is one; a test can be another. */
2
- export interface StateSource {
3
- read(key: string, viewKey?: string): unknown;
4
- subscribe(listener: () => void): () => void;
5
- }
6
- export interface StateWaitOptions {
7
- key: string;
8
- viewKey?: string;
9
- /** Settle when the key reads exactly this. Null waits for it to change from whatever it holds now. */
10
- equals?: string | null;
11
- /** Straight off the tool argument, so it may be anything; `StateWait.seconds` is what makes it a number. */
12
- seconds?: unknown;
13
- }
14
- /**
15
- * One `waitFor` call: park until a published state key settles, or until the timeout says how it is going.
16
- *
17
- * Two clocks, because neither covers the other. The store's own subscription catches the value changing, which is
18
- * the whole point of the tool and has to land immediately. The tick catches what the store never announces —
19
- * `retainLive` / `releaseLive` mutate the live-key map without notifying any listener, so a page navigated away
20
- * from mid-wait would otherwise hold the turn until the timeout — and the countdown row needs a tick anyway.
21
- *
22
- * Nothing here throws on a wait that ran out. A key still reading `generating` after two minutes is an answer, not
23
- * a failure, and the model is the one that decides whether to wait again.
24
- */
25
- export declare class StateWait {
26
- #private;
27
- static readonly tickMs = 1000;
28
- static readonly defaultSeconds = 120;
29
- static readonly maxSeconds = 600;
30
- /** Clamped rather than refused: a model that asks for an hour gets the longest wait on offer and reads how long. */
31
- static seconds(value: unknown): number;
32
- /** A string is itself; everything else is JSON, so a number, a boolean and null each compare as they are written. */
33
- static print(value: unknown): string;
34
- constructor(source: StateSource, { key, viewKey, equals, seconds }: StateWaitOptions);
35
- run(): Promise<string>;
36
- }