@qilin-agent/client-ui-session 0.0.0-stage → 3.1.4
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/README.i18n.yaml +28 -0
- package/README.md +44 -2
- package/README.zh.md +45 -0
- package/lib/client.js +1134 -0
- package/lib/index.js +6 -0
- package/lib/types/client/index.d.ts +192 -0
- package/lib/types/client/session-provider.d.ts +11 -0
- package/lib/types/index.d.ts +4 -0
- package/package.json +63 -3
package/lib/client.js
ADDED
|
@@ -0,0 +1,1134 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@qilin-agent/client-ui-session",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _qilin_agent_kylin = require("@qilin-agent/kylin");
|
|
8
|
+
let _qilin_agent_client_store = require("@qilin-agent/client-store");
|
|
9
|
+
let _qilin_agent_client_ui_slots = require("@qilin-agent/client-ui-slots");
|
|
10
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
|
+
//#region ../../util/values/src/partial-json.ts
|
|
12
|
+
/**
|
|
13
|
+
* Lazily scanned view of one JSON object's top-level fields, built from text
|
|
14
|
+
* that may still be streaming or from an already parsed object. Nothing is
|
|
15
|
+
* scanned until a reader asks; the view remembers every question it answered
|
|
16
|
+
* and reports changed answers when the owner refreshes for publication.
|
|
17
|
+
* Used for model tool-call arguments: a row reads the fields it
|
|
18
|
+
* cares about at whatever granularity it displays, at every stage of the call.
|
|
19
|
+
* @module @qilin-agent/util-values/src/partial-json
|
|
20
|
+
*/
|
|
21
|
+
const SIMPLE_ESCAPES = {
|
|
22
|
+
"\"": "\"",
|
|
23
|
+
"\\": "\\",
|
|
24
|
+
"/": "/",
|
|
25
|
+
b: "\b",
|
|
26
|
+
f: "\f",
|
|
27
|
+
n: "\n",
|
|
28
|
+
r: "\r",
|
|
29
|
+
t: " "
|
|
30
|
+
};
|
|
31
|
+
const CONTENT_ESCAPE = /[\\\u0000-\u001f]/u;
|
|
32
|
+
function isWhitespace(c) {
|
|
33
|
+
return c === " " || c === "\n" || c === "\r" || c === " ";
|
|
34
|
+
}
|
|
35
|
+
function isHex(c) {
|
|
36
|
+
return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
|
|
37
|
+
}
|
|
38
|
+
(class PartialArguments {
|
|
39
|
+
/** The view of a call with no arguments available. */
|
|
40
|
+
static EMPTY = PartialArguments.fromObject({});
|
|
41
|
+
/**
|
|
42
|
+
* View finished argument text without scanning it until a reader asks.
|
|
43
|
+
* @param text - the complete argument JSON text.
|
|
44
|
+
* @returns a sealed view.
|
|
45
|
+
*/
|
|
46
|
+
static fromText(text) {
|
|
47
|
+
const view = new PartialArguments();
|
|
48
|
+
view.append(text);
|
|
49
|
+
view.sealed = true;
|
|
50
|
+
return view;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* View an already parsed argument payload, such as a PTC dispatch object.
|
|
54
|
+
* @param value - the parsed argument value.
|
|
55
|
+
* @returns a sealed view; a non-object payload has no fields.
|
|
56
|
+
*/
|
|
57
|
+
static fromObject(value) {
|
|
58
|
+
const view = new PartialArguments();
|
|
59
|
+
view.object = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
60
|
+
view.sealed = true;
|
|
61
|
+
return view;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The source: text so far or a parsed object, plus whether it can still grow.
|
|
65
|
+
* These are the only enumerable fields, so two views over the same source
|
|
66
|
+
* compare equal structurally however far each has been read.
|
|
67
|
+
*/
|
|
68
|
+
chunks = [];
|
|
69
|
+
object;
|
|
70
|
+
sealed = false;
|
|
71
|
+
#ends = [];
|
|
72
|
+
#size = 0;
|
|
73
|
+
#consumed = 0;
|
|
74
|
+
#mode = "root";
|
|
75
|
+
#escape = false;
|
|
76
|
+
#keyStart = 0;
|
|
77
|
+
#keyEscaped = false;
|
|
78
|
+
#key = "";
|
|
79
|
+
#current = null;
|
|
80
|
+
#nestedEnds = [];
|
|
81
|
+
#nestedInString = false;
|
|
82
|
+
#invalidAt;
|
|
83
|
+
#invalidValue = false;
|
|
84
|
+
#entries = /* @__PURE__ */ new Map();
|
|
85
|
+
#order = [];
|
|
86
|
+
#reads = /* @__PURE__ */ new Map();
|
|
87
|
+
/** Whether this view rejects further appends; does not scan text or register reads. */
|
|
88
|
+
get isSealed() {
|
|
89
|
+
return this.sealed;
|
|
90
|
+
}
|
|
91
|
+
/** Whether indexing or a content read found invalid JSON; unread value contents are not validated. */
|
|
92
|
+
get invalid() {
|
|
93
|
+
this.scan();
|
|
94
|
+
return this.#mode === "invalid" || this.#invalidValue;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Retain streamed argument text without scanning or comparing observed answers.
|
|
98
|
+
* @param fragment - the text following every fragment appended before.
|
|
99
|
+
*/
|
|
100
|
+
append(fragment) {
|
|
101
|
+
if (this.sealed) throw new Error("PartialArguments: cannot append to a sealed view");
|
|
102
|
+
if (fragment.length === 0) return;
|
|
103
|
+
this.chunks.push(fragment);
|
|
104
|
+
this.#size += fragment.length;
|
|
105
|
+
this.#ends.push(this.#size);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Reconcile a streamed prefix with authoritative complete text without joining the fragments.
|
|
109
|
+
* @param text - the final argument text, which replaces missing or conflicting deltas.
|
|
110
|
+
* @returns this view sealed with its caches retained when every character matches; otherwise a new sealed view.
|
|
111
|
+
*/
|
|
112
|
+
settle(text) {
|
|
113
|
+
if (this.object !== void 0 || text.length !== this.#size) return PartialArguments.fromText(text);
|
|
114
|
+
let offset = 0;
|
|
115
|
+
for (const chunk of this.chunks) {
|
|
116
|
+
if (!text.startsWith(chunk, offset)) return PartialArguments.fromText(text);
|
|
117
|
+
offset += chunk.length;
|
|
118
|
+
}
|
|
119
|
+
this.chunks = text.length === 0 ? [] : [text];
|
|
120
|
+
this.#ends = text.length === 0 ? [] : [text.length];
|
|
121
|
+
this.sealed = true;
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Compare observed answers and advance their publication baseline. Unread views remain unscanned.
|
|
126
|
+
* @returns whether any observed answer changed since its first read or the preceding refresh.
|
|
127
|
+
*/
|
|
128
|
+
refresh() {
|
|
129
|
+
if (this.#reads.size === 0) return false;
|
|
130
|
+
this.scan();
|
|
131
|
+
let changed = false;
|
|
132
|
+
let completions = false;
|
|
133
|
+
for (const read of this.#reads.values()) {
|
|
134
|
+
if (read.completion) {
|
|
135
|
+
completions = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
changed = this.refreshRead(read) || changed;
|
|
139
|
+
}
|
|
140
|
+
if (completions) {
|
|
141
|
+
for (const read of this.#reads.values()) if (read.completion) changed = this.refreshRead(read) || changed;
|
|
142
|
+
}
|
|
143
|
+
if (this.sealed) this.#reads.clear();
|
|
144
|
+
return changed;
|
|
145
|
+
}
|
|
146
|
+
refreshRead(read) {
|
|
147
|
+
const now = read.answer();
|
|
148
|
+
if (Object.is(now, read.last)) return false;
|
|
149
|
+
read.last = now;
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Check whether no further fields can arrive.
|
|
154
|
+
* @returns whether the outer object closed, indexing failed, or the view is sealed; unread values are not validated.
|
|
155
|
+
*/
|
|
156
|
+
closed() {
|
|
157
|
+
return this.remember("closed", "", () => this.closedNow());
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* List discovered fields in first-appearance order.
|
|
161
|
+
* @returns top-level keys seen so far, in first-appearance order.
|
|
162
|
+
*/
|
|
163
|
+
keys() {
|
|
164
|
+
return this.remember("keys", "", () => this.keysNow(), (keys) => keys.length);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check whether a top-level field has appeared.
|
|
168
|
+
* @param key - argument name.
|
|
169
|
+
* @returns whether the field has appeared (a string opened or another value began).
|
|
170
|
+
*/
|
|
171
|
+
has(key) {
|
|
172
|
+
return this.remember("has", key, () => this.hasNow(key));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Check whether a field's closing delimiter has arrived, without validating its contents.
|
|
176
|
+
* @param key - argument name.
|
|
177
|
+
* @returns whether its delimiter arrived and no content reader has reported an error for this value.
|
|
178
|
+
*/
|
|
179
|
+
complete(key) {
|
|
180
|
+
return this.remember("complete", key, () => this.completeNow(key));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Read string length without materializing its text.
|
|
184
|
+
* @param key - argument name.
|
|
185
|
+
* @param options - change granularity for a streaming string.
|
|
186
|
+
* @returns decoded UTF-16 length of the string field so far; undefined when absent or not a string.
|
|
187
|
+
*/
|
|
188
|
+
stringLength(key, options) {
|
|
189
|
+
const step = Math.max(1, Math.floor(options?.step ?? 1));
|
|
190
|
+
const offset = options?.offset ?? 0;
|
|
191
|
+
return this.remember(`length:${step}:${offset}`, key, () => this.lengthNow(key), (length) => length === void 0 ? void 0 : Math.ceil((length + offset) / step));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Check a string against a decoded UTF-16 length limit without materializing it.
|
|
195
|
+
* @param key - argument name.
|
|
196
|
+
* @param maxLength - decoded UTF-16 limit, floored to at least zero.
|
|
197
|
+
* @returns whether the string is longer than the limit; false when absent or not a string.
|
|
198
|
+
*/
|
|
199
|
+
stringExceeds(key, maxLength) {
|
|
200
|
+
const limit = Math.max(0, Math.floor(maxLength));
|
|
201
|
+
return this.remember(`exceeds:${limit}`, key, () => (this.lengthNow(key, limit + 1) ?? 0) > limit);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Read a decoded string, including a streaming prefix.
|
|
205
|
+
* @param key - argument name.
|
|
206
|
+
* @returns the string field's decoded text so far; undefined when absent or not a string.
|
|
207
|
+
*/
|
|
208
|
+
text(key) {
|
|
209
|
+
return this.remember("text", key, () => this.textNow(key));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Read at most the first decoded UTF-16 units of a string.
|
|
213
|
+
* @param key - argument name.
|
|
214
|
+
* @param maxLength - maximum decoded UTF-16 length, floored to at least one.
|
|
215
|
+
* @returns the bounded string prefix; undefined when absent or not a string.
|
|
216
|
+
*/
|
|
217
|
+
textPrefix(key, maxLength) {
|
|
218
|
+
const limit = Math.max(1, Math.floor(maxLength));
|
|
219
|
+
return this.remember(`prefix:${limit}`, key, () => this.textPrefixNow(key, limit));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Read a completed non-string argument.
|
|
223
|
+
* @param key - argument name.
|
|
224
|
+
* @returns the parsed non-string value once it closed; undefined while open, absent, or a string.
|
|
225
|
+
*/
|
|
226
|
+
value(key) {
|
|
227
|
+
return this.remember("value", key, () => this.valueNow(key));
|
|
228
|
+
}
|
|
229
|
+
/** Answer a question and, on a streaming view, remember it for change detection. */
|
|
230
|
+
remember(kind, key, read, comparison) {
|
|
231
|
+
this.scan();
|
|
232
|
+
const result = read();
|
|
233
|
+
if (!this.sealed) {
|
|
234
|
+
const id = `${kind}/${key}`;
|
|
235
|
+
if (!this.#reads.has(id)) this.#reads.set(id, {
|
|
236
|
+
completion: kind === "complete",
|
|
237
|
+
answer: comparison === void 0 ? read : () => comparison(read()),
|
|
238
|
+
last: comparison === void 0 ? result : comparison(result)
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
closedNow() {
|
|
244
|
+
return this.sealed || this.#mode === "closed" || this.#mode === "invalid";
|
|
245
|
+
}
|
|
246
|
+
keysNow() {
|
|
247
|
+
return this.object === void 0 ? this.#order : Object.keys(this.object);
|
|
248
|
+
}
|
|
249
|
+
hasNow(key) {
|
|
250
|
+
return this.object === void 0 ? this.#entries.has(key) : Object.hasOwn(this.object, key);
|
|
251
|
+
}
|
|
252
|
+
completeNow(key) {
|
|
253
|
+
if (this.object !== void 0) return Object.hasOwn(this.object, key);
|
|
254
|
+
const entry = this.#entries.get(key);
|
|
255
|
+
return entry !== void 0 && entry.end >= 0 && (entry.kind === "string" ? entry.invalidAt === void 0 : !entry.invalid);
|
|
256
|
+
}
|
|
257
|
+
lengthNow(key, limit = Number.POSITIVE_INFINITY) {
|
|
258
|
+
if (this.object !== void 0) {
|
|
259
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
260
|
+
return typeof field === "string" ? field.length : void 0;
|
|
261
|
+
}
|
|
262
|
+
const entry = this.#entries.get(key);
|
|
263
|
+
if (entry?.kind !== "string") return void 0;
|
|
264
|
+
if (entry.text !== void 0 && entry.text.at === entry.end) return entry.text.length;
|
|
265
|
+
const read = entry.length ??= {
|
|
266
|
+
at: entry.start,
|
|
267
|
+
length: 0,
|
|
268
|
+
text: ""
|
|
269
|
+
};
|
|
270
|
+
this.readString(entry, read, limit, false);
|
|
271
|
+
return read.length;
|
|
272
|
+
}
|
|
273
|
+
textNow(key) {
|
|
274
|
+
if (this.object !== void 0) {
|
|
275
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
276
|
+
return typeof field === "string" ? field : void 0;
|
|
277
|
+
}
|
|
278
|
+
const entry = this.#entries.get(key);
|
|
279
|
+
if (entry?.kind !== "string") return void 0;
|
|
280
|
+
if (entry.text === void 0 && entry.end >= 0 && entry.needsDecoding && entry.invalidAt === void 0) {
|
|
281
|
+
let text;
|
|
282
|
+
try {
|
|
283
|
+
text = JSON.parse(`"${this.slice(entry.start, entry.end)}"`);
|
|
284
|
+
} catch (_error) {}
|
|
285
|
+
if (text !== void 0) entry.text = {
|
|
286
|
+
at: entry.end,
|
|
287
|
+
length: text.length,
|
|
288
|
+
text
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const read = entry.text ??= {
|
|
292
|
+
at: entry.start,
|
|
293
|
+
length: 0,
|
|
294
|
+
text: ""
|
|
295
|
+
};
|
|
296
|
+
this.readString(entry, read, Number.POSITIVE_INFINITY, true);
|
|
297
|
+
return read.text;
|
|
298
|
+
}
|
|
299
|
+
textPrefixNow(key, maxLength) {
|
|
300
|
+
if (this.object !== void 0) {
|
|
301
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
302
|
+
return typeof field === "string" ? field.slice(0, maxLength) : void 0;
|
|
303
|
+
}
|
|
304
|
+
const entry = this.#entries.get(key);
|
|
305
|
+
if (entry?.kind !== "string") return void 0;
|
|
306
|
+
const prefixes = entry.prefixes ??= /* @__PURE__ */ new Map();
|
|
307
|
+
let read = prefixes.get(maxLength);
|
|
308
|
+
if (read === void 0) {
|
|
309
|
+
read = {
|
|
310
|
+
at: entry.start,
|
|
311
|
+
length: 0,
|
|
312
|
+
text: ""
|
|
313
|
+
};
|
|
314
|
+
prefixes.set(maxLength, read);
|
|
315
|
+
}
|
|
316
|
+
this.readString(entry, read, maxLength, true);
|
|
317
|
+
return read.text;
|
|
318
|
+
}
|
|
319
|
+
valueNow(key) {
|
|
320
|
+
if (this.object !== void 0) {
|
|
321
|
+
if (!Object.hasOwn(this.object, key)) return void 0;
|
|
322
|
+
const field = this.object[key];
|
|
323
|
+
return typeof field === "string" ? void 0 : field;
|
|
324
|
+
}
|
|
325
|
+
const entry = this.#entries.get(key);
|
|
326
|
+
if (entry?.kind !== "value" || entry.end < 0 || entry.invalid) return void 0;
|
|
327
|
+
if (entry.parsed === void 0) try {
|
|
328
|
+
entry.parsed = JSON.parse(this.slice(entry.start, entry.end));
|
|
329
|
+
} catch (_error) {
|
|
330
|
+
entry.invalid = true;
|
|
331
|
+
this.#invalidValue = true;
|
|
332
|
+
}
|
|
333
|
+
return entry.parsed;
|
|
334
|
+
}
|
|
335
|
+
chunkAt(at) {
|
|
336
|
+
let low = 0;
|
|
337
|
+
let high = this.#ends.length;
|
|
338
|
+
while (low < high) {
|
|
339
|
+
const mid = low + high >>> 1;
|
|
340
|
+
if (this.#ends[mid] <= at) low = mid + 1;
|
|
341
|
+
else high = mid;
|
|
342
|
+
}
|
|
343
|
+
return low;
|
|
344
|
+
}
|
|
345
|
+
/** Materialize only a requested range, never the cumulative source. */
|
|
346
|
+
slice(start, end) {
|
|
347
|
+
if (start >= end) return "";
|
|
348
|
+
const first = this.chunkAt(start);
|
|
349
|
+
const last = this.chunkAt(end - 1);
|
|
350
|
+
const base = first === 0 ? 0 : this.#ends[first - 1];
|
|
351
|
+
if (first === last) return this.chunks[first].slice(start - base, end - base);
|
|
352
|
+
const parts = [this.chunks[first].slice(start - base)];
|
|
353
|
+
for (let i = first + 1; i < last; i++) parts.push(this.chunks[i]);
|
|
354
|
+
parts.push(this.chunks[last].slice(0, end - this.#ends[last - 1]));
|
|
355
|
+
return parts.join("");
|
|
356
|
+
}
|
|
357
|
+
readString(entry, read, limit, materialize) {
|
|
358
|
+
const end = Math.min(entry.end < 0 ? this.#consumed : entry.end, entry.invalidAt ?? Number.POSITIVE_INFINITY, this.#invalidAt ?? Number.POSITIVE_INFINITY);
|
|
359
|
+
if (!entry.needsDecoding) {
|
|
360
|
+
const length = Math.min(end - read.at, limit - read.length);
|
|
361
|
+
if (length <= 0) return;
|
|
362
|
+
if (materialize) read.text += this.slice(read.at, read.at + length);
|
|
363
|
+
read.at += length;
|
|
364
|
+
read.length += length;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
let chunkIndex = this.chunkAt(read.at);
|
|
368
|
+
while (read.at < end && read.length < limit) {
|
|
369
|
+
const base = chunkIndex === 0 ? 0 : this.#ends[chunkIndex - 1];
|
|
370
|
+
const chunk = this.chunks[chunkIndex];
|
|
371
|
+
const remaining = chunk.slice(read.at - base, Math.min(chunk.length, end - base));
|
|
372
|
+
const boundary = remaining.search(CONTENT_ESCAPE);
|
|
373
|
+
const length = Math.min(boundary < 0 ? remaining.length : boundary, limit - read.length);
|
|
374
|
+
if (length > 0) {
|
|
375
|
+
if (materialize) read.text += remaining.slice(0, length);
|
|
376
|
+
read.at += length;
|
|
377
|
+
read.length += length;
|
|
378
|
+
if (read.at === base + chunk.length) chunkIndex++;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const type = remaining.length > 1 ? remaining[1] : read.at + 1 < end ? this.chunks[chunkIndex + 1][0] : void 0;
|
|
382
|
+
let decoded;
|
|
383
|
+
let width = 2;
|
|
384
|
+
if (remaining[0] === "\\" && type === void 0 && entry.end < 0) return;
|
|
385
|
+
if (remaining[0] === "\\" && type === "u") {
|
|
386
|
+
const hex = this.slice(read.at + 2, Math.min(end, read.at + 6));
|
|
387
|
+
let valid = true;
|
|
388
|
+
for (let i = 0; i < hex.length; i++) if (!isHex(hex[i])) valid = false;
|
|
389
|
+
if (valid) {
|
|
390
|
+
if (hex.length < 4 && entry.end < 0) return;
|
|
391
|
+
if (hex.length === 4) decoded = String.fromCharCode(Number.parseInt(hex, 16));
|
|
392
|
+
}
|
|
393
|
+
width = 6;
|
|
394
|
+
} else if (remaining[0] === "\\" && type !== void 0) decoded = SIMPLE_ESCAPES[type];
|
|
395
|
+
if (decoded === void 0) {
|
|
396
|
+
entry.invalidAt = read.at;
|
|
397
|
+
this.#invalidValue = true;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (materialize) read.text += decoded;
|
|
401
|
+
read.length++;
|
|
402
|
+
read.at += width;
|
|
403
|
+
while (chunkIndex < this.chunks.length && read.at >= this.#ends[chunkIndex]) chunkIndex++;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/** Locate new field ranges without decoding or parsing their contents. */
|
|
407
|
+
scan() {
|
|
408
|
+
if (this.object !== void 0 || this.#consumed === this.#size) return;
|
|
409
|
+
for (let i = this.chunkAt(this.#consumed); i < this.chunks.length && this.#invalidAt === void 0; i++) {
|
|
410
|
+
const pending = this.chunks[i];
|
|
411
|
+
const base = i === 0 ? 0 : this.#ends[i - 1];
|
|
412
|
+
for (let index = this.#consumed - base; index < pending.length && this.#mode !== "invalid"; index++) {
|
|
413
|
+
if (this.#mode === "string" || this.#mode === "nested" && this.#nestedInString) {
|
|
414
|
+
const end = this.stringBoundary(pending, index);
|
|
415
|
+
this.#consumed += end - index;
|
|
416
|
+
index = end;
|
|
417
|
+
if (index === pending.length) break;
|
|
418
|
+
}
|
|
419
|
+
this.step(pending[index], this.#consumed);
|
|
420
|
+
this.#consumed++;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/** Only raw quotes and their preceding backslash runs can terminate a string. */
|
|
425
|
+
stringBoundary(fragment, start) {
|
|
426
|
+
let at = start;
|
|
427
|
+
while (true) {
|
|
428
|
+
const quote = fragment.indexOf("\"", at);
|
|
429
|
+
const end = quote < 0 ? fragment.length : quote;
|
|
430
|
+
if (this.#mode === "string") {
|
|
431
|
+
const entry = this.#current;
|
|
432
|
+
if (!entry.needsDecoding && CONTENT_ESCAPE.test(fragment.slice(at, end))) entry.needsDecoding = true;
|
|
433
|
+
}
|
|
434
|
+
let slashStart = end;
|
|
435
|
+
while (slashStart > at && fragment[slashStart - 1] === "\\") slashStart--;
|
|
436
|
+
const escaped = (end - slashStart) % 2 === 1 !== (slashStart === at && this.#escape);
|
|
437
|
+
this.#escape = quote < 0 && escaped;
|
|
438
|
+
if (quote < 0 || !escaped) return end;
|
|
439
|
+
at = quote + 1;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
step(c, at) {
|
|
443
|
+
switch (this.#mode) {
|
|
444
|
+
case "root":
|
|
445
|
+
if (isWhitespace(c)) return;
|
|
446
|
+
if (c === "{") {
|
|
447
|
+
this.#mode = "key-or-end";
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
this.fail();
|
|
451
|
+
return;
|
|
452
|
+
case "key-or-end":
|
|
453
|
+
if (isWhitespace(c)) return;
|
|
454
|
+
if (c === "}") {
|
|
455
|
+
this.#mode = "closed";
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (c === "\"") {
|
|
459
|
+
this.beginKey(at);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
this.fail();
|
|
463
|
+
return;
|
|
464
|
+
case "key-only":
|
|
465
|
+
if (isWhitespace(c)) return;
|
|
466
|
+
if (c === "\"") {
|
|
467
|
+
this.beginKey(at);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
this.fail();
|
|
471
|
+
return;
|
|
472
|
+
case "key":
|
|
473
|
+
this.stepKey(c, at);
|
|
474
|
+
return;
|
|
475
|
+
case "colon":
|
|
476
|
+
if (isWhitespace(c)) return;
|
|
477
|
+
if (c === ":") {
|
|
478
|
+
this.#mode = "value";
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
this.fail();
|
|
482
|
+
return;
|
|
483
|
+
case "value":
|
|
484
|
+
this.beginValue(c, at);
|
|
485
|
+
return;
|
|
486
|
+
case "string": {
|
|
487
|
+
const entry = this.#current;
|
|
488
|
+
entry.end = at;
|
|
489
|
+
this.#current = null;
|
|
490
|
+
this.#mode = "comma-or-end";
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
case "scalar":
|
|
494
|
+
this.stepScalar(c, at);
|
|
495
|
+
return;
|
|
496
|
+
case "nested":
|
|
497
|
+
this.stepNested(c, at);
|
|
498
|
+
return;
|
|
499
|
+
case "comma-or-end":
|
|
500
|
+
if (isWhitespace(c)) return;
|
|
501
|
+
if (c === ",") {
|
|
502
|
+
this.#mode = "key-only";
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (c === "}") {
|
|
506
|
+
this.#mode = "closed";
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.fail();
|
|
510
|
+
return;
|
|
511
|
+
case "closed":
|
|
512
|
+
if (isWhitespace(c)) return;
|
|
513
|
+
this.fail();
|
|
514
|
+
return;
|
|
515
|
+
/* v8 ignore next 2 -- scan() stops stepping once the view is invalid. */
|
|
516
|
+
case "invalid": return;
|
|
517
|
+
/* v8 ignore next 2 -- Every scanner mode has a handler above. */
|
|
518
|
+
default: assertNever(this.#mode);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
fail() {
|
|
522
|
+
this.#invalidAt = this.#consumed;
|
|
523
|
+
this.#mode = "invalid";
|
|
524
|
+
this.#current = null;
|
|
525
|
+
}
|
|
526
|
+
beginKey(at) {
|
|
527
|
+
this.#mode = "key";
|
|
528
|
+
this.#keyStart = at + 1;
|
|
529
|
+
this.#keyEscaped = false;
|
|
530
|
+
this.#escape = false;
|
|
531
|
+
}
|
|
532
|
+
stepKey(c, at) {
|
|
533
|
+
if (c < " ") {
|
|
534
|
+
this.fail();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (this.#escape) {
|
|
538
|
+
this.#escape = false;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (c === "\\") {
|
|
542
|
+
this.#escape = true;
|
|
543
|
+
this.#keyEscaped = true;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (c !== "\"") return;
|
|
547
|
+
const raw = this.slice(this.#keyStart, at);
|
|
548
|
+
if (this.#keyEscaped) try {
|
|
549
|
+
this.#key = JSON.parse(`"${raw}"`);
|
|
550
|
+
} catch (_error) {
|
|
551
|
+
this.fail();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
else this.#key = raw;
|
|
555
|
+
this.#mode = "colon";
|
|
556
|
+
}
|
|
557
|
+
open(entry) {
|
|
558
|
+
if (!this.#entries.has(this.#key)) this.#order.push(this.#key);
|
|
559
|
+
this.#entries.set(this.#key, entry);
|
|
560
|
+
this.#current = entry;
|
|
561
|
+
}
|
|
562
|
+
beginValue(c, at) {
|
|
563
|
+
if (isWhitespace(c)) return;
|
|
564
|
+
if (c === "\"") {
|
|
565
|
+
this.open({
|
|
566
|
+
kind: "string",
|
|
567
|
+
start: at + 1,
|
|
568
|
+
end: -1,
|
|
569
|
+
needsDecoding: false,
|
|
570
|
+
invalidAt: void 0,
|
|
571
|
+
length: void 0,
|
|
572
|
+
text: void 0,
|
|
573
|
+
prefixes: void 0
|
|
574
|
+
});
|
|
575
|
+
this.#escape = false;
|
|
576
|
+
this.#mode = "string";
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (c === "}" || c === "," || c === ":" || c === "]") {
|
|
580
|
+
this.fail();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
this.open({
|
|
584
|
+
kind: "value",
|
|
585
|
+
start: at,
|
|
586
|
+
end: -1,
|
|
587
|
+
parsed: void 0,
|
|
588
|
+
invalid: false
|
|
589
|
+
});
|
|
590
|
+
if (c === "{" || c === "[") {
|
|
591
|
+
this.#mode = "nested";
|
|
592
|
+
this.#nestedEnds = [c === "{" ? "}" : "]"];
|
|
593
|
+
this.#nestedInString = false;
|
|
594
|
+
this.#escape = false;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
this.#mode = "scalar";
|
|
598
|
+
}
|
|
599
|
+
stepScalar(c, at) {
|
|
600
|
+
if (c !== "," && c !== "}" && !isWhitespace(c)) return;
|
|
601
|
+
this.closeValue(at);
|
|
602
|
+
this.#mode = c === "," ? "key-only" : c === "}" ? "closed" : "comma-or-end";
|
|
603
|
+
}
|
|
604
|
+
stepNested(c, at) {
|
|
605
|
+
if (this.#nestedInString) {
|
|
606
|
+
this.#nestedInString = false;
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (c === "\"") {
|
|
610
|
+
this.#nestedInString = true;
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (c === "{" || c === "[") {
|
|
614
|
+
this.#nestedEnds.push(c === "{" ? "}" : "]");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (c === "}" || c === "]") {
|
|
618
|
+
if (this.#nestedEnds.pop() !== c) {
|
|
619
|
+
this.fail();
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (this.#nestedEnds.length === 0) {
|
|
623
|
+
this.closeValue(at + 1);
|
|
624
|
+
this.#mode = "comma-or-end";
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
closeValue(end) {
|
|
629
|
+
const entry = this.#current;
|
|
630
|
+
entry.end = end;
|
|
631
|
+
this.#current = null;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region ../../util/values/src/index.ts
|
|
636
|
+
/**
|
|
637
|
+
* Mark an unreachable closed-union branch.
|
|
638
|
+
* @param value - impossible value; an unhandled typed variant fails at the call site.
|
|
639
|
+
* @param context - optional switch-site label included in the failure message.
|
|
640
|
+
* @returns never; a runtime value that escaped its type always throws.
|
|
641
|
+
*/
|
|
642
|
+
function assertNever(value, context) {
|
|
643
|
+
const rendered = JSON.stringify(value) ?? String(value);
|
|
644
|
+
throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Weak-key lookup with a strongly retained iterable set of associated values.
|
|
648
|
+
*
|
|
649
|
+
* Each value must belong to only one key. The container performs no automatic
|
|
650
|
+
* cleanup; owners delete associations or clear the container at lifecycle end.
|
|
651
|
+
*/
|
|
652
|
+
var WeakMapWithValues = class {
|
|
653
|
+
keys = /* @__PURE__ */ new WeakMap();
|
|
654
|
+
valueSet = /* @__PURE__ */ new Set();
|
|
655
|
+
/** Live strongly retained values in insertion order. */
|
|
656
|
+
values = this.valueSet;
|
|
657
|
+
/**
|
|
658
|
+
* Read the value associated with a key.
|
|
659
|
+
* @param key - weakly held lookup key.
|
|
660
|
+
* @returns the associated value, or absence.
|
|
661
|
+
*/
|
|
662
|
+
get(key) {
|
|
663
|
+
return this.keys.get(key);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Test whether a key has an association.
|
|
667
|
+
* @param key - weakly held lookup key.
|
|
668
|
+
* @returns whether the key is present.
|
|
669
|
+
*/
|
|
670
|
+
has(key) {
|
|
671
|
+
return this.keys.has(key);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Associate one key with one caller-unique value.
|
|
675
|
+
* @param key - weakly held lookup key.
|
|
676
|
+
* @param value - strongly retained value that belongs to no other key.
|
|
677
|
+
* @returns this container.
|
|
678
|
+
*/
|
|
679
|
+
set(key, value) {
|
|
680
|
+
if (this.keys.has(key)) {
|
|
681
|
+
const previous = this.keys.get(key);
|
|
682
|
+
if (previous === value) return this;
|
|
683
|
+
this.valueSet.delete(previous);
|
|
684
|
+
}
|
|
685
|
+
this.keys.set(key, value);
|
|
686
|
+
this.valueSet.add(value);
|
|
687
|
+
return this;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Remove one association and its strongly retained value.
|
|
691
|
+
* @param key - weakly held lookup key.
|
|
692
|
+
* @returns whether an association was removed.
|
|
693
|
+
*/
|
|
694
|
+
delete(key) {
|
|
695
|
+
if (!this.keys.has(key)) return false;
|
|
696
|
+
const value = this.keys.get(key);
|
|
697
|
+
const deleted = this.keys.delete(key);
|
|
698
|
+
this.valueSet.delete(value);
|
|
699
|
+
return deleted;
|
|
700
|
+
}
|
|
701
|
+
/** Remove every association and strongly retained value. */
|
|
702
|
+
clear() {
|
|
703
|
+
this.keys = /* @__PURE__ */ new WeakMap();
|
|
704
|
+
this.valueSet.clear();
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region lib/types/client/session-provider.js
|
|
709
|
+
/**
|
|
710
|
+
* Render the selected Session body or its empty branch.
|
|
711
|
+
* @param binding - current Session scope binding.
|
|
712
|
+
* @param props - standard Session area render props.
|
|
713
|
+
* @returns the selected Session subtree.
|
|
714
|
+
*/
|
|
715
|
+
function renderSessionArea(binding, { empty, children }) {
|
|
716
|
+
if (binding.key === void 0) return (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: empty?.() ?? null });
|
|
717
|
+
return (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children });
|
|
718
|
+
}
|
|
719
|
+
//#endregion
|
|
720
|
+
//#region lib/types/client/index.js
|
|
721
|
+
/** Session Controller adapter for React selector hooks and Slot scope data. */
|
|
722
|
+
var PendingInteractionDomain = class {
|
|
723
|
+
precedence;
|
|
724
|
+
changed;
|
|
725
|
+
values = /* @__PURE__ */ new Map();
|
|
726
|
+
constructor(precedence, changed) {
|
|
727
|
+
this.precedence = precedence;
|
|
728
|
+
this.changed = changed;
|
|
729
|
+
}
|
|
730
|
+
valuesSnapshot() {
|
|
731
|
+
return [...this.values.values()].map((entry) => entry.interaction);
|
|
732
|
+
}
|
|
733
|
+
publish(interaction, delegate) {
|
|
734
|
+
if (this.values.has(interaction.key)) throw new Error(`ui-session: duplicate pending interaction key '${interaction.key}'`);
|
|
735
|
+
this.values.set(interaction.key, {
|
|
736
|
+
interaction,
|
|
737
|
+
delegate
|
|
738
|
+
});
|
|
739
|
+
this.changed();
|
|
740
|
+
let active = true;
|
|
741
|
+
return () => {
|
|
742
|
+
if (!active) return;
|
|
743
|
+
active = false;
|
|
744
|
+
if (!this.values.delete(interaction.key)) return;
|
|
745
|
+
this.changed();
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
/** Remove every pending value and return the operations that settle their owners. */
|
|
749
|
+
release() {
|
|
750
|
+
const delegates = [...this.values.values()].map((entry) => entry.delegate);
|
|
751
|
+
this.values.clear();
|
|
752
|
+
return delegates;
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
const BUILTIN_SOURCE = {
|
|
756
|
+
hooks: ["session"],
|
|
757
|
+
keyedHooks: ["projection"],
|
|
758
|
+
props: ["sessionId"],
|
|
759
|
+
resolve: (binding) => ({
|
|
760
|
+
hooks: { session: binding.session },
|
|
761
|
+
keyedHooks: { projection: (key) => binding.session.projections.faceOf(key) },
|
|
762
|
+
props: { sessionId: binding.sessionId }
|
|
763
|
+
})
|
|
764
|
+
};
|
|
765
|
+
/** Session-scoped source roster and renderer adapter. */
|
|
766
|
+
var UiSession = class extends _qilin_agent_kylin.Service {
|
|
767
|
+
sessions;
|
|
768
|
+
descriptors = [BUILTIN_SOURCE];
|
|
769
|
+
bindings = new WeakMapWithValues();
|
|
770
|
+
absent;
|
|
771
|
+
current;
|
|
772
|
+
pendingDomains = [];
|
|
773
|
+
pendingSnapshot = /* @__PURE__ */ new Map();
|
|
774
|
+
running = /* @__PURE__ */ new Map();
|
|
775
|
+
completionUnread = /* @__PURE__ */ new Set();
|
|
776
|
+
statusSnapshot = /* @__PURE__ */ new Map();
|
|
777
|
+
statusListeners = /* @__PURE__ */ new Set();
|
|
778
|
+
mainRetainId;
|
|
779
|
+
disposeMainRetain = () => {};
|
|
780
|
+
active = true;
|
|
781
|
+
/** Root source combining running, pending-interaction, and completion-reminder facts. */
|
|
782
|
+
sessionStatus = {
|
|
783
|
+
getSnapshot: () => this.statusSnapshot,
|
|
784
|
+
subscribe: (listener) => {
|
|
785
|
+
this.statusListeners.add(listener);
|
|
786
|
+
return () => {
|
|
787
|
+
this.statusListeners.delete(listener);
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
/** Renderer-facing adapter for `session` and `session-maybe` scopes. */
|
|
792
|
+
adapter;
|
|
793
|
+
/**
|
|
794
|
+
* @param ctx - Client root context.
|
|
795
|
+
* @param sessions - Controller-owned Session object layer.
|
|
796
|
+
*/
|
|
797
|
+
constructor(ctx, sessions) {
|
|
798
|
+
super(ctx, "uiSession");
|
|
799
|
+
this.sessions = sessions;
|
|
800
|
+
this.absent = createBindingSource(this.materializeAbsent());
|
|
801
|
+
this.current = createBindingSource(this.absent.value);
|
|
802
|
+
this.adapter = {
|
|
803
|
+
current: this.current,
|
|
804
|
+
bindingSource: (target) => this.bindingSource(target),
|
|
805
|
+
renderArea: renderSessionArea
|
|
806
|
+
};
|
|
807
|
+
ctx.effect(() => {
|
|
808
|
+
const disposeList = sessions.list.subscribe(() => {
|
|
809
|
+
this.publishMain();
|
|
810
|
+
});
|
|
811
|
+
const disposeStatus = sessions.list.subscribe(() => {
|
|
812
|
+
this.reconcileStatus();
|
|
813
|
+
});
|
|
814
|
+
const disposeRemoteStatus = ctx.remote.$on("api-session/status", (sessionId, running) => {
|
|
815
|
+
this.observeRunning(sessionId, running);
|
|
816
|
+
});
|
|
817
|
+
this.publishMain();
|
|
818
|
+
this.reconcileStatus();
|
|
819
|
+
return () => {
|
|
820
|
+
this.active = false;
|
|
821
|
+
disposeList();
|
|
822
|
+
disposeStatus();
|
|
823
|
+
disposeRemoteStatus();
|
|
824
|
+
this.disposeMainRetain();
|
|
825
|
+
const records = [...this.bindings.values];
|
|
826
|
+
this.bindings.clear();
|
|
827
|
+
for (const record of records) record.release();
|
|
828
|
+
};
|
|
829
|
+
}, "ui-session: Session binding projection");
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Resolve a stable renderer source for an owned Session reference or explicit absence.
|
|
833
|
+
* @param reference - active reference supplied by the Provider owner, or absence.
|
|
834
|
+
* @returns the binding source, which falls back to the absent projection when its generation ends.
|
|
835
|
+
* @throws when the reference does not belong to the active Controller generation.
|
|
836
|
+
*/
|
|
837
|
+
bindingSource(reference) {
|
|
838
|
+
if (!this.active) return this.absent;
|
|
839
|
+
if (reference === void 0) return this.absent;
|
|
840
|
+
const owner = reference.binding;
|
|
841
|
+
if (this.sessions.binding(reference.sessionId) !== owner) throw new Error("ui-session: Session reference is not active in this Controller");
|
|
842
|
+
return this.sourceFor(owner);
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Register one Session-scoped standard-source contribution.
|
|
846
|
+
* @param descriptor - static member roster and per-binding resolver.
|
|
847
|
+
* @returns disposer owned by the caller's Cordis fiber.
|
|
848
|
+
*/
|
|
849
|
+
provide(descriptor) {
|
|
850
|
+
const runtimeDescriptor = descriptor;
|
|
851
|
+
const dispose = this.ctx.effect(() => {
|
|
852
|
+
this.descriptors.push(runtimeDescriptor);
|
|
853
|
+
try {
|
|
854
|
+
this.rebuildBindings();
|
|
855
|
+
} catch (error) {
|
|
856
|
+
this.descriptors.pop();
|
|
857
|
+
throw error;
|
|
858
|
+
}
|
|
859
|
+
return () => {
|
|
860
|
+
const index = this.descriptors.indexOf(runtimeDescriptor);
|
|
861
|
+
this.descriptors.splice(index, 1);
|
|
862
|
+
this.rebuildBindings();
|
|
863
|
+
};
|
|
864
|
+
}, "uiSession.provide()");
|
|
865
|
+
return () => {
|
|
866
|
+
dispose();
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Register one pending-interaction domain and return its publication function.
|
|
871
|
+
* Domain teardown first removes its visible values, then delegates and awaits
|
|
872
|
+
* every still-active owner request.
|
|
873
|
+
* @param precedence - deterministic cross-domain precedence; larger values win.
|
|
874
|
+
* @returns a function that publishes one interaction and its teardown delegation.
|
|
875
|
+
*/
|
|
876
|
+
registerPendingInteraction(precedence) {
|
|
877
|
+
const domain = new PendingInteractionDomain(precedence, () => {
|
|
878
|
+
this.publishPendingInteractions();
|
|
879
|
+
});
|
|
880
|
+
const runtimeDomain = domain;
|
|
881
|
+
this.ctx.effect(() => {
|
|
882
|
+
this.pendingDomains.push(runtimeDomain);
|
|
883
|
+
this.publishPendingInteractions();
|
|
884
|
+
return async () => {
|
|
885
|
+
const delegates = domain.release();
|
|
886
|
+
const index = this.pendingDomains.indexOf(runtimeDomain);
|
|
887
|
+
this.pendingDomains.splice(index, 1);
|
|
888
|
+
this.publishPendingInteractions();
|
|
889
|
+
await Promise.allSettled(delegates.map((delegate) => Promise.resolve().then(delegate)));
|
|
890
|
+
};
|
|
891
|
+
}, "uiSession.registerPendingInteraction()");
|
|
892
|
+
return (interaction, delegate) => domain.publish(interaction, delegate);
|
|
893
|
+
}
|
|
894
|
+
rebuildBindings() {
|
|
895
|
+
const absent = this.materializeAbsent();
|
|
896
|
+
const updates = [...this.bindings.values].map((record) => ({
|
|
897
|
+
source: record.source,
|
|
898
|
+
value: this.materialize(record.owner)
|
|
899
|
+
}));
|
|
900
|
+
this.absent.value = absent;
|
|
901
|
+
for (const { source, value } of updates) source.value = value;
|
|
902
|
+
(0, _qilin_agent_client_store.notifySubscribers)(this.absent.listeners, "[ui-session] absent binding");
|
|
903
|
+
for (const { source } of updates) (0, _qilin_agent_client_store.notifySubscribers)(source.listeners, "[ui-session] Session binding");
|
|
904
|
+
this.publishMain();
|
|
905
|
+
}
|
|
906
|
+
sourceFor(owner) {
|
|
907
|
+
const cached = this.bindings.get(owner);
|
|
908
|
+
if (cached !== void 0) return cached.source;
|
|
909
|
+
const record = this.createMaterializedBinding(owner);
|
|
910
|
+
this.bindings.set(owner, record);
|
|
911
|
+
return record.source;
|
|
912
|
+
}
|
|
913
|
+
publishMain() {
|
|
914
|
+
if (!this.active) return;
|
|
915
|
+
const byId = this.sessions.list.getSnapshot().byId;
|
|
916
|
+
const currentId = this.current.value.key;
|
|
917
|
+
const nextId = currentId !== void 0 && (this.sessions.retainInfo(currentId).getSnapshot().retainedBy.mainView ?? 0) > 0 ? currentId : Object.values(byId).find((candidate) => (candidate.retainedBy.mainView ?? 0) > 0)?.id;
|
|
918
|
+
this.watchMainRetention(nextId);
|
|
919
|
+
const owner = nextId === void 0 ? void 0 : this.sessions.binding(nextId);
|
|
920
|
+
const value = owner === void 0 ? this.absent.value : this.sourceFor(owner).value;
|
|
921
|
+
if (this.current.value === value) return;
|
|
922
|
+
this.current.value = value;
|
|
923
|
+
(0, _qilin_agent_client_store.notifySubscribers)(this.current.listeners, "[ui-session] main binding");
|
|
924
|
+
}
|
|
925
|
+
watchMainRetention(sessionId) {
|
|
926
|
+
if (sessionId === this.mainRetainId) return;
|
|
927
|
+
this.disposeMainRetain();
|
|
928
|
+
this.mainRetainId = sessionId;
|
|
929
|
+
this.disposeMainRetain = sessionId === void 0 ? () => {} : this.sessions.retainInfo(sessionId).subscribe(() => {
|
|
930
|
+
this.publishMain();
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
publishPendingInteractions() {
|
|
934
|
+
const next = /* @__PURE__ */ new Map();
|
|
935
|
+
for (const domain of this.pendingDomains) for (const interaction of domain.valuesSnapshot()) {
|
|
936
|
+
const precedence = domain.precedence(interaction);
|
|
937
|
+
const previous = next.get(interaction.sessionId);
|
|
938
|
+
if (previous === void 0 || precedence >= previous.precedence) next.set(interaction.sessionId, {
|
|
939
|
+
interaction,
|
|
940
|
+
precedence
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const projected = new Map([...next].map(([sessionId, value]) => [sessionId, value.interaction]));
|
|
944
|
+
if (samePendingInteractions(this.pendingSnapshot, projected)) return;
|
|
945
|
+
this.pendingSnapshot = projected;
|
|
946
|
+
this.publishStatus();
|
|
947
|
+
}
|
|
948
|
+
observeRunning(sessionId, running) {
|
|
949
|
+
const previous = this.running.get(sessionId);
|
|
950
|
+
const beforeBaseline = this.sessions.list.getSnapshot().phase === "pending";
|
|
951
|
+
this.running.set(sessionId, running);
|
|
952
|
+
if (running) this.completionUnread.delete(sessionId);
|
|
953
|
+
else if ((previous === true || previous === void 0 && beforeBaseline) && !this.isMain(sessionId)) this.completionUnread.add(sessionId);
|
|
954
|
+
this.publishStatus();
|
|
955
|
+
}
|
|
956
|
+
reconcileStatus() {
|
|
957
|
+
const list = this.sessions.list.getSnapshot();
|
|
958
|
+
const present = new Set(Object.keys(list.byId));
|
|
959
|
+
for (const id of present) {
|
|
960
|
+
const row = list.byId[id];
|
|
961
|
+
if (row === void 0) continue;
|
|
962
|
+
const previous = this.running.get(id);
|
|
963
|
+
if (previous === void 0) this.running.set(id, row.running);
|
|
964
|
+
else if (previous !== row.running) this.observeRunning(id, row.running);
|
|
965
|
+
if ((row.retainedBy.mainView ?? 0) > 0) this.completionUnread.delete(id);
|
|
966
|
+
}
|
|
967
|
+
if (list.phase === "ready") for (const id of this.running.keys()) {
|
|
968
|
+
if (present.has(id)) continue;
|
|
969
|
+
this.running.delete(id);
|
|
970
|
+
this.completionUnread.delete(id);
|
|
971
|
+
}
|
|
972
|
+
this.publishStatus();
|
|
973
|
+
}
|
|
974
|
+
isMain(sessionId) {
|
|
975
|
+
return (this.sessions.list.getSnapshot().byId[sessionId]?.retainedBy.mainView ?? 0) > 0;
|
|
976
|
+
}
|
|
977
|
+
publishStatus() {
|
|
978
|
+
const ids = new Set([
|
|
979
|
+
...Object.keys(this.sessions.list.getSnapshot().byId),
|
|
980
|
+
...this.running.keys(),
|
|
981
|
+
...this.pendingSnapshot.keys(),
|
|
982
|
+
...this.completionUnread
|
|
983
|
+
]);
|
|
984
|
+
const next = /* @__PURE__ */ new Map();
|
|
985
|
+
for (const id of ids) next.set(id, {
|
|
986
|
+
running: this.running.get(id),
|
|
987
|
+
pendingInteraction: this.pendingSnapshot.get(id),
|
|
988
|
+
completionUnread: this.completionUnread.has(id)
|
|
989
|
+
});
|
|
990
|
+
if (sameSessionStatus(this.statusSnapshot, next)) return;
|
|
991
|
+
this.statusSnapshot = next;
|
|
992
|
+
(0, _qilin_agent_client_store.notifySubscribers)(this.statusListeners, "[ui-session] Session status");
|
|
993
|
+
}
|
|
994
|
+
createMaterializedBinding(owner) {
|
|
995
|
+
const value = this.materialize(owner);
|
|
996
|
+
this.ctx.slots.bindStoreScope(value);
|
|
997
|
+
const source = createBindingSource(value);
|
|
998
|
+
const releaseEffect = owner.ctx.effect(() => () => {
|
|
999
|
+
if (this.bindings.get(owner) === record) this.bindings.delete(owner);
|
|
1000
|
+
source.value = this.absent.value;
|
|
1001
|
+
(0, _qilin_agent_client_store.notifySubscribers)(source.listeners, "[ui-session] Session binding");
|
|
1002
|
+
this.publishMain();
|
|
1003
|
+
}, `ui-session: binding ${owner.sessionId}`);
|
|
1004
|
+
const record = {
|
|
1005
|
+
owner,
|
|
1006
|
+
source,
|
|
1007
|
+
release: () => {
|
|
1008
|
+
releaseEffect();
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
return record;
|
|
1012
|
+
}
|
|
1013
|
+
materialize(binding) {
|
|
1014
|
+
const hooks = {};
|
|
1015
|
+
const keyedHooks = {};
|
|
1016
|
+
const props = {};
|
|
1017
|
+
const finalProps = /* @__PURE__ */ new Set();
|
|
1018
|
+
for (const descriptor of this.descriptors) {
|
|
1019
|
+
const contribution = descriptor.resolve(binding);
|
|
1020
|
+
validateContribution(descriptor, contribution);
|
|
1021
|
+
copyDeclared("hook", hooks, descriptor.hooks, contribution.hooks, finalProps);
|
|
1022
|
+
copyDeclared("keyed hook", keyedHooks, descriptor.keyedHooks, contribution.keyedHooks, finalProps);
|
|
1023
|
+
copyDeclared("prop", props, descriptor.props, contribution.props, finalProps);
|
|
1024
|
+
}
|
|
1025
|
+
return {
|
|
1026
|
+
key: binding.sessionId,
|
|
1027
|
+
ctx: binding.ctx,
|
|
1028
|
+
hooks,
|
|
1029
|
+
keyedHooks,
|
|
1030
|
+
props
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
materializeAbsent() {
|
|
1034
|
+
const hooks = {};
|
|
1035
|
+
const keyedHooks = {};
|
|
1036
|
+
const props = {};
|
|
1037
|
+
const finalProps = /* @__PURE__ */ new Set();
|
|
1038
|
+
for (const descriptor of this.descriptors) {
|
|
1039
|
+
declareAbsent("hook", hooks, descriptor.hooks, finalProps);
|
|
1040
|
+
declareAbsent("keyed hook", keyedHooks, descriptor.keyedHooks, finalProps);
|
|
1041
|
+
declareAbsent("prop", props, descriptor.props, finalProps);
|
|
1042
|
+
}
|
|
1043
|
+
return {
|
|
1044
|
+
key: void 0,
|
|
1045
|
+
hooks,
|
|
1046
|
+
keyedHooks,
|
|
1047
|
+
props
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
function createBindingSource(value) {
|
|
1052
|
+
const source = {
|
|
1053
|
+
value,
|
|
1054
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
1055
|
+
getSnapshot: () => source.value,
|
|
1056
|
+
subscribe: (listener) => {
|
|
1057
|
+
source.listeners.add(listener);
|
|
1058
|
+
return () => {
|
|
1059
|
+
source.listeners.delete(listener);
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
return source;
|
|
1064
|
+
}
|
|
1065
|
+
function validateContribution(descriptor, contribution) {
|
|
1066
|
+
rejectUndeclared("hook", descriptor.hooks, contribution.hooks);
|
|
1067
|
+
rejectUndeclared("keyed hook", descriptor.keyedHooks, contribution.keyedHooks);
|
|
1068
|
+
rejectUndeclared("prop", descriptor.props, contribution.props);
|
|
1069
|
+
}
|
|
1070
|
+
function rejectUndeclared(kind, declared, values) {
|
|
1071
|
+
for (const name of Object.keys(values ?? {})) if (!(declared ?? []).includes(name)) throw new Error(`uiSession.provide: undeclared ${kind} '${name}'`);
|
|
1072
|
+
}
|
|
1073
|
+
function copyDeclared(kind, target, declared, values, finalProps) {
|
|
1074
|
+
for (const name of declared ?? []) {
|
|
1075
|
+
claimStandardProp(kind, name, finalProps);
|
|
1076
|
+
const value = values?.[name];
|
|
1077
|
+
if (value === void 0) throw new Error(`uiSession.provide: missing ${kind} '${name}'`);
|
|
1078
|
+
target[name] = value;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function declareAbsent(kind, target, declared, finalProps) {
|
|
1082
|
+
for (const name of declared ?? []) {
|
|
1083
|
+
claimStandardProp(kind, name, finalProps);
|
|
1084
|
+
target[name] = void 0;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
function claimStandardProp(kind, name, finalProps) {
|
|
1088
|
+
const propName = kind === "prop" ? name : (0, _qilin_agent_client_ui_slots.standardHookPropName)(name);
|
|
1089
|
+
if (finalProps.has(propName)) throw new Error(`uiSession.provide: duplicate ${kind} '${name}' at prop '${propName}'`);
|
|
1090
|
+
finalProps.add(propName);
|
|
1091
|
+
}
|
|
1092
|
+
/** Required Controller and renderer services. */
|
|
1093
|
+
const inject = [
|
|
1094
|
+
"sessions",
|
|
1095
|
+
"slots",
|
|
1096
|
+
"remote"
|
|
1097
|
+
];
|
|
1098
|
+
/**
|
|
1099
|
+
* Install the Session root source and scoped adapter.
|
|
1100
|
+
* @param ctx - Client Cordis context.
|
|
1101
|
+
*/
|
|
1102
|
+
function apply(ctx) {
|
|
1103
|
+
const service = new UiSession(ctx, ctx.sessions);
|
|
1104
|
+
ctx.slots.provideRoot({
|
|
1105
|
+
hooks: {
|
|
1106
|
+
sessions: ctx.sessions.list,
|
|
1107
|
+
sessionStatus: service.sessionStatus
|
|
1108
|
+
},
|
|
1109
|
+
keyedHooks: { sessionRetainInfo: (key) => ctx.sessions.retainInfo(key) }
|
|
1110
|
+
});
|
|
1111
|
+
ctx.slots.installScope("session", service.adapter);
|
|
1112
|
+
}
|
|
1113
|
+
function sameSessionStatus(left, right) {
|
|
1114
|
+
if (left.size !== right.size) return false;
|
|
1115
|
+
for (const [id, status] of left) {
|
|
1116
|
+
const candidate = right.get(id);
|
|
1117
|
+
if (candidate === void 0 || candidate.running !== status.running || candidate.pendingInteraction !== status.pendingInteraction || candidate.completionUnread !== status.completionUnread) return false;
|
|
1118
|
+
}
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
function samePendingInteractions(left, right) {
|
|
1122
|
+
if (left.size !== right.size) return false;
|
|
1123
|
+
for (const [sessionId, interaction] of left) if (right.get(sessionId) !== interaction) return false;
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
//#endregion
|
|
1127
|
+
exports.UiSession = UiSession;
|
|
1128
|
+
exports.apply = apply;
|
|
1129
|
+
exports.inject = inject;
|
|
1130
|
+
return module.exports;
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
//# sourceMappingURL=client.js.map
|