@ai-gui/core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/index.cjs +2088 -196
- package/dist/index.d.cts +468 -37
- package/dist/index.d.ts +468 -37
- package/dist/index.js +2049 -196
- package/package.json +14 -4
package/dist/index.js
CHANGED
|
@@ -102,7 +102,10 @@ function repair(str) {
|
|
|
102
102
|
break;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
if (inString && !stringIsKey)
|
|
105
|
+
if (inString && !stringIsKey) {
|
|
106
|
+
const body = escaped ? str.slice(0, -1) : str;
|
|
107
|
+
return [closeContainers(body + "\"", stack)];
|
|
108
|
+
}
|
|
106
109
|
const fallback = lastGoodEnd === -1 ? [] : [closeContainers(str.slice(0, lastGoodEnd), stack)];
|
|
107
110
|
if (inLiteral) return [closeContainers(str, stack), ...fallback];
|
|
108
111
|
return fallback;
|
|
@@ -126,20 +129,68 @@ function closeContainers(body, stack) {
|
|
|
126
129
|
*/
|
|
127
130
|
function repairMarkdown(buffer) {
|
|
128
131
|
let out = buffer;
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
132
|
+
const openFence = findOpenFence(out);
|
|
133
|
+
if (openFence) {
|
|
131
134
|
if (!out.endsWith("\n")) out += "\n";
|
|
132
|
-
out +=
|
|
135
|
+
out += openFence;
|
|
133
136
|
return out;
|
|
134
137
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
const bold = (
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const inline = findOpenInlineCode(out);
|
|
139
|
+
const visible = inline ? out.slice(0, inline.index) : out;
|
|
140
|
+
const bold = countUnescaped(visible, "**");
|
|
141
|
+
const strike = countUnescaped(visible, "~~");
|
|
142
|
+
if (inline) out += inline.marker;
|
|
140
143
|
if (strike % 2 === 1) out += "~~";
|
|
144
|
+
if (bold % 2 === 1) out += "**";
|
|
141
145
|
return out;
|
|
142
146
|
}
|
|
147
|
+
function findOpenFence(buffer) {
|
|
148
|
+
let open;
|
|
149
|
+
for (const line of buffer.split(/\r\n|\r|\n/)) {
|
|
150
|
+
const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
151
|
+
if (!match) continue;
|
|
152
|
+
const marker = match[1];
|
|
153
|
+
if (!open) open = {
|
|
154
|
+
char: marker[0],
|
|
155
|
+
length: marker.length,
|
|
156
|
+
marker
|
|
157
|
+
};
|
|
158
|
+
else if (marker[0] === open.char && marker.length >= open.length && match[2].trim() === "") open = void 0;
|
|
159
|
+
}
|
|
160
|
+
return open?.marker;
|
|
161
|
+
}
|
|
162
|
+
function findOpenInlineCode(buffer) {
|
|
163
|
+
let open;
|
|
164
|
+
for (let i = 0; i < buffer.length;) {
|
|
165
|
+
if (buffer[i] !== "`" || isEscaped(buffer, i)) {
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
let end = i + 1;
|
|
170
|
+
while (buffer[end] === "`") end++;
|
|
171
|
+
const marker = buffer.slice(i, end);
|
|
172
|
+
if (!open) open = {
|
|
173
|
+
index: i,
|
|
174
|
+
marker
|
|
175
|
+
};
|
|
176
|
+
else if (open.marker === marker) open = void 0;
|
|
177
|
+
i = end;
|
|
178
|
+
}
|
|
179
|
+
return open;
|
|
180
|
+
}
|
|
181
|
+
function countUnescaped(buffer, marker) {
|
|
182
|
+
let count = 0;
|
|
183
|
+
for (let i = 0; i <= buffer.length - marker.length; i++) if (buffer.startsWith(marker, i) && !isEscaped(buffer, i)) {
|
|
184
|
+
count++;
|
|
185
|
+
i += marker.length - 1;
|
|
186
|
+
}
|
|
187
|
+
return count;
|
|
188
|
+
}
|
|
189
|
+
function isEscaped(buffer, index) {
|
|
190
|
+
let slashes = 0;
|
|
191
|
+
for (let i = index - 1; i >= 0 && buffer[i] === "\\"; i--) slashes++;
|
|
192
|
+
return slashes % 2 === 1;
|
|
193
|
+
}
|
|
143
194
|
|
|
144
195
|
//#endregion
|
|
145
196
|
//#region src/sanitizer.ts
|
|
@@ -155,11 +206,270 @@ function escapeHtml(html) {
|
|
|
155
206
|
* fall back to escaping the HTML-significant characters. This never emits raw
|
|
156
207
|
* markup and never throws.
|
|
157
208
|
*/
|
|
158
|
-
function sanitizeHtml(html) {
|
|
159
|
-
if (
|
|
209
|
+
function sanitizeHtml(html, options = {}) {
|
|
210
|
+
if (options.sanitizer) return options.sanitizer(html);
|
|
211
|
+
if (typeof window === "undefined") {
|
|
212
|
+
if (options.ssr === "throw") throw new Error("sanitizeHtml requires a DOM or an injected sanitizer");
|
|
213
|
+
return escapeHtml(html);
|
|
214
|
+
}
|
|
160
215
|
return DOMPurify.sanitize(html);
|
|
161
216
|
}
|
|
162
217
|
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/debug-events.ts
|
|
220
|
+
const DEFAULT_MAX_STRING_LENGTH = 2048;
|
|
221
|
+
const DEFAULT_MAX_DEPTH = 8;
|
|
222
|
+
const DEFAULT_MAX_NODES = 1e3;
|
|
223
|
+
const SENSITIVE_KEY_PARTS = [
|
|
224
|
+
"authorization",
|
|
225
|
+
"auth",
|
|
226
|
+
"accesstoken",
|
|
227
|
+
"refreshtoken",
|
|
228
|
+
"clientsecret",
|
|
229
|
+
"credentials",
|
|
230
|
+
"cookie",
|
|
231
|
+
"setcookie",
|
|
232
|
+
"proxyauthorization",
|
|
233
|
+
"password",
|
|
234
|
+
"passwd",
|
|
235
|
+
"apikey",
|
|
236
|
+
"secret",
|
|
237
|
+
"token"
|
|
238
|
+
];
|
|
239
|
+
var DebugEmitter = class {
|
|
240
|
+
enabled;
|
|
241
|
+
source;
|
|
242
|
+
limits;
|
|
243
|
+
listeners = new Set();
|
|
244
|
+
sequence = 0;
|
|
245
|
+
constructor(source, options = {}) {
|
|
246
|
+
this.source = source;
|
|
247
|
+
this.enabled = options.debug === true;
|
|
248
|
+
this.limits = {
|
|
249
|
+
maxStringLength: positiveInteger(options.maxStringLength, DEFAULT_MAX_STRING_LENGTH, "maxStringLength"),
|
|
250
|
+
maxDepth: positiveInteger(options.maxDepth, DEFAULT_MAX_DEPTH, "maxDepth"),
|
|
251
|
+
maxNodes: positiveInteger(options.maxNodes, DEFAULT_MAX_NODES, "maxNodes"),
|
|
252
|
+
redact: options.redact
|
|
253
|
+
};
|
|
254
|
+
if (this.enabled && options.onDebugEvent) this.listeners.add(options.onDebugEvent);
|
|
255
|
+
}
|
|
256
|
+
get active() {
|
|
257
|
+
return this.enabled && this.listeners.size > 0;
|
|
258
|
+
}
|
|
259
|
+
get available() {
|
|
260
|
+
return this.enabled;
|
|
261
|
+
}
|
|
262
|
+
subscribe(listener) {
|
|
263
|
+
if (!this.enabled) return () => {};
|
|
264
|
+
this.listeners.add(listener);
|
|
265
|
+
return () => this.listeners.delete(listener);
|
|
266
|
+
}
|
|
267
|
+
emit(type, data = {}) {
|
|
268
|
+
if (!this.active) return;
|
|
269
|
+
const event = Object.freeze({
|
|
270
|
+
type,
|
|
271
|
+
source: this.source,
|
|
272
|
+
timestamp: Date.now(),
|
|
273
|
+
sequence: ++this.sequence,
|
|
274
|
+
data: safeDebugValue(data, this.limits)
|
|
275
|
+
});
|
|
276
|
+
for (const listener of this.listeners) try {
|
|
277
|
+
listener(event);
|
|
278
|
+
} catch {}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
function safeDebugValue(value, options = {}) {
|
|
282
|
+
const maxStringLength = positiveInteger(options.maxStringLength, DEFAULT_MAX_STRING_LENGTH, "maxStringLength");
|
|
283
|
+
const maxDepth = positiveInteger(options.maxDepth, DEFAULT_MAX_DEPTH, "maxDepth");
|
|
284
|
+
const maxNodes = positiveInteger(options.maxNodes, DEFAULT_MAX_NODES, "maxNodes");
|
|
285
|
+
const seen = new WeakSet();
|
|
286
|
+
let nodes = 0;
|
|
287
|
+
const visit = (input, key = "", path = [], depth = 0) => {
|
|
288
|
+
if (++nodes > maxNodes) return "[MAX_NODES]";
|
|
289
|
+
if (isSensitiveKey(key) || options.redact?.({
|
|
290
|
+
key,
|
|
291
|
+
path,
|
|
292
|
+
value: input
|
|
293
|
+
})) return "[REDACTED]";
|
|
294
|
+
if (typeof input === "string") return limitString(redactText(input), maxStringLength);
|
|
295
|
+
if (input === null || typeof input === "boolean") return input;
|
|
296
|
+
if (typeof input === "number") return Number.isFinite(input) ? input : String(input);
|
|
297
|
+
if (typeof input === "bigint" || typeof input === "symbol" || typeof input === "function" || typeof input === "undefined") return String(input);
|
|
298
|
+
if (input instanceof Error) return {
|
|
299
|
+
name: limitString(input.name || "Error", maxStringLength),
|
|
300
|
+
message: limitString(redactText(input.message), maxStringLength)
|
|
301
|
+
};
|
|
302
|
+
if (depth >= maxDepth) return "[MAX_DEPTH]";
|
|
303
|
+
if (seen.has(input)) return "[CIRCULAR]";
|
|
304
|
+
seen.add(input);
|
|
305
|
+
try {
|
|
306
|
+
if (Array.isArray(input)) return input.map((item, index) => visit(item, "", [...path, index], depth + 1));
|
|
307
|
+
const output = {};
|
|
308
|
+
for (const [childKey, child] of Object.entries(input)) output[childKey] = visit(child, childKey, [...path, childKey], depth + 1);
|
|
309
|
+
return output;
|
|
310
|
+
} finally {
|
|
311
|
+
seen.delete(input);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
return visit(value);
|
|
315
|
+
}
|
|
316
|
+
function isSensitiveKey(key) {
|
|
317
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
318
|
+
return normalized !== "" && SENSITIVE_KEY_PARTS.some((part) => normalized.includes(part));
|
|
319
|
+
}
|
|
320
|
+
function redactText(value) {
|
|
321
|
+
return value.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [REDACTED]").replace(/([?&](?:access[_-]?token|refresh[_-]?token|client[_-]?secret|api[_-]?key|password|passwd|token|secret)=)[^&#\s]*/gi, "$1[REDACTED]").replace(/\b((?:access[_-]?token|refresh[_-]?token|client[_-]?secret|api[_-]?key|password|passwd|token|secret)\s*[:=]\s*)[^\s,;]+/gi, "$1[REDACTED]");
|
|
322
|
+
}
|
|
323
|
+
function limitString(value, maxLength) {
|
|
324
|
+
if (value.includes("[REDACTED]") && maxLength < 20) return `[REDACTED][TRUNCATED length=${value.length}]`;
|
|
325
|
+
return value.length > maxLength ? `${value.slice(0, maxLength)}[TRUNCATED length=${value.length}]` : value;
|
|
326
|
+
}
|
|
327
|
+
function positiveInteger(value, fallback, name) {
|
|
328
|
+
if (value === void 0) return fallback;
|
|
329
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive integer`);
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/json-schema.ts
|
|
335
|
+
/** Small, dependency-free validator for the JSON Schema subset used by AIGUI definitions. */
|
|
336
|
+
function validateJSONSchema(schema, value) {
|
|
337
|
+
const issues = [];
|
|
338
|
+
validate(schema, value, "$", issues);
|
|
339
|
+
return {
|
|
340
|
+
valid: issues.length === 0,
|
|
341
|
+
issues
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function validate(schema, value, path, issues) {
|
|
345
|
+
if (Object.hasOwn(schema, "const") && !jsonEqual(value, schema.const)) {
|
|
346
|
+
issues.push(`${path} must equal ${describeJSONValue(schema.const)}`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (schema.enum && !schema.enum.some((candidate) => jsonEqual(candidate, value))) {
|
|
350
|
+
issues.push(`${path} must be one of the allowed values`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (schema.type === "object") {
|
|
354
|
+
if (!isJSONObject(value)) {
|
|
355
|
+
issues.push(`${path} must be an object`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
for (const required of schema.required ?? []) if (!Object.hasOwn(value, required)) issues.push(`${path}.${required} is required`);
|
|
359
|
+
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) if (Object.hasOwn(value, key)) validate(childSchema, value[key], `${path}.${key}`, issues);
|
|
360
|
+
if (schema.additionalProperties === false) {
|
|
361
|
+
const allowed = new Set(Object.keys(schema.properties ?? {}));
|
|
362
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) issues.push(`${path}.${key} is not allowed`);
|
|
363
|
+
} else if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) {
|
|
364
|
+
const allowed = new Set(Object.keys(schema.properties ?? {}));
|
|
365
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) validate(schema.additionalProperties, value[key], `${path}.${key}`, issues);
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (schema.type === "array") {
|
|
370
|
+
if (!Array.isArray(value)) {
|
|
371
|
+
issues.push(`${path} must be an array`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (schema.minItems !== void 0 && value.length < schema.minItems) issues.push(`${path} must contain at least ${schema.minItems} items`);
|
|
375
|
+
if (schema.maxItems !== void 0 && value.length > schema.maxItems) issues.push(`${path} must contain at most ${schema.maxItems} items`);
|
|
376
|
+
if (schema.items) value.forEach((item, index) => validate(schema.items, item, `${path}[${index}]`, issues));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (schema.type === "string") {
|
|
380
|
+
if (typeof value !== "string") {
|
|
381
|
+
issues.push(`${path} must be a string`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (schema.minLength !== void 0 && value.length < schema.minLength) issues.push(`${path} must contain at least ${schema.minLength} characters`);
|
|
385
|
+
if (schema.maxLength !== void 0 && value.length > schema.maxLength) issues.push(`${path} must contain at most ${schema.maxLength} characters`);
|
|
386
|
+
if (schema.pattern !== void 0) try {
|
|
387
|
+
if (!new RegExp(schema.pattern).test(value)) issues.push(`${path} must match ${schema.pattern}`);
|
|
388
|
+
} catch {
|
|
389
|
+
issues.push(`${path} has an invalid schema pattern`);
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
394
|
+
if (typeof value !== "number" || !Number.isFinite(value) || schema.type === "integer" && !Number.isInteger(value)) {
|
|
395
|
+
issues.push(`${path} must be ${schema.type === "integer" ? "an integer" : "a number"}`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (schema.minimum !== void 0 && value < schema.minimum) issues.push(`${path} must be at least ${schema.minimum}`);
|
|
399
|
+
if (schema.maximum !== void 0 && value > schema.maximum) issues.push(`${path} must be at most ${schema.maximum}`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (schema.type === "boolean" && typeof value !== "boolean") issues.push(`${path} must be a boolean`);
|
|
403
|
+
if (schema.type === "null" && value !== null) issues.push(`${path} must be null`);
|
|
404
|
+
}
|
|
405
|
+
function isJSONObject(value) {
|
|
406
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
407
|
+
const prototype = Object.getPrototypeOf(value);
|
|
408
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
409
|
+
if (Object.getOwnPropertySymbols(value).length > 0) return false;
|
|
410
|
+
return Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => descriptor.enumerable && "value" in descriptor);
|
|
411
|
+
}
|
|
412
|
+
function jsonEqual(left, right) {
|
|
413
|
+
return compareJSON(left, right, new WeakSet(), new WeakSet());
|
|
414
|
+
}
|
|
415
|
+
function compareJSON(left, right, leftPath, rightPath) {
|
|
416
|
+
if (left === null || right === null) return left === right;
|
|
417
|
+
if (typeof left !== typeof right) return false;
|
|
418
|
+
if (typeof left === "string" || typeof left === "boolean") return left === right;
|
|
419
|
+
if (typeof left === "number") return Number.isFinite(left) && Number.isFinite(right) && left === right;
|
|
420
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
421
|
+
if (leftPath.has(left) || rightPath.has(right)) return false;
|
|
422
|
+
const leftArray = Array.isArray(left);
|
|
423
|
+
if (leftArray !== Array.isArray(right)) return false;
|
|
424
|
+
if (leftArray) {
|
|
425
|
+
if (!isJSONArray(left) || !isJSONArray(right)) return false;
|
|
426
|
+
} else if (!isJSONObject(left) || !isJSONObject(right)) return false;
|
|
427
|
+
leftPath.add(left);
|
|
428
|
+
rightPath.add(right);
|
|
429
|
+
try {
|
|
430
|
+
if (leftArray) {
|
|
431
|
+
const rightArray = right;
|
|
432
|
+
if (left.length !== rightArray.length) return false;
|
|
433
|
+
for (let index = 0; index < left.length; index++) if (!compareJSON(left[index], rightArray[index], leftPath, rightPath)) return false;
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
const leftRecord = left;
|
|
437
|
+
const rightRecord = right;
|
|
438
|
+
const leftKeys = Object.keys(leftRecord);
|
|
439
|
+
const rightKeys = Object.keys(rightRecord);
|
|
440
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
441
|
+
for (const key of leftKeys) {
|
|
442
|
+
if (!Object.hasOwn(rightRecord, key)) return false;
|
|
443
|
+
if (!compareJSON(leftRecord[key], rightRecord[key], leftPath, rightPath)) return false;
|
|
444
|
+
}
|
|
445
|
+
return true;
|
|
446
|
+
} finally {
|
|
447
|
+
leftPath.delete(left);
|
|
448
|
+
rightPath.delete(right);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function describeJSONValue(value) {
|
|
452
|
+
try {
|
|
453
|
+
const serialized = JSON.stringify(value);
|
|
454
|
+
return serialized === void 0 ? "a valid JSON value" : serialized;
|
|
455
|
+
} catch {
|
|
456
|
+
return "a valid JSON value";
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function isJSONArray(value) {
|
|
460
|
+
if (Object.getOwnPropertySymbols(value).length > 0) return false;
|
|
461
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
462
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
463
|
+
if (key === "length") continue;
|
|
464
|
+
if (!isArrayIndex(key) || !descriptor.enumerable || !("value" in descriptor)) return false;
|
|
465
|
+
}
|
|
466
|
+
return Object.keys(value).length === value.length;
|
|
467
|
+
}
|
|
468
|
+
function isArrayIndex(key) {
|
|
469
|
+
const index = Number(key);
|
|
470
|
+
return Number.isInteger(index) && index >= 0 && index < 2 ** 32 - 1 && String(index) === key;
|
|
471
|
+
}
|
|
472
|
+
|
|
163
473
|
//#endregion
|
|
164
474
|
//#region src/card-registry.ts
|
|
165
475
|
var CardRegistry = class {
|
|
@@ -181,19 +491,28 @@ var CardRegistry = class {
|
|
|
181
491
|
complete,
|
|
182
492
|
valid: false
|
|
183
493
|
};
|
|
184
|
-
const valid = complete && this.
|
|
494
|
+
const valid = complete && this.validateDefinition(def, data);
|
|
185
495
|
return {
|
|
186
496
|
data,
|
|
187
497
|
complete,
|
|
188
498
|
valid
|
|
189
499
|
};
|
|
190
500
|
}
|
|
191
|
-
validate(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
501
|
+
validate(type, data) {
|
|
502
|
+
const def = this.cards.get(type);
|
|
503
|
+
return def ? this.validateDefinition(def, data) : false;
|
|
504
|
+
}
|
|
505
|
+
validateDefinition(def, data) {
|
|
506
|
+
try {
|
|
507
|
+
if (def.validate) return def.validate(data);
|
|
508
|
+
if (def.schema) return validateJSONSchema(def.schema, data).valid;
|
|
509
|
+
return true;
|
|
510
|
+
} catch {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
195
513
|
}
|
|
196
514
|
toPromptSpec() {
|
|
515
|
+
if (this.cards.size === 0) return "";
|
|
197
516
|
const lines = ["You can output cards. Format: a ```card:<type> fenced block with JSON inside. Available cards:"];
|
|
198
517
|
for (const def of this.cards.values()) {
|
|
199
518
|
lines.push(`- \`card:${def.type}\`: ${def.description}`);
|
|
@@ -205,6 +524,9 @@ var CardRegistry = class {
|
|
|
205
524
|
}
|
|
206
525
|
return lines.join("\n");
|
|
207
526
|
}
|
|
527
|
+
get size() {
|
|
528
|
+
return this.cards.size;
|
|
529
|
+
}
|
|
208
530
|
toJSONSchema() {
|
|
209
531
|
const properties = {};
|
|
210
532
|
for (const def of this.cards.values()) if (def.schema) properties[def.type] = def.schema;
|
|
@@ -214,53 +536,949 @@ var CardRegistry = class {
|
|
|
214
536
|
};
|
|
215
537
|
}
|
|
216
538
|
};
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
539
|
+
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/card-store.ts
|
|
542
|
+
const CARD_ID_MAX_LENGTH = 256;
|
|
543
|
+
const CARD_JSON_MAX_DEPTH = 32;
|
|
544
|
+
const CARD_JSON_MAX_NODES = 1e4;
|
|
545
|
+
const CARD_PATCH_BATCH_MAX_SIZE = 100;
|
|
546
|
+
var CardStore = class {
|
|
547
|
+
debugSource = "card-store";
|
|
548
|
+
registry;
|
|
549
|
+
cards = new Map();
|
|
550
|
+
lastMutationEpoch = new Map();
|
|
551
|
+
mutationEpoch = 0;
|
|
552
|
+
listeners = new Map();
|
|
553
|
+
allListeners = new Set();
|
|
554
|
+
debug;
|
|
555
|
+
constructor(options = {}) {
|
|
556
|
+
this.registry = options.registry;
|
|
557
|
+
this.debug = new DebugEmitter(this.debugSource, options);
|
|
558
|
+
}
|
|
559
|
+
register(input) {
|
|
560
|
+
validateCardId(input.id);
|
|
561
|
+
validateCardType(input.type);
|
|
562
|
+
assertRecordId(input.id, input.data);
|
|
563
|
+
const existing = this.cards.get(input.id);
|
|
564
|
+
if (existing) {
|
|
565
|
+
if (existing.type !== input.type) throw new CardTypeConflictError(input.id, existing.type, input.type);
|
|
566
|
+
return existing;
|
|
567
|
+
}
|
|
568
|
+
const data = cloneJSON(input.data);
|
|
569
|
+
this.assertValid(input.type, data);
|
|
570
|
+
const record = makeRecord(input.id, input.type, data, 0, { status: "idle" });
|
|
571
|
+
this.cards.set(input.id, record);
|
|
572
|
+
this.lastMutationEpoch.set(input.id, this.nextMutationEpoch());
|
|
573
|
+
this.notify(input.id);
|
|
574
|
+
return record;
|
|
575
|
+
}
|
|
576
|
+
get(id) {
|
|
577
|
+
return this.cards.get(id);
|
|
578
|
+
}
|
|
579
|
+
list() {
|
|
580
|
+
return [...this.cards.values()];
|
|
581
|
+
}
|
|
582
|
+
subscribe(id, listener) {
|
|
583
|
+
let listeners = this.listeners.get(id);
|
|
584
|
+
if (!listeners) {
|
|
585
|
+
listeners = new Set();
|
|
586
|
+
this.listeners.set(id, listeners);
|
|
587
|
+
}
|
|
588
|
+
listeners.add(listener);
|
|
589
|
+
return () => {
|
|
590
|
+
listeners?.delete(listener);
|
|
591
|
+
if (listeners?.size === 0) this.listeners.delete(id);
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
subscribeAll(listener) {
|
|
595
|
+
this.allListeners.add(listener);
|
|
596
|
+
return () => this.allListeners.delete(listener);
|
|
597
|
+
}
|
|
598
|
+
subscribeDebug(listener) {
|
|
599
|
+
return this.debug.subscribe(listener);
|
|
600
|
+
}
|
|
601
|
+
apply(patch) {
|
|
602
|
+
const next = this.preparePatch(this.cards, patch);
|
|
603
|
+
this.cards.set(next.id, next);
|
|
604
|
+
this.lastMutationEpoch.set(next.id, this.nextMutationEpoch());
|
|
605
|
+
this.notify(next.id);
|
|
606
|
+
if (this.debug.active) this.debug.emit("card-store-patch", {
|
|
607
|
+
patch,
|
|
608
|
+
cards: this.list()
|
|
609
|
+
});
|
|
610
|
+
return next;
|
|
611
|
+
}
|
|
612
|
+
applyAll(patches) {
|
|
613
|
+
if (patches.length > CARD_PATCH_BATCH_MAX_SIZE) throw new CardLimitError(`Card patch batches cannot exceed ${CARD_PATCH_BATCH_MAX_SIZE} operations`);
|
|
614
|
+
const nextCards = new Map(this.cards);
|
|
615
|
+
const changed = new Map();
|
|
616
|
+
for (const patch of patches) {
|
|
617
|
+
const next = this.preparePatch(nextCards, patch);
|
|
618
|
+
nextCards.set(next.id, next);
|
|
619
|
+
changed.set(next.id, next);
|
|
620
|
+
}
|
|
621
|
+
this.cards = nextCards;
|
|
622
|
+
for (const id of changed.keys()) this.lastMutationEpoch.set(id, this.nextMutationEpoch());
|
|
623
|
+
for (const id of changed.keys()) this.notifyOne(id);
|
|
624
|
+
if (changed.size) this.notifyAll();
|
|
625
|
+
if (changed.size && this.debug.active) {
|
|
626
|
+
const cards = this.list();
|
|
627
|
+
this.debug.emit("card-store-change", {
|
|
628
|
+
cardIds: [...changed.keys()],
|
|
629
|
+
cards
|
|
630
|
+
});
|
|
631
|
+
this.debug.emit("card-store-patch", {
|
|
632
|
+
patch: {
|
|
633
|
+
op: "batch",
|
|
634
|
+
patches
|
|
635
|
+
},
|
|
636
|
+
cards
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return [...changed.values()];
|
|
640
|
+
}
|
|
641
|
+
delete(id) {
|
|
642
|
+
if (!this.cards.delete(id)) return false;
|
|
643
|
+
this.lastMutationEpoch.delete(id);
|
|
644
|
+
this.nextMutationEpoch();
|
|
645
|
+
this.notify(id);
|
|
224
646
|
return true;
|
|
225
647
|
}
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
|
|
648
|
+
clear() {
|
|
649
|
+
if (this.cards.size === 0) return;
|
|
650
|
+
const ids = [...this.cards.keys()];
|
|
651
|
+
this.cards.clear();
|
|
652
|
+
this.lastMutationEpoch.clear();
|
|
653
|
+
this.nextMutationEpoch();
|
|
654
|
+
for (const id of ids) this.notifyOne(id);
|
|
655
|
+
this.notifyAll();
|
|
656
|
+
if (this.debug.active) this.debug.emit("card-store-change", {
|
|
657
|
+
operation: "clear",
|
|
658
|
+
cardIds: ids,
|
|
659
|
+
cards: []
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
snapshot() {
|
|
663
|
+
return {
|
|
664
|
+
version: 1,
|
|
665
|
+
cards: this.list().map(({ id, type, data, revision }) => ({
|
|
666
|
+
id,
|
|
667
|
+
type,
|
|
668
|
+
data: cloneJSON(data),
|
|
669
|
+
revision
|
|
670
|
+
}))
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
restore(snapshot) {
|
|
674
|
+
if (!snapshot || snapshot.version !== 1 || !Array.isArray(snapshot.cards)) throw new CardSnapshotError("Unsupported card snapshot");
|
|
675
|
+
const restored = new Map();
|
|
676
|
+
for (const input of snapshot.cards) {
|
|
677
|
+
if (!input || typeof input !== "object") throw new CardSnapshotError("Invalid card snapshot record");
|
|
678
|
+
validateCardId(input.id);
|
|
679
|
+
validateCardType(input.type);
|
|
680
|
+
if (!Number.isSafeInteger(input.revision) || input.revision < 0) throw new CardSnapshotError(`Invalid revision for card "${input.id}"`);
|
|
681
|
+
if (restored.has(input.id)) throw new CardSnapshotError(`Duplicate card id "${input.id}" in snapshot`);
|
|
682
|
+
const data = cloneJSON(input.data);
|
|
683
|
+
try {
|
|
684
|
+
assertRecordId(input.id, data);
|
|
685
|
+
} catch (cause) {
|
|
686
|
+
throw new CardSnapshotError(`Card snapshot data id does not match record id "${input.id}"`, { cause });
|
|
687
|
+
}
|
|
688
|
+
this.assertValid(input.type, data);
|
|
689
|
+
restored.set(input.id, makeRecord(input.id, input.type, data, input.revision, { status: "idle" }));
|
|
690
|
+
}
|
|
691
|
+
const ids = new Set([...this.cards.keys(), ...restored.keys()]);
|
|
692
|
+
this.cards = restored;
|
|
693
|
+
this.lastMutationEpoch = new Map([...restored.keys()].map((id) => [id, this.nextMutationEpoch()]));
|
|
694
|
+
if (restored.size === 0 && ids.size > 0) this.nextMutationEpoch();
|
|
695
|
+
for (const id of ids) this.notifyOne(id);
|
|
696
|
+
if (ids.size) this.notifyAll();
|
|
697
|
+
if (ids.size && this.debug.active) this.debug.emit("card-store-change", {
|
|
698
|
+
operation: "restore",
|
|
699
|
+
cardIds: [...ids],
|
|
700
|
+
cards: this.list()
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
beginAction(id, actionId) {
|
|
704
|
+
return this.setAction(id, actionId, {
|
|
705
|
+
status: "loading",
|
|
706
|
+
actionId
|
|
707
|
+
}, true);
|
|
708
|
+
}
|
|
709
|
+
succeedAction(id, actionId) {
|
|
710
|
+
return this.setAction(id, actionId, {
|
|
711
|
+
status: "success",
|
|
712
|
+
actionId
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
failAction(id, actionId, error) {
|
|
716
|
+
return this.setAction(id, actionId, {
|
|
717
|
+
status: "error",
|
|
718
|
+
actionId,
|
|
719
|
+
error: normalizeCardActionError(error)
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
cancelAction(id, actionId) {
|
|
723
|
+
return this.setAction(id, actionId, { status: "idle" });
|
|
724
|
+
}
|
|
725
|
+
isActionCurrent(id, actionId) {
|
|
726
|
+
const action = this.cards.get(id)?.action;
|
|
727
|
+
return action?.status === "loading" && action.actionId === actionId;
|
|
728
|
+
}
|
|
729
|
+
revisions() {
|
|
730
|
+
return new Map([...this.cards].map(([id, card]) => [id, card.revision]));
|
|
731
|
+
}
|
|
732
|
+
captureMutationEpoch() {
|
|
733
|
+
return this.mutationEpoch;
|
|
734
|
+
}
|
|
735
|
+
applyActionResult(result, startedEpoch) {
|
|
736
|
+
const patches = result.op === "batch" ? result.patches : [result];
|
|
737
|
+
if (patches.length > CARD_PATCH_BATCH_MAX_SIZE) throw new CardLimitError(`Card patch batches cannot exceed ${CARD_PATCH_BATCH_MAX_SIZE} operations`);
|
|
738
|
+
for (const cardId of new Set(patches.map((patch) => patch.cardId))) {
|
|
739
|
+
const current = this.cards.get(cardId);
|
|
740
|
+
if (!current) throw new CardNotFoundError(cardId);
|
|
741
|
+
const lastMutationEpoch = this.lastMutationEpoch.get(cardId);
|
|
742
|
+
if (lastMutationEpoch === void 0 || lastMutationEpoch > startedEpoch) throw new CardRevisionConflictError(cardId, startedEpoch, lastMutationEpoch ?? this.mutationEpoch);
|
|
743
|
+
}
|
|
744
|
+
return result.op === "batch" ? this.applyAll(result.patches) : [this.apply(result)];
|
|
745
|
+
}
|
|
746
|
+
preparePatch(cards, patch) {
|
|
747
|
+
assertPatch(patch);
|
|
748
|
+
const current = cards.get(patch.cardId);
|
|
749
|
+
if (!current) throw new CardNotFoundError(patch.cardId);
|
|
750
|
+
if (patch.revision !== void 0 && patch.revision !== current.revision) throw new CardRevisionConflictError(patch.cardId, patch.revision, current.revision);
|
|
751
|
+
const patchData = cloneJSON(patch.data);
|
|
752
|
+
assertPatchId(current.id, patchData);
|
|
753
|
+
const data = patch.op === "replace" ? patchData : mergeJSON(current.data, patchData);
|
|
754
|
+
assertStableId(current.id, current.data, data);
|
|
755
|
+
this.assertValid(current.type, data);
|
|
756
|
+
return makeRecord(current.id, current.type, data, current.revision + 1, current.action);
|
|
757
|
+
}
|
|
758
|
+
assertValid(type, data) {
|
|
759
|
+
if (this.registry && !this.registry.validate(type, data)) throw new CardValidationError(type);
|
|
760
|
+
}
|
|
761
|
+
setAction(id, actionId, action, start = false) {
|
|
762
|
+
const current = this.cards.get(id);
|
|
763
|
+
if (!current) throw new CardNotFoundError(id);
|
|
764
|
+
if (!start && (current.action.status !== "loading" || current.action.actionId !== actionId)) return false;
|
|
765
|
+
this.cards.set(id, makeRecord(current.id, current.type, current.data, current.revision, action));
|
|
766
|
+
this.notify(id);
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
nextMutationEpoch() {
|
|
770
|
+
return ++this.mutationEpoch;
|
|
771
|
+
}
|
|
772
|
+
notify(id) {
|
|
773
|
+
this.notifyOne(id);
|
|
774
|
+
this.notifyAll();
|
|
775
|
+
if (this.debug.active) this.debug.emit("card-store-change", {
|
|
776
|
+
cardId: id,
|
|
777
|
+
cards: this.list()
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
notifyOne(id) {
|
|
781
|
+
const card = this.cards.get(id);
|
|
782
|
+
for (const listener of this.listeners.get(id) ?? []) safeCall(listener, card);
|
|
783
|
+
}
|
|
784
|
+
notifyAll() {
|
|
785
|
+
const cards = this.list();
|
|
786
|
+
for (const listener of this.allListeners) safeCall(listener, cards);
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
var CardStoreError = class extends Error {
|
|
790
|
+
constructor(message, options) {
|
|
791
|
+
super(message, options);
|
|
792
|
+
this.name = new.target.name;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
var CardNotFoundError = class extends CardStoreError {
|
|
796
|
+
constructor(id) {
|
|
797
|
+
super(`Card "${id}" was not found`);
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
var CardTypeConflictError = class extends CardStoreError {
|
|
801
|
+
constructor(id, currentType, nextType) {
|
|
802
|
+
super(`Card "${id}" is already registered as "${currentType}", not "${nextType}"`);
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
var CardValidationError = class extends CardStoreError {
|
|
806
|
+
constructor(type) {
|
|
807
|
+
super(`Card data is invalid for type "${type}"`);
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
var CardRevisionConflictError = class extends CardStoreError {
|
|
811
|
+
constructor(id, expected, actual) {
|
|
812
|
+
super(`Card "${id}" revision conflict: expected ${expected}, actual ${actual}`);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
var CardJSONError = class extends CardStoreError {};
|
|
816
|
+
var CardLimitError = class extends CardStoreError {};
|
|
817
|
+
var CardSnapshotError = class extends CardStoreError {};
|
|
818
|
+
function isCardPatchResult(value) {
|
|
819
|
+
if (!isPlainObject(value) || typeof value.op !== "string") return false;
|
|
820
|
+
if (value.op === "batch") return Array.isArray(value.patches) && value.patches.every(isCardPatch);
|
|
821
|
+
return isCardPatch(value);
|
|
822
|
+
}
|
|
823
|
+
function isCardPatch(value) {
|
|
824
|
+
return isPlainObject(value) && (value.op === "merge" || value.op === "replace") && typeof value.cardId === "string" && Object.hasOwn(value, "data") && (value.revision === void 0 || Number.isSafeInteger(value.revision));
|
|
825
|
+
}
|
|
826
|
+
function assertPatch(patch) {
|
|
827
|
+
if (!isCardPatch(patch)) throw new CardStoreError("Invalid card patch");
|
|
828
|
+
validateCardId(patch.cardId);
|
|
829
|
+
if (patch.revision !== void 0 && patch.revision < 0) throw new CardStoreError("Card patch revision must be non-negative");
|
|
830
|
+
}
|
|
831
|
+
function validateCardId(id) {
|
|
832
|
+
if (typeof id !== "string" || id.trim().length === 0 || id.length > CARD_ID_MAX_LENGTH) throw new CardStoreError(`Card id must be a non-empty string up to ${CARD_ID_MAX_LENGTH} characters`);
|
|
833
|
+
}
|
|
834
|
+
function validateCardType(type) {
|
|
835
|
+
if (typeof type !== "string" || type.trim().length === 0) throw new CardStoreError("Card type must be a non-empty string");
|
|
836
|
+
}
|
|
837
|
+
function cloneJSON(value) {
|
|
838
|
+
let nodes = 0;
|
|
839
|
+
const path = new WeakSet();
|
|
840
|
+
const clone = (input, depth) => {
|
|
841
|
+
nodes++;
|
|
842
|
+
if (nodes > CARD_JSON_MAX_NODES) throw new CardLimitError(`Card JSON cannot exceed ${CARD_JSON_MAX_NODES} nodes`);
|
|
843
|
+
if (depth > CARD_JSON_MAX_DEPTH) throw new CardLimitError(`Card JSON cannot exceed depth ${CARD_JSON_MAX_DEPTH}`);
|
|
844
|
+
if (input === null || typeof input === "string" || typeof input === "boolean") return input;
|
|
845
|
+
if (typeof input === "number" && Number.isFinite(input)) return input;
|
|
846
|
+
if (typeof input !== "object") throw new CardJSONError("Card data must contain only JSON values");
|
|
847
|
+
if (path.has(input)) throw new CardJSONError("Card data cannot contain cycles");
|
|
848
|
+
path.add(input);
|
|
849
|
+
try {
|
|
850
|
+
if (Array.isArray(input)) {
|
|
851
|
+
if (Object.keys(input).length !== input.length) throw new CardJSONError("Card arrays must not be sparse");
|
|
852
|
+
return Object.freeze(input.map((item) => clone(item, depth + 1)));
|
|
853
|
+
}
|
|
854
|
+
if (!isPlainObject(input)) throw new CardJSONError("Card objects must be plain JSON objects");
|
|
855
|
+
const output = {};
|
|
856
|
+
for (const key of Object.keys(input)) {
|
|
857
|
+
if (isDangerousKey(key)) throw new CardJSONError(`Card data contains dangerous key "${key}"`);
|
|
858
|
+
Object.defineProperty(output, key, {
|
|
859
|
+
value: clone(input[key], depth + 1),
|
|
860
|
+
enumerable: true,
|
|
861
|
+
writable: false,
|
|
862
|
+
configurable: false
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
return Object.freeze(output);
|
|
866
|
+
} finally {
|
|
867
|
+
path.delete(input);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
return clone(value, 0);
|
|
871
|
+
}
|
|
872
|
+
function mergeJSON(current, patch) {
|
|
873
|
+
if (!isPlainObject(current) || !isPlainObject(patch)) return patch;
|
|
874
|
+
const output = {};
|
|
875
|
+
for (const key of Object.keys(current)) output[key] = current[key];
|
|
876
|
+
for (const key of Object.keys(patch)) output[key] = mergeJSON(current[key], patch[key]);
|
|
877
|
+
return cloneJSON(output);
|
|
878
|
+
}
|
|
879
|
+
function assertPatchId(cardId, data) {
|
|
880
|
+
if (isPlainObject(data) && Object.hasOwn(data, "id") && data.id !== cardId) throw new CardStoreError(`Card patch cannot change id from "${cardId}"`);
|
|
881
|
+
}
|
|
882
|
+
function assertRecordId(cardId, data) {
|
|
883
|
+
if (isPlainObject(data) && Object.hasOwn(data, "id") && data.id !== cardId) throw new CardStoreError(`Card data id must match record id "${cardId}"`);
|
|
884
|
+
}
|
|
885
|
+
function assertStableId(cardId, current, next) {
|
|
886
|
+
const currentHasId = isPlainObject(current) && Object.hasOwn(current, "id");
|
|
887
|
+
const nextHasId = isPlainObject(next) && Object.hasOwn(next, "id");
|
|
888
|
+
if (currentHasId !== nextHasId || nextHasId && next.id !== cardId) throw new CardStoreError(`Card patch cannot change id from "${cardId}"`);
|
|
889
|
+
}
|
|
890
|
+
function makeRecord(id, type, data, revision, action) {
|
|
891
|
+
return Object.freeze({
|
|
892
|
+
id,
|
|
893
|
+
type,
|
|
894
|
+
data,
|
|
895
|
+
revision,
|
|
896
|
+
action: Object.freeze(action)
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
function normalizeCardActionError(error) {
|
|
900
|
+
if (error instanceof Error) return Object.freeze({
|
|
901
|
+
name: error.name || "Error",
|
|
902
|
+
message: error.message
|
|
903
|
+
});
|
|
904
|
+
return Object.freeze({
|
|
905
|
+
name: "Error",
|
|
906
|
+
message: typeof error === "string" ? error : "Unknown error"
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function isPlainObject(value) {
|
|
910
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
911
|
+
const prototype = Object.getPrototypeOf(value);
|
|
912
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
913
|
+
if (Object.getOwnPropertySymbols(value).length) return false;
|
|
914
|
+
return Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => descriptor.enumerable && "value" in descriptor);
|
|
915
|
+
}
|
|
916
|
+
function isDangerousKey(key) {
|
|
917
|
+
return key === "__proto__" || key === "prototype" || key === "constructor";
|
|
918
|
+
}
|
|
919
|
+
function safeCall(listener, ...args) {
|
|
920
|
+
try {
|
|
921
|
+
listener(...args);
|
|
922
|
+
} catch {}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/actions.ts
|
|
927
|
+
let nextRuntimeId = 0;
|
|
928
|
+
var ActionRegistry = class {
|
|
929
|
+
actions = new Map();
|
|
930
|
+
register(definition, options = {}) {
|
|
931
|
+
if (typeof definition?.type !== "string" || definition.type.trim() === "") throw new TypeError("Action type must be a non-empty string");
|
|
932
|
+
if (typeof definition.run !== "function") throw new TypeError(`Action "${definition.type}" run must be a function`);
|
|
933
|
+
if (this.actions.has(definition.type) && !options.override) throw new ActionAlreadyRegisteredError(definition.type);
|
|
934
|
+
this.actions.set(definition.type, definition);
|
|
935
|
+
}
|
|
936
|
+
has(type) {
|
|
937
|
+
return this.actions.has(type);
|
|
938
|
+
}
|
|
939
|
+
get(type) {
|
|
940
|
+
return this.actions.get(type);
|
|
941
|
+
}
|
|
942
|
+
list() {
|
|
943
|
+
return [...this.actions.values()];
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
var ActionRuntime = class {
|
|
947
|
+
debugSource = "action-runtime";
|
|
948
|
+
registry;
|
|
949
|
+
cardStore;
|
|
950
|
+
defaultTimeoutMs;
|
|
951
|
+
onActionStart;
|
|
952
|
+
onActionSuccess;
|
|
953
|
+
onActionError;
|
|
954
|
+
states = new Map();
|
|
955
|
+
pending = new Map();
|
|
956
|
+
listeners = new Set();
|
|
957
|
+
defaultOwner = {};
|
|
958
|
+
runtimeId = ++nextRuntimeId;
|
|
959
|
+
generation = 0;
|
|
960
|
+
nextActionId = 0;
|
|
961
|
+
destroyed = false;
|
|
962
|
+
debug;
|
|
963
|
+
constructor(options) {
|
|
964
|
+
this.debug = new DebugEmitter(this.debugSource, options);
|
|
965
|
+
this.registry = options.registry;
|
|
966
|
+
this.cardStore = options.cardStore;
|
|
967
|
+
this.defaultTimeoutMs = options.timeoutMs;
|
|
968
|
+
this.onActionStart = options.onActionStart;
|
|
969
|
+
this.onActionSuccess = options.onActionSuccess;
|
|
970
|
+
this.onActionError = options.onActionError;
|
|
971
|
+
}
|
|
972
|
+
dispatch(request, options = {}) {
|
|
973
|
+
if (this.destroyed) return Promise.reject(new ActionDestroyedError());
|
|
974
|
+
const key = getActionKey(request.type, request.cardType, request.cardId);
|
|
975
|
+
const owner = options.owner ?? this.defaultOwner;
|
|
976
|
+
const existing = this.pending.get(key)?.get(owner);
|
|
977
|
+
if (existing) return existing.promise;
|
|
978
|
+
const actionId = `r${this.runtimeId}:${request.type}:${++this.nextActionId}`;
|
|
979
|
+
const event = {
|
|
980
|
+
key,
|
|
981
|
+
type: request.type,
|
|
982
|
+
params: request.params,
|
|
983
|
+
actionId,
|
|
984
|
+
cardType: request.cardType,
|
|
985
|
+
cardId: request.cardId
|
|
986
|
+
};
|
|
987
|
+
if (this.debug.active) this.debug.emit("action-dispatched", { ...event });
|
|
988
|
+
const definition = this.registry.get(request.type);
|
|
989
|
+
if (!definition) return this.rejectPreflight(event, new ActionNotFoundError(request.type));
|
|
990
|
+
if (definition.schema !== void 0) try {
|
|
991
|
+
const validation = validateJSONSchema(definition.schema, request.params);
|
|
992
|
+
if (!validation.valid) return this.rejectPreflight(event, new ActionValidationError(request.type, validation.issues));
|
|
993
|
+
} catch (cause) {
|
|
994
|
+
return this.rejectPreflight(event, new ActionExecutionError(request.type, cause));
|
|
995
|
+
}
|
|
996
|
+
let startedMutationEpoch;
|
|
997
|
+
try {
|
|
998
|
+
startedMutationEpoch = this.cardStore?.captureMutationEpoch();
|
|
999
|
+
} catch (cause) {
|
|
1000
|
+
return this.rejectPreflight(event, new ActionExecutionError(request.type, cause));
|
|
1001
|
+
}
|
|
1002
|
+
if (request.cardId && this.cardStore) try {
|
|
1003
|
+
this.cardStore.beginAction(request.cardId, actionId);
|
|
1004
|
+
} catch (cause) {
|
|
1005
|
+
return this.rejectPreflight(event, new ActionExecutionError(request.type, cause));
|
|
1006
|
+
}
|
|
1007
|
+
const generation = this.generation;
|
|
1008
|
+
const controller = new AbortController();
|
|
1009
|
+
const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
|
|
1010
|
+
let timer;
|
|
1011
|
+
let timedOut = false;
|
|
1012
|
+
let settled = false;
|
|
1013
|
+
let resolvePromise;
|
|
1014
|
+
let rejectPromise;
|
|
1015
|
+
const promise = new Promise((resolve, reject) => {
|
|
1016
|
+
resolvePromise = resolve;
|
|
1017
|
+
rejectPromise = reject;
|
|
1018
|
+
});
|
|
1019
|
+
const state = {
|
|
1020
|
+
status: "pending",
|
|
1021
|
+
key,
|
|
1022
|
+
type: request.type,
|
|
1023
|
+
cardType: request.cardType,
|
|
1024
|
+
cardId: request.cardId,
|
|
1025
|
+
actionId
|
|
1026
|
+
};
|
|
1027
|
+
const pending = {
|
|
1028
|
+
controller,
|
|
1029
|
+
promise
|
|
1030
|
+
};
|
|
1031
|
+
const runtime = this;
|
|
1032
|
+
let cleanupExternalSignal = () => {};
|
|
1033
|
+
let cleanupAbortSignal = () => {};
|
|
1034
|
+
let owners = this.pending.get(key);
|
|
1035
|
+
if (!owners) {
|
|
1036
|
+
owners = new Map();
|
|
1037
|
+
this.pending.set(key, owners);
|
|
1038
|
+
}
|
|
1039
|
+
owners.set(owner, pending);
|
|
1040
|
+
const cleanup = () => {
|
|
1041
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1042
|
+
cleanupExternalSignal();
|
|
1043
|
+
cleanupAbortSignal();
|
|
1044
|
+
const currentOwners = this.pending.get(key);
|
|
1045
|
+
if (currentOwners?.get(owner) === pending) {
|
|
1046
|
+
currentOwners.delete(owner);
|
|
1047
|
+
if (currentOwners.size === 0) this.pending.delete(key);
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
const finishSuccess = (result) => {
|
|
1051
|
+
if (settled) return;
|
|
1052
|
+
const wasPublic = this.isPublic(key, actionId, generation);
|
|
1053
|
+
const canAffectCard = !request.cardId || !this.cardStore || this.cardStore.isActionCurrent(request.cardId, actionId);
|
|
1054
|
+
const canApplyResult = request.cardId ? canAffectCard : wasPublic;
|
|
1055
|
+
if (canApplyResult && this.cardStore && startedMutationEpoch !== void 0 && isCardPatchResult(result)) try {
|
|
1056
|
+
this.cardStore.applyActionResult(result, startedMutationEpoch);
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
finishError(error);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
settled = true;
|
|
1062
|
+
cleanup();
|
|
1063
|
+
if (wasPublic) {
|
|
1064
|
+
this.commit({
|
|
1065
|
+
status: "success",
|
|
1066
|
+
key,
|
|
1067
|
+
type: request.type,
|
|
1068
|
+
cardType: request.cardType,
|
|
1069
|
+
cardId: request.cardId,
|
|
1070
|
+
actionId,
|
|
1071
|
+
result
|
|
1072
|
+
});
|
|
1073
|
+
callEventHandler(this.onActionSuccess, {
|
|
1074
|
+
...event,
|
|
1075
|
+
result
|
|
1076
|
+
});
|
|
1077
|
+
if (this.debug.active) this.debug.emit("action-success", {
|
|
1078
|
+
...event,
|
|
1079
|
+
result
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
if (request.cardId && this.cardStore && canAffectCard) settleCardAction(() => this.cardStore?.succeedAction(request.cardId, actionId));
|
|
1083
|
+
resolvePromise(result);
|
|
1084
|
+
};
|
|
1085
|
+
function finishError(cause) {
|
|
1086
|
+
if (settled) return;
|
|
1087
|
+
settled = true;
|
|
1088
|
+
const wasPublic = runtime.isPublic(key, actionId, generation);
|
|
1089
|
+
cleanup();
|
|
1090
|
+
const error = normalizeError(request.type, cause, controller.signal.aborted, timedOut, timeoutMs);
|
|
1091
|
+
if (request.cardId && runtime.cardStore) settleCardAction(() => error instanceof ActionAbortedError ? runtime.cardStore?.cancelAction(request.cardId, actionId) : runtime.cardStore?.failAction(request.cardId, actionId, error));
|
|
1092
|
+
if (wasPublic) {
|
|
1093
|
+
const state$1 = error instanceof ActionAbortedError ? {
|
|
1094
|
+
status: "cancelled",
|
|
1095
|
+
key,
|
|
1096
|
+
type: request.type,
|
|
1097
|
+
cardType: request.cardType,
|
|
1098
|
+
actionId,
|
|
1099
|
+
error
|
|
1100
|
+
} : {
|
|
1101
|
+
status: "error",
|
|
1102
|
+
key,
|
|
1103
|
+
type: request.type,
|
|
1104
|
+
cardType: request.cardType,
|
|
1105
|
+
cardId: request.cardId,
|
|
1106
|
+
actionId,
|
|
1107
|
+
error
|
|
1108
|
+
};
|
|
1109
|
+
if (request.cardId) state$1.cardId = request.cardId;
|
|
1110
|
+
runtime.commit(state$1);
|
|
1111
|
+
callEventHandler(runtime.onActionError, {
|
|
1112
|
+
...event,
|
|
1113
|
+
error
|
|
1114
|
+
});
|
|
1115
|
+
if (runtime.debug.active) runtime.debug.emit("action-error", {
|
|
1116
|
+
...event,
|
|
1117
|
+
error
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
rejectPromise(error);
|
|
1121
|
+
}
|
|
1122
|
+
this.commit(state);
|
|
1123
|
+
callEventHandler(this.onActionStart, event);
|
|
1124
|
+
cleanupExternalSignal = forwardAbort(options.signal, controller);
|
|
1125
|
+
cleanupAbortSignal = listenForAbort(controller.signal, (cause) => finishError(cause));
|
|
1126
|
+
if (timeoutMs !== void 0 && timeoutMs > 0 && !settled) timer = setTimeout(() => {
|
|
1127
|
+
timedOut = true;
|
|
1128
|
+
controller.abort(new ActionTimeoutError(request.type, timeoutMs));
|
|
1129
|
+
}, timeoutMs);
|
|
1130
|
+
if (controller.signal.aborted || this.generation !== generation || this.destroyed) {
|
|
1131
|
+
finishError(controller.signal.reason ?? new ActionAbortedError(request.type));
|
|
1132
|
+
return promise;
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
const result = definition.run(request.params, {
|
|
1136
|
+
signal: controller.signal,
|
|
1137
|
+
actionId,
|
|
1138
|
+
cardType: request.cardType,
|
|
1139
|
+
cardId: request.cardId
|
|
1140
|
+
});
|
|
1141
|
+
Promise.resolve(result).then(finishSuccess, finishError);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
finishError(error);
|
|
1144
|
+
}
|
|
1145
|
+
return promise;
|
|
1146
|
+
}
|
|
1147
|
+
getState(key) {
|
|
1148
|
+
return this.states.get(key) ?? getIdleActionState(key);
|
|
1149
|
+
}
|
|
1150
|
+
/** Check the runtime allowlist without exposing executable action definitions. */
|
|
1151
|
+
hasAction(type) {
|
|
1152
|
+
return !this.destroyed && this.registry.has(type);
|
|
1153
|
+
}
|
|
1154
|
+
subscribe(listener) {
|
|
1155
|
+
if (this.destroyed) return () => {};
|
|
1156
|
+
this.listeners.add(listener);
|
|
1157
|
+
return () => this.listeners.delete(listener);
|
|
1158
|
+
}
|
|
1159
|
+
subscribeDebug(listener) {
|
|
1160
|
+
return this.debug.subscribe(listener);
|
|
1161
|
+
}
|
|
1162
|
+
cancel(key) {
|
|
1163
|
+
const actions = this.pending.get(key);
|
|
1164
|
+
if (!actions?.size) return false;
|
|
1165
|
+
const type = this.states.get(key)?.type ?? getIdleActionState(key).type;
|
|
1166
|
+
for (const action of [...actions.values()]) action.controller.abort(new ActionAbortedError(type));
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
reset() {
|
|
1170
|
+
const oldStates = [...this.states.values()];
|
|
1171
|
+
this.generation++;
|
|
1172
|
+
const actions = [...this.pending.values()].flatMap((owners) => [...owners.values()]);
|
|
1173
|
+
for (const action of actions) action.controller.abort(new ActionAbortedError("Action"));
|
|
1174
|
+
this.pending.clear();
|
|
1175
|
+
this.states.clear();
|
|
1176
|
+
for (const state of oldStates) this.notify(idleStateFrom(state));
|
|
1177
|
+
}
|
|
1178
|
+
destroy() {
|
|
1179
|
+
if (this.destroyed) return;
|
|
1180
|
+
this.reset();
|
|
1181
|
+
this.destroyed = true;
|
|
1182
|
+
this.listeners.clear();
|
|
1183
|
+
}
|
|
1184
|
+
rejectPreflight(event, error) {
|
|
1185
|
+
const errorState = {
|
|
1186
|
+
status: "error",
|
|
1187
|
+
key: event.key,
|
|
1188
|
+
type: event.type,
|
|
1189
|
+
cardType: event.cardType,
|
|
1190
|
+
cardId: event.cardId,
|
|
1191
|
+
actionId: event.actionId,
|
|
1192
|
+
error
|
|
1193
|
+
};
|
|
1194
|
+
this.commit(errorState);
|
|
1195
|
+
callEventHandler(this.onActionError, {
|
|
1196
|
+
...event,
|
|
1197
|
+
error
|
|
1198
|
+
});
|
|
1199
|
+
if (this.debug.active) this.debug.emit("action-error", {
|
|
1200
|
+
...event,
|
|
1201
|
+
error
|
|
1202
|
+
});
|
|
1203
|
+
return Promise.reject(error);
|
|
1204
|
+
}
|
|
1205
|
+
isPublic(key, actionId, generation) {
|
|
1206
|
+
return !this.destroyed && this.generation === generation && this.states.get(key)?.actionId === actionId;
|
|
1207
|
+
}
|
|
1208
|
+
commit(state) {
|
|
1209
|
+
this.states.set(state.key, state);
|
|
1210
|
+
if (this.debug.active) this.debug.emit("action-state", { ...state });
|
|
1211
|
+
this.notify(state);
|
|
1212
|
+
}
|
|
1213
|
+
notify(state) {
|
|
1214
|
+
for (const listener of this.listeners) try {
|
|
1215
|
+
listener(state);
|
|
1216
|
+
} catch {}
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
function createActionRuntime(options) {
|
|
1220
|
+
return new ActionRuntime(options);
|
|
1221
|
+
}
|
|
1222
|
+
var ActionRuntimeError = class extends Error {
|
|
1223
|
+
constructor(message, options) {
|
|
1224
|
+
super(message, options);
|
|
1225
|
+
this.name = new.target.name;
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
var ActionAlreadyRegisteredError = class extends ActionRuntimeError {
|
|
1229
|
+
constructor(type) {
|
|
1230
|
+
super(`Action "${type}" is already registered`);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
var ActionNotFoundError = class extends ActionRuntimeError {
|
|
1234
|
+
constructor(type) {
|
|
1235
|
+
super(`Action "${type}" is not registered`);
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
var ActionValidationError = class extends ActionRuntimeError {
|
|
1239
|
+
constructor(actionType, issues) {
|
|
1240
|
+
super(`Action "${actionType}" parameters are invalid: ${issues.join(", ")}`);
|
|
1241
|
+
this.actionType = actionType;
|
|
1242
|
+
this.issues = issues;
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
var ActionExecutionError = class extends ActionRuntimeError {
|
|
1246
|
+
constructor(type, cause) {
|
|
1247
|
+
super(`Action "${type}" failed`, { cause });
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
var ActionAbortedError = class extends ActionRuntimeError {
|
|
1251
|
+
constructor(type) {
|
|
1252
|
+
super(`Action "${type}" was cancelled`);
|
|
229
1253
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
1254
|
+
};
|
|
1255
|
+
var ActionTimeoutError = class extends ActionRuntimeError {
|
|
1256
|
+
constructor(type, timeoutMs) {
|
|
1257
|
+
super(`Action "${type}" timed out after ${timeoutMs}ms`);
|
|
1258
|
+
this.timeoutMs = timeoutMs;
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
var ActionDestroyedError = class extends ActionRuntimeError {
|
|
1262
|
+
constructor() {
|
|
1263
|
+
super("Action runtime has been destroyed");
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
function getActionKey(type, cardType, cardId) {
|
|
1267
|
+
if (cardId !== void 0) return `::${JSON.stringify([
|
|
1268
|
+
cardType ?? null,
|
|
1269
|
+
type,
|
|
1270
|
+
cardId
|
|
1271
|
+
])}`;
|
|
1272
|
+
if (!type.includes(":") && !cardType?.includes(":")) return cardType ? `${cardType}:${type}` : type;
|
|
1273
|
+
return `::${JSON.stringify([cardType ?? null, type])}`;
|
|
1274
|
+
}
|
|
1275
|
+
function getIdleActionState(key) {
|
|
1276
|
+
if (key.startsWith("::")) try {
|
|
1277
|
+
const [cardType, type, cardId] = JSON.parse(key.slice(2));
|
|
1278
|
+
if (cardId !== void 0) return cardType === null ? {
|
|
1279
|
+
status: "idle",
|
|
1280
|
+
key,
|
|
1281
|
+
type,
|
|
1282
|
+
cardId
|
|
1283
|
+
} : {
|
|
1284
|
+
status: "idle",
|
|
1285
|
+
key,
|
|
1286
|
+
cardType,
|
|
1287
|
+
type,
|
|
1288
|
+
cardId
|
|
1289
|
+
};
|
|
1290
|
+
return cardType === null ? {
|
|
1291
|
+
status: "idle",
|
|
1292
|
+
key,
|
|
1293
|
+
type
|
|
1294
|
+
} : {
|
|
1295
|
+
status: "idle",
|
|
1296
|
+
key,
|
|
1297
|
+
cardType,
|
|
1298
|
+
type
|
|
1299
|
+
};
|
|
1300
|
+
} catch {
|
|
1301
|
+
return {
|
|
1302
|
+
status: "idle",
|
|
1303
|
+
key,
|
|
1304
|
+
type: key
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
const separator = key.indexOf(":");
|
|
1308
|
+
return separator === -1 ? {
|
|
1309
|
+
status: "idle",
|
|
1310
|
+
key,
|
|
1311
|
+
type: key
|
|
1312
|
+
} : {
|
|
1313
|
+
status: "idle",
|
|
1314
|
+
key,
|
|
1315
|
+
cardType: key.slice(0, separator),
|
|
1316
|
+
type: key.slice(separator + 1)
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function idleStateFrom(state) {
|
|
1320
|
+
return {
|
|
1321
|
+
status: "idle",
|
|
1322
|
+
key: state.key,
|
|
1323
|
+
type: state.type,
|
|
1324
|
+
...state.cardType === void 0 ? {} : { cardType: state.cardType },
|
|
1325
|
+
...state.cardId === void 0 ? {} : { cardId: state.cardId }
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function listenForAbort(signal, onAbort) {
|
|
1329
|
+
const abort = () => onAbort(signal.reason);
|
|
1330
|
+
if (signal.aborted) abort();
|
|
1331
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
1332
|
+
return () => signal.removeEventListener("abort", abort);
|
|
1333
|
+
}
|
|
1334
|
+
function forwardAbort(signal, controller) {
|
|
1335
|
+
if (!signal) return () => {};
|
|
1336
|
+
const abort = () => controller.abort(signal.reason);
|
|
1337
|
+
if (signal.aborted) abort();
|
|
1338
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
1339
|
+
return () => signal.removeEventListener("abort", abort);
|
|
1340
|
+
}
|
|
1341
|
+
function normalizeError(type, cause, aborted, timedOut, timeoutMs) {
|
|
1342
|
+
if (cause instanceof ActionRuntimeError) return cause;
|
|
1343
|
+
if (timedOut) return new ActionTimeoutError(type, timeoutMs ?? 0);
|
|
1344
|
+
if (aborted || isAbortError(cause)) return new ActionAbortedError(type);
|
|
1345
|
+
return new ActionExecutionError(type, cause);
|
|
1346
|
+
}
|
|
1347
|
+
function isAbortError(error) {
|
|
1348
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
1349
|
+
}
|
|
1350
|
+
function callEventHandler(handler, event) {
|
|
1351
|
+
try {
|
|
1352
|
+
handler?.(event);
|
|
1353
|
+
} catch {}
|
|
1354
|
+
}
|
|
1355
|
+
function settleCardAction(settle) {
|
|
1356
|
+
try {
|
|
1357
|
+
settle();
|
|
1358
|
+
} catch {}
|
|
234
1359
|
}
|
|
235
1360
|
|
|
236
1361
|
//#endregion
|
|
237
1362
|
//#region src/plugins.ts
|
|
238
1363
|
/** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
|
|
239
|
-
function collectNodeRenderers(plugins = []) {
|
|
1364
|
+
function collectNodeRenderers(plugins = [], debugOptions = {}) {
|
|
240
1365
|
const map = {};
|
|
241
|
-
|
|
1366
|
+
const target = debugOptions.debugTarget;
|
|
1367
|
+
const debug = target ? void 0 : debugOptions.debug === true ? new DebugEmitter("renderer", debugOptions) : void 0;
|
|
1368
|
+
for (const p of plugins) for (const [k, render] of Object.entries(p.nodeRenderers ?? {})) {
|
|
1369
|
+
if (!(target?.debugEnabled || debugOptions.debug === true)) {
|
|
1370
|
+
map[k] = render;
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
map[k] = (node) => {
|
|
1374
|
+
const emit = (type, data) => target?.emitDebug(type, data) ?? debug?.emit(type, data);
|
|
1375
|
+
emit("plugin-render-started", {
|
|
1376
|
+
plugin: p.name,
|
|
1377
|
+
nodeType: node.type,
|
|
1378
|
+
nodeKey: node.key
|
|
1379
|
+
});
|
|
1380
|
+
try {
|
|
1381
|
+
const output = render(node);
|
|
1382
|
+
if (isPromise(output)) return output.then((resolved) => {
|
|
1383
|
+
emit("async-output-resolved", {
|
|
1384
|
+
plugin: p.name,
|
|
1385
|
+
nodeType: node.type,
|
|
1386
|
+
nodeKey: node.key,
|
|
1387
|
+
outputKind: resolved.kind
|
|
1388
|
+
});
|
|
1389
|
+
emit("plugin-render-completed", {
|
|
1390
|
+
plugin: p.name,
|
|
1391
|
+
nodeType: node.type,
|
|
1392
|
+
nodeKey: node.key,
|
|
1393
|
+
outputKind: resolved.kind
|
|
1394
|
+
});
|
|
1395
|
+
return resolved;
|
|
1396
|
+
}, (error) => {
|
|
1397
|
+
emit("async-output-rejected", {
|
|
1398
|
+
plugin: p.name,
|
|
1399
|
+
nodeType: node.type,
|
|
1400
|
+
nodeKey: node.key,
|
|
1401
|
+
error
|
|
1402
|
+
});
|
|
1403
|
+
emit("plugin-render-failed", {
|
|
1404
|
+
plugin: p.name,
|
|
1405
|
+
nodeType: node.type,
|
|
1406
|
+
nodeKey: node.key,
|
|
1407
|
+
error
|
|
1408
|
+
});
|
|
1409
|
+
throw error;
|
|
1410
|
+
});
|
|
1411
|
+
emit("plugin-render-completed", {
|
|
1412
|
+
plugin: p.name,
|
|
1413
|
+
nodeType: node.type,
|
|
1414
|
+
nodeKey: node.key,
|
|
1415
|
+
outputKind: output.kind
|
|
1416
|
+
});
|
|
1417
|
+
return output;
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
emit("plugin-render-failed", {
|
|
1420
|
+
plugin: p.name,
|
|
1421
|
+
nodeType: node.type,
|
|
1422
|
+
nodeKey: node.key,
|
|
1423
|
+
error
|
|
1424
|
+
});
|
|
1425
|
+
throw error;
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
242
1429
|
return map;
|
|
243
1430
|
}
|
|
244
1431
|
/** The set of node types claimed by the given plugins. */
|
|
245
1432
|
function pluginNodeTypes(plugins = []) {
|
|
246
1433
|
return new Set(Object.keys(collectNodeRenderers(plugins)));
|
|
247
1434
|
}
|
|
1435
|
+
function isPromise(value) {
|
|
1436
|
+
return typeof value?.then === "function";
|
|
1437
|
+
}
|
|
248
1438
|
|
|
249
1439
|
//#endregion
|
|
250
1440
|
//#region src/parser.ts
|
|
251
1441
|
/** Build a parser that turns markdown source into a flat list of ASTNodes. */
|
|
252
1442
|
function createParser(options = {}) {
|
|
1443
|
+
const parse = createParserWithMetadata(options);
|
|
1444
|
+
return (src, rawSrc = src) => parse(src, rawSrc).nodes;
|
|
1445
|
+
}
|
|
1446
|
+
/** Build a parser that also reports the source range for each top-level block. */
|
|
1447
|
+
function createParserWithMetadata(options = {}) {
|
|
253
1448
|
const md = new MarkdownIt({
|
|
254
1449
|
html: true,
|
|
255
1450
|
linkify: true
|
|
256
1451
|
});
|
|
257
1452
|
options.configureMd?.(md);
|
|
258
1453
|
for (const plugin of options.plugins ?? []) plugin.extendParser?.(md);
|
|
1454
|
+
const hasParserExtensions = Boolean(options.configureMd || options.plugins?.some((plugin) => plugin.extendParser));
|
|
259
1455
|
const pluginTypes = pluginNodeTypes(options.plugins);
|
|
260
|
-
|
|
261
|
-
|
|
1456
|
+
const completeness = new Map();
|
|
1457
|
+
for (const plugin of options.plugins ?? []) for (const type of Object.keys(plugin.nodeRenderers ?? {})) if (plugin.isBlockComplete) completeness.set(type, plugin.isBlockComplete);
|
|
1458
|
+
return (src, rawSrc = src, sourceOffset = 0) => {
|
|
1459
|
+
const env = {};
|
|
1460
|
+
const tokens = md.parse(normalizeLineEndings(src), env);
|
|
262
1461
|
const nodes = [];
|
|
263
|
-
|
|
1462
|
+
const blocks = [];
|
|
1463
|
+
const lineOffsets = getLineOffsets(rawSrc);
|
|
1464
|
+
const startSlots = new Map();
|
|
1465
|
+
const addNode = (node, map) => {
|
|
1466
|
+
const range = sourceRange(map, rawSrc.length, lineOffsets, sourceOffset);
|
|
1467
|
+
const previous = blocks.at(-1);
|
|
1468
|
+
const block = previous && previous.start === range.start && previous.end === range.end ? previous : void 0;
|
|
1469
|
+
const slot = startSlots.get(range.start) ?? 0;
|
|
1470
|
+
startSlots.set(range.start, slot + 1);
|
|
1471
|
+
nodes.push({
|
|
1472
|
+
key: `${range.start}:${slot}`,
|
|
1473
|
+
...node
|
|
1474
|
+
});
|
|
1475
|
+
if (block) block.nodeEnd = nodes.length;
|
|
1476
|
+
else blocks.push({
|
|
1477
|
+
...range,
|
|
1478
|
+
nodeStart: nodes.length - 1,
|
|
1479
|
+
nodeEnd: nodes.length
|
|
1480
|
+
});
|
|
1481
|
+
};
|
|
264
1482
|
for (let i = 0; i < tokens.length; i++) {
|
|
265
1483
|
const t = tokens[i];
|
|
266
1484
|
if (t.type === "fence") {
|
|
@@ -268,79 +1486,84 @@ function createParser(options = {}) {
|
|
|
268
1486
|
if (info.startsWith("card:") && options.registry) {
|
|
269
1487
|
const cardType = info.slice(5);
|
|
270
1488
|
const res = options.registry.parse(cardType, t.content);
|
|
271
|
-
|
|
272
|
-
|
|
1489
|
+
const complete = res.complete && isClosedFence(rawSrc, t.map, t.markup);
|
|
1490
|
+
const idResult = complete && res.valid ? getCardId(res.data) : void 0;
|
|
1491
|
+
addNode({
|
|
273
1492
|
type: "card",
|
|
274
1493
|
card: {
|
|
275
1494
|
type: cardType,
|
|
276
1495
|
data: res.data,
|
|
277
|
-
complete
|
|
278
|
-
valid: res.valid
|
|
1496
|
+
complete,
|
|
1497
|
+
valid: complete && res.valid && idResult !== false,
|
|
1498
|
+
...typeof idResult === "string" ? { id: idResult } : {}
|
|
279
1499
|
}
|
|
280
|
-
});
|
|
281
|
-
} else if (pluginTypes.has(info))
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
1500
|
+
}, t.map);
|
|
1501
|
+
} else if (pluginTypes.has(info)) {
|
|
1502
|
+
const predicate = completeness.get(info);
|
|
1503
|
+
const fenceComplete = isClosedFence(rawSrc, t.map, t.markup);
|
|
1504
|
+
let complete = fenceComplete;
|
|
1505
|
+
if (complete && predicate) try {
|
|
1506
|
+
complete = predicate(info, t.content);
|
|
1507
|
+
} catch {
|
|
1508
|
+
complete = false;
|
|
1509
|
+
}
|
|
1510
|
+
addNode({
|
|
1511
|
+
type: info,
|
|
1512
|
+
content: t.content,
|
|
1513
|
+
attrs: { info },
|
|
1514
|
+
complete
|
|
1515
|
+
}, t.map);
|
|
1516
|
+
} else addNode({
|
|
289
1517
|
type: "code",
|
|
290
1518
|
tag: "code",
|
|
291
1519
|
attrs: info ? { lang: info } : void 0,
|
|
292
1520
|
content: t.content
|
|
293
|
-
});
|
|
1521
|
+
}, t.map);
|
|
294
1522
|
continue;
|
|
295
1523
|
}
|
|
296
1524
|
if (t.type === "hr") {
|
|
297
|
-
|
|
298
|
-
key: `${index++}:hr`,
|
|
1525
|
+
addNode({
|
|
299
1526
|
type: "hr",
|
|
300
1527
|
tag: "hr"
|
|
301
|
-
});
|
|
1528
|
+
}, t.map);
|
|
302
1529
|
continue;
|
|
303
1530
|
}
|
|
304
1531
|
if (t.type === "code_block") {
|
|
305
|
-
|
|
306
|
-
key: `${index++}:code`,
|
|
1532
|
+
addNode({
|
|
307
1533
|
type: "code",
|
|
308
1534
|
tag: "code",
|
|
309
1535
|
content: t.content
|
|
310
|
-
});
|
|
1536
|
+
}, t.map);
|
|
311
1537
|
continue;
|
|
312
1538
|
}
|
|
313
1539
|
if (t.type === "html_block") {
|
|
314
|
-
|
|
315
|
-
key: `${index++}:html`,
|
|
1540
|
+
addNode({
|
|
316
1541
|
type: "html",
|
|
317
1542
|
content: t.content
|
|
318
|
-
});
|
|
1543
|
+
}, t.map);
|
|
319
1544
|
continue;
|
|
320
1545
|
}
|
|
321
1546
|
if (t.type === "heading_open") {
|
|
322
1547
|
const inline = tokens[i + 1];
|
|
323
1548
|
const raw = inline?.content ?? "";
|
|
324
|
-
|
|
325
|
-
key: `${index++}:heading`,
|
|
1549
|
+
addNode({
|
|
326
1550
|
type: "heading",
|
|
327
1551
|
tag: t.tag,
|
|
328
1552
|
content: raw,
|
|
329
|
-
html: md.renderInline(
|
|
330
|
-
});
|
|
1553
|
+
html: md.renderer.renderInline(inline?.children ?? [], md.options, env)
|
|
1554
|
+
}, t.map);
|
|
331
1555
|
i += 2;
|
|
332
1556
|
continue;
|
|
333
1557
|
}
|
|
334
1558
|
if (t.type === "paragraph_open") {
|
|
335
1559
|
const inline = tokens[i + 1];
|
|
336
1560
|
const raw = inline?.content ?? "";
|
|
337
|
-
|
|
338
|
-
key: `${index++}:paragraph`,
|
|
1561
|
+
addNode({
|
|
339
1562
|
type: "paragraph",
|
|
340
1563
|
tag: "p",
|
|
341
1564
|
content: raw,
|
|
342
|
-
html: md.renderInline(
|
|
343
|
-
});
|
|
1565
|
+
html: md.renderer.renderInline(inline?.children ?? [], md.options, env)
|
|
1566
|
+
}, t.map);
|
|
344
1567
|
i += 2;
|
|
345
1568
|
continue;
|
|
346
1569
|
}
|
|
@@ -356,54 +1579,132 @@ function createParser(options = {}) {
|
|
|
356
1579
|
}
|
|
357
1580
|
}
|
|
358
1581
|
const slice = tokens.slice(i, j + 1);
|
|
359
|
-
|
|
360
|
-
key: `${index++}:${t.type}`,
|
|
1582
|
+
addNode({
|
|
361
1583
|
type: "html",
|
|
362
|
-
content: md.renderer.render(slice, md.options,
|
|
363
|
-
});
|
|
1584
|
+
content: md.renderer.render(slice, md.options, env)
|
|
1585
|
+
}, t.map);
|
|
364
1586
|
i = j;
|
|
365
1587
|
continue;
|
|
366
1588
|
}
|
|
367
1589
|
if (t.block && t.level === 0) {
|
|
368
|
-
|
|
369
|
-
key: `${index++}:${t.type}`,
|
|
1590
|
+
addNode({
|
|
370
1591
|
type: "html",
|
|
371
|
-
content: md.renderer.render([t], md.options,
|
|
372
|
-
});
|
|
1592
|
+
content: md.renderer.render([t], md.options, env)
|
|
1593
|
+
}, t.map);
|
|
373
1594
|
continue;
|
|
374
1595
|
}
|
|
375
1596
|
}
|
|
376
|
-
return
|
|
1597
|
+
return {
|
|
1598
|
+
nodes,
|
|
1599
|
+
blocks,
|
|
1600
|
+
incrementalSafe: !hasParserExtensions && !hasReferenceSyntax(rawSrc)
|
|
1601
|
+
};
|
|
377
1602
|
};
|
|
378
1603
|
}
|
|
1604
|
+
function getCardId(data) {
|
|
1605
|
+
if (typeof data !== "object" || data === null || Array.isArray(data) || !Object.hasOwn(data, "id")) return void 0;
|
|
1606
|
+
const id = data.id;
|
|
1607
|
+
return typeof id === "string" && id.trim().length > 0 && id.length <= 256 ? id : false;
|
|
1608
|
+
}
|
|
1609
|
+
function getLineOffsets(src) {
|
|
1610
|
+
const offsets = [0];
|
|
1611
|
+
for (let i = 0; i < src.length; i++) if (src[i] === "\r") {
|
|
1612
|
+
if (src[i + 1] === "\n") i++;
|
|
1613
|
+
offsets.push(i + 1);
|
|
1614
|
+
} else if (src[i] === "\n") offsets.push(i + 1);
|
|
1615
|
+
offsets.push(src.length);
|
|
1616
|
+
return offsets;
|
|
1617
|
+
}
|
|
1618
|
+
function sourceRange(map, sourceLength, lineOffsets, sourceOffset) {
|
|
1619
|
+
if (!map) return {
|
|
1620
|
+
start: sourceOffset,
|
|
1621
|
+
end: sourceOffset + sourceLength
|
|
1622
|
+
};
|
|
1623
|
+
return {
|
|
1624
|
+
start: sourceOffset + Math.min(lineOffsets[map[0]] ?? sourceLength, sourceLength),
|
|
1625
|
+
end: sourceOffset + Math.min(lineOffsets[map[1]] ?? sourceLength, sourceLength)
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
function hasReferenceSyntax(src) {
|
|
1629
|
+
return /^ {0,3}\[[^\]\n]+\]:/m.test(src) || /\[[^\]\n]+\]\s*\[[^\]\n]*\]/.test(src) || /(^|[^!])\[[^\]\n]+\](?![([])/m.test(src);
|
|
1630
|
+
}
|
|
1631
|
+
function isClosedFence(src, map, markup) {
|
|
1632
|
+
if (!map) return false;
|
|
1633
|
+
const line = src.split(/\r\n|\r|\n/)[map[1] - 1]?.trim() ?? "";
|
|
1634
|
+
if (line.length < markup.length || line[0] !== markup[0]) return false;
|
|
1635
|
+
return line.split("").every((char) => char === markup[0]);
|
|
1636
|
+
}
|
|
1637
|
+
function normalizeLineEndings(src) {
|
|
1638
|
+
return src.replace(/\r\n|\r/g, "\n");
|
|
1639
|
+
}
|
|
379
1640
|
|
|
380
1641
|
//#endregion
|
|
381
1642
|
//#region src/diff.ts
|
|
382
1643
|
/** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
|
|
383
1644
|
function diffAst(prev, next) {
|
|
384
1645
|
const patches = [];
|
|
385
|
-
const prevByKey = new Map(prev.map((n) => [n.key, n]));
|
|
386
1646
|
const nextKeys = new Set(next.map((n) => n.key));
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
op: "
|
|
391
|
-
index
|
|
392
|
-
node
|
|
1647
|
+
const working = [...prev];
|
|
1648
|
+
for (let index = working.length - 1; index >= 0; index--) if (!nextKeys.has(working[index].key)) {
|
|
1649
|
+
patches.push({
|
|
1650
|
+
op: "remove",
|
|
1651
|
+
key: working[index].key
|
|
393
1652
|
});
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
1653
|
+
working.splice(index, 1);
|
|
1654
|
+
}
|
|
1655
|
+
for (let index = 0; index < next.length; index++) {
|
|
1656
|
+
const node = next[index];
|
|
1657
|
+
const currentIndex = working.findIndex((candidate) => candidate.key === node.key);
|
|
1658
|
+
if (currentIndex === -1) {
|
|
1659
|
+
patches.push({
|
|
1660
|
+
op: "insert",
|
|
1661
|
+
index,
|
|
1662
|
+
node
|
|
1663
|
+
});
|
|
1664
|
+
working.splice(index, 0, node);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
if (currentIndex !== index) {
|
|
1668
|
+
patches.push({
|
|
1669
|
+
op: "move",
|
|
1670
|
+
key: node.key,
|
|
1671
|
+
index
|
|
1672
|
+
});
|
|
1673
|
+
const [moved] = working.splice(currentIndex, 1);
|
|
1674
|
+
working.splice(index, 0, moved);
|
|
1675
|
+
}
|
|
1676
|
+
if (!nodeEqual(working[index], node)) {
|
|
1677
|
+
patches.push({
|
|
1678
|
+
op: "update",
|
|
1679
|
+
key: node.key,
|
|
1680
|
+
node
|
|
1681
|
+
});
|
|
1682
|
+
working[index] = node;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
404
1685
|
return patches;
|
|
405
1686
|
}
|
|
1687
|
+
/** Apply patches in order, primarily for framework adapters and verification. */
|
|
1688
|
+
function applyPatches(nodes, patches) {
|
|
1689
|
+
const next = [...nodes];
|
|
1690
|
+
for (const patch of patches) if (patch.op === "insert") next.splice(patch.index, 0, patch.node);
|
|
1691
|
+
else if (patch.op === "remove") {
|
|
1692
|
+
const index = next.findIndex((node) => node.key === patch.key);
|
|
1693
|
+
if (index !== -1) next.splice(index, 1);
|
|
1694
|
+
} else if (patch.op === "move") {
|
|
1695
|
+
const index = next.findIndex((node) => node.key === patch.key);
|
|
1696
|
+
if (index !== -1) {
|
|
1697
|
+
const [node] = next.splice(index, 1);
|
|
1698
|
+
next.splice(patch.index, 0, node);
|
|
1699
|
+
}
|
|
1700
|
+
} else {
|
|
1701
|
+
const index = next.findIndex((node) => node.key === patch.key);
|
|
1702
|
+
if (index !== -1) next[index] = patch.node;
|
|
1703
|
+
}
|
|
1704
|
+
return next;
|
|
1705
|
+
}
|
|
406
1706
|
function nodeEqual(a, b) {
|
|
1707
|
+
if (a === b) return true;
|
|
407
1708
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
408
1709
|
}
|
|
409
1710
|
|
|
@@ -415,166 +1716,434 @@ function nodeEqual(a, b) {
|
|
|
415
1716
|
* the resulting patches via `onPatch`.
|
|
416
1717
|
*/
|
|
417
1718
|
var Renderer = class {
|
|
1719
|
+
debugSource = "renderer";
|
|
418
1720
|
buffer = "";
|
|
419
1721
|
prevAst = [];
|
|
420
1722
|
parse;
|
|
1723
|
+
parsed;
|
|
421
1724
|
options;
|
|
422
1725
|
sanitize;
|
|
1726
|
+
generation = 0;
|
|
1727
|
+
activeFeed;
|
|
1728
|
+
renderScheduled = false;
|
|
1729
|
+
scheduleGeneration = 0;
|
|
1730
|
+
debug;
|
|
423
1731
|
constructor(options = {}) {
|
|
424
1732
|
this.options = options;
|
|
425
|
-
this.
|
|
1733
|
+
this.debug = new DebugEmitter(this.debugSource, options);
|
|
1734
|
+
this.sanitize = options.sanitize === false ? false : typeof options.sanitize === "object" ? options.sanitize : {};
|
|
426
1735
|
if (options.registry) for (const plugin of options.plugins ?? []) for (const card of plugin.cards ?? []) options.registry.register(card);
|
|
427
|
-
this.parse =
|
|
1736
|
+
this.parse = createParserWithMetadata({
|
|
428
1737
|
registry: options.registry,
|
|
429
1738
|
plugins: options.plugins
|
|
430
1739
|
});
|
|
431
1740
|
}
|
|
1741
|
+
get debugEnabled() {
|
|
1742
|
+
return this.debug.available;
|
|
1743
|
+
}
|
|
1744
|
+
emitDebug(type, data = {}) {
|
|
1745
|
+
this.debug.emit(type, data);
|
|
1746
|
+
}
|
|
432
1747
|
push(chunk) {
|
|
433
1748
|
this.buffer += chunk;
|
|
434
|
-
this.
|
|
1749
|
+
if (this.debug.active) this.debug.emit("chunk-received", {
|
|
1750
|
+
chunk,
|
|
1751
|
+
length: chunk.length,
|
|
1752
|
+
bufferLength: this.buffer.length
|
|
1753
|
+
});
|
|
1754
|
+
this.scheduleRender();
|
|
435
1755
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1756
|
+
subscribeDebug(listener) {
|
|
1757
|
+
return this.debug.subscribe(listener);
|
|
1758
|
+
}
|
|
1759
|
+
async feed(source, options = {}) {
|
|
1760
|
+
const generation = ++this.generation;
|
|
1761
|
+
this.cancelActiveFeed(createAbortError("Superseded by a newer feed"));
|
|
1762
|
+
if (this.debug.active) this.debug.emit("feed-started", { generation });
|
|
1763
|
+
const decoder = new TextDecoder();
|
|
1764
|
+
const signal = options.signal;
|
|
1765
|
+
if (signal?.aborted) throw abortReason$1(signal);
|
|
1766
|
+
let aborted = false;
|
|
1767
|
+
let abortError;
|
|
1768
|
+
const abort = () => {
|
|
1769
|
+
aborted = true;
|
|
1770
|
+
abortError = abortReason$1(signal);
|
|
1771
|
+
if (this.activeFeed?.generation === generation) this.cancelActiveFeed(abortError);
|
|
1772
|
+
};
|
|
1773
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1774
|
+
const consume = (chunk) => {
|
|
1775
|
+
if (generation !== this.generation || aborted) return;
|
|
1776
|
+
const text = typeof chunk === "string" ? decoder.decode() + chunk : decoder.decode(chunk, { stream: true });
|
|
1777
|
+
if (text) this.push(text);
|
|
1778
|
+
};
|
|
1779
|
+
try {
|
|
1780
|
+
if (isReadableStream$2(source)) {
|
|
1781
|
+
const reader = source.getReader();
|
|
1782
|
+
let cancelled = false;
|
|
1783
|
+
this.activeFeed = {
|
|
1784
|
+
generation,
|
|
1785
|
+
cancel: async (reason) => {
|
|
1786
|
+
if (cancelled) return;
|
|
1787
|
+
cancelled = true;
|
|
1788
|
+
await reader.cancel(reason);
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
try {
|
|
1792
|
+
for (;;) {
|
|
1793
|
+
const { done, value } = await reader.read();
|
|
1794
|
+
if (done || generation !== this.generation || aborted) break;
|
|
1795
|
+
if (value != null) consume(value);
|
|
1796
|
+
}
|
|
1797
|
+
} finally {
|
|
1798
|
+
reader.releaseLock();
|
|
1799
|
+
}
|
|
1800
|
+
} else {
|
|
1801
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
1802
|
+
let returned = false;
|
|
1803
|
+
this.activeFeed = {
|
|
1804
|
+
generation,
|
|
1805
|
+
cancel: async () => {
|
|
1806
|
+
if (returned) return;
|
|
1807
|
+
returned = true;
|
|
1808
|
+
await iterator.return?.();
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
try {
|
|
1812
|
+
for (;;) {
|
|
1813
|
+
const { done, value } = await iterator.next();
|
|
1814
|
+
if (done || generation !== this.generation || aborted) break;
|
|
1815
|
+
consume(value);
|
|
1816
|
+
}
|
|
1817
|
+
} finally {
|
|
1818
|
+
if ((generation !== this.generation || aborted) && !returned) {
|
|
1819
|
+
returned = true;
|
|
1820
|
+
await iterator.return?.();
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (generation === this.generation && !aborted) {
|
|
1825
|
+
const tail = decoder.decode();
|
|
1826
|
+
if (tail) this.push(tail);
|
|
1827
|
+
this.flush();
|
|
1828
|
+
}
|
|
1829
|
+
if (aborted) {
|
|
1830
|
+
if (this.debug.active) this.debug.emit("feed-cancelled", {
|
|
1831
|
+
generation,
|
|
1832
|
+
error: abortError
|
|
1833
|
+
});
|
|
1834
|
+
throw abortError;
|
|
1835
|
+
}
|
|
1836
|
+
if (generation === this.generation && this.debug.active) this.debug.emit("feed-completed", {
|
|
1837
|
+
generation,
|
|
1838
|
+
bufferLength: this.buffer.length
|
|
1839
|
+
});
|
|
1840
|
+
} finally {
|
|
1841
|
+
if (!aborted && generation !== this.generation && this.debug.active) this.debug.emit("feed-cancelled", {
|
|
1842
|
+
generation,
|
|
1843
|
+
reason: "superseded-or-reset"
|
|
1844
|
+
});
|
|
1845
|
+
signal?.removeEventListener("abort", abort);
|
|
1846
|
+
if (this.activeFeed?.generation === generation) this.activeFeed = void 0;
|
|
446
1847
|
}
|
|
447
1848
|
}
|
|
448
1849
|
reset() {
|
|
1850
|
+
this.generation++;
|
|
1851
|
+
this.cancelActiveFeed(createAbortError("Renderer reset"));
|
|
1852
|
+
this.activeFeed = void 0;
|
|
449
1853
|
this.buffer = "";
|
|
1854
|
+
this.renderScheduled = false;
|
|
1855
|
+
this.scheduleGeneration++;
|
|
1856
|
+
const patches = diffAst(this.prevAst, []);
|
|
450
1857
|
this.prevAst = [];
|
|
1858
|
+
this.parsed = void 0;
|
|
1859
|
+
this.options.onPatch?.(patches, []);
|
|
1860
|
+
if (this.debug.active) this.debug.emit("renderer-reset", { patches });
|
|
1861
|
+
}
|
|
1862
|
+
/** Immediately render pending buffered input, bypassing the scheduler. */
|
|
1863
|
+
flush() {
|
|
1864
|
+
if (!this.renderScheduled) return;
|
|
1865
|
+
this.renderScheduled = false;
|
|
1866
|
+
this.scheduleGeneration++;
|
|
1867
|
+
this.render();
|
|
1868
|
+
}
|
|
1869
|
+
scheduleRender() {
|
|
1870
|
+
if (!this.options.scheduler) {
|
|
1871
|
+
this.render();
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (this.renderScheduled) return;
|
|
1875
|
+
this.renderScheduled = true;
|
|
1876
|
+
const scheduledGeneration = ++this.scheduleGeneration;
|
|
1877
|
+
this.options.scheduler(() => {
|
|
1878
|
+
if (!this.renderScheduled || scheduledGeneration !== this.scheduleGeneration) return;
|
|
1879
|
+
this.renderScheduled = false;
|
|
1880
|
+
this.render();
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
cancelActiveFeed(reason) {
|
|
1884
|
+
const active = this.activeFeed;
|
|
1885
|
+
if (!active) return;
|
|
1886
|
+
Promise.resolve(active.cancel(reason)).catch(() => {});
|
|
451
1887
|
}
|
|
452
1888
|
render() {
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
|
|
1889
|
+
const debug = this.debug.active;
|
|
1890
|
+
const renderStarted = debug ? now() : 0;
|
|
1891
|
+
const previous = this.parsed;
|
|
1892
|
+
let next;
|
|
1893
|
+
let mode = "full";
|
|
1894
|
+
if (previous?.incrementalSafe && previous.blocks.length > 1) {
|
|
1895
|
+
const mutable = previous.blocks.at(-1);
|
|
1896
|
+
const stableBlocks = previous.blocks.slice(0, -1);
|
|
1897
|
+
const stableNodeEnd = mutable.nodeStart;
|
|
1898
|
+
const rawTail = this.buffer.slice(mutable.start);
|
|
1899
|
+
const repaired = repairMarkdown(rawTail);
|
|
1900
|
+
if (debug) this.debug.emit("markdown-repaired", {
|
|
1901
|
+
mode: "mutable-tail",
|
|
1902
|
+
raw: rawTail,
|
|
1903
|
+
rawLength: rawTail.length,
|
|
1904
|
+
repaired,
|
|
1905
|
+
repairedLength: repaired.length,
|
|
1906
|
+
sourceOffset: mutable.start
|
|
1907
|
+
});
|
|
1908
|
+
const parseStarted = debug ? now() : 0;
|
|
1909
|
+
const tail = this.parse(repaired, rawTail, mutable.start);
|
|
1910
|
+
if (debug) this.debug.emit("mutable-tail-reparsed", {
|
|
1911
|
+
sourceOffset: mutable.start,
|
|
1912
|
+
durationMs: now() - parseStarted
|
|
1913
|
+
});
|
|
1914
|
+
if (tail.incrementalSafe) {
|
|
1915
|
+
mode = "mutable-tail";
|
|
1916
|
+
if (this.sanitize !== false) this.sanitizeNodesWithDebug(tail.nodes);
|
|
1917
|
+
next = {
|
|
1918
|
+
nodes: [...previous.nodes.slice(0, stableNodeEnd), ...tail.nodes],
|
|
1919
|
+
blocks: [...stableBlocks, ...tail.blocks.map((block) => ({
|
|
1920
|
+
...block,
|
|
1921
|
+
nodeStart: block.nodeStart + stableNodeEnd,
|
|
1922
|
+
nodeEnd: block.nodeEnd + stableNodeEnd
|
|
1923
|
+
}))],
|
|
1924
|
+
incrementalSafe: true
|
|
1925
|
+
};
|
|
1926
|
+
if (debug) this.debug.emit("stable-prefix-committed", {
|
|
1927
|
+
blocks: stableBlocks.length,
|
|
1928
|
+
nodes: stableNodeEnd
|
|
1929
|
+
});
|
|
1930
|
+
} else {
|
|
1931
|
+
const fullRepaired = repairMarkdown(this.buffer);
|
|
1932
|
+
if (debug) this.debug.emit("markdown-repaired", {
|
|
1933
|
+
mode: "full",
|
|
1934
|
+
raw: this.buffer,
|
|
1935
|
+
rawLength: this.buffer.length,
|
|
1936
|
+
repaired: fullRepaired,
|
|
1937
|
+
repairedLength: fullRepaired.length
|
|
1938
|
+
});
|
|
1939
|
+
const fullParseStarted = debug ? now() : 0;
|
|
1940
|
+
next = this.parse(fullRepaired, this.buffer);
|
|
1941
|
+
if (debug) this.debug.emit("parse-completed", {
|
|
1942
|
+
mode: "full",
|
|
1943
|
+
durationMs: now() - fullParseStarted
|
|
1944
|
+
});
|
|
1945
|
+
if (this.sanitize !== false) this.sanitizeNodesWithDebug(next.nodes);
|
|
1946
|
+
}
|
|
1947
|
+
} else {
|
|
1948
|
+
const repaired = repairMarkdown(this.buffer);
|
|
1949
|
+
if (debug) this.debug.emit("markdown-repaired", {
|
|
1950
|
+
mode: "full",
|
|
1951
|
+
raw: this.buffer,
|
|
1952
|
+
rawLength: this.buffer.length,
|
|
1953
|
+
repaired,
|
|
1954
|
+
repairedLength: repaired.length
|
|
1955
|
+
});
|
|
1956
|
+
const parseStarted = debug ? now() : 0;
|
|
1957
|
+
next = this.parse(repaired, this.buffer);
|
|
1958
|
+
if (debug) this.debug.emit("parse-completed", {
|
|
1959
|
+
mode: "full",
|
|
1960
|
+
durationMs: now() - parseStarted
|
|
1961
|
+
});
|
|
1962
|
+
if (this.sanitize !== false) this.sanitizeNodesWithDebug(next.nodes);
|
|
1963
|
+
}
|
|
1964
|
+
const nextAst = next.nodes;
|
|
1965
|
+
const diffStarted = debug ? now() : 0;
|
|
456
1966
|
const patches = diffAst(this.prevAst, nextAst);
|
|
1967
|
+
const diffDurationMs = debug ? now() - diffStarted : 0;
|
|
1968
|
+
this.parsed = next;
|
|
457
1969
|
this.prevAst = nextAst;
|
|
1970
|
+
const patchDispatchStarted = debug ? now() : 0;
|
|
458
1971
|
if (patches.length > 0) this.options.onPatch?.(patches, nextAst);
|
|
1972
|
+
const patchDispatchDurationMs = debug ? now() - patchDispatchStarted : 0;
|
|
1973
|
+
if (debug) {
|
|
1974
|
+
this.debug.emit("ast-snapshot", {
|
|
1975
|
+
mode,
|
|
1976
|
+
nodes: nextAst
|
|
1977
|
+
});
|
|
1978
|
+
this.debug.emit("ast-patches", {
|
|
1979
|
+
patches,
|
|
1980
|
+
durationMs: diffDurationMs
|
|
1981
|
+
});
|
|
1982
|
+
this.debug.emit("patch-dispatched", {
|
|
1983
|
+
patches: patches.length,
|
|
1984
|
+
durationMs: patchDispatchDurationMs,
|
|
1985
|
+
renderDurationMs: now() - renderStarted
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
sanitizeNodesWithDebug(nodes) {
|
|
1990
|
+
if (this.sanitize === false) return;
|
|
1991
|
+
if (!this.debug.active) {
|
|
1992
|
+
sanitizeNodes(nodes, this.sanitize);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const inputSize = nodeMarkupSize(nodes);
|
|
1996
|
+
const started = now();
|
|
1997
|
+
sanitizeNodes(nodes, this.sanitize);
|
|
1998
|
+
this.debug.emit("sanitizer-completed", {
|
|
1999
|
+
inputSize,
|
|
2000
|
+
outputSize: nodeMarkupSize(nodes),
|
|
2001
|
+
durationMs: now() - started
|
|
2002
|
+
});
|
|
459
2003
|
}
|
|
460
2004
|
};
|
|
461
2005
|
/**
|
|
462
2006
|
* Recursively sanitize node markup in place: the content of `html` nodes and
|
|
463
2007
|
* the rendered inline `html` field carried by any node.
|
|
464
2008
|
*/
|
|
465
|
-
function sanitizeNodes(nodes) {
|
|
2009
|
+
function sanitizeNodes(nodes, options) {
|
|
466
2010
|
for (const node of nodes) {
|
|
467
|
-
if (node.type === "html" && typeof node.content === "string") node.content = sanitizeHtml(node.content);
|
|
468
|
-
if (node.html) node.html = sanitizeHtml(node.html);
|
|
469
|
-
if (node.children) sanitizeNodes(node.children);
|
|
2011
|
+
if (node.type === "html" && typeof node.content === "string") node.content = sanitizeHtml(node.content, options);
|
|
2012
|
+
if (node.html) node.html = sanitizeHtml(node.html, options);
|
|
2013
|
+
if (node.children) sanitizeNodes(node.children, options);
|
|
470
2014
|
}
|
|
471
2015
|
}
|
|
2016
|
+
function isReadableStream$2(source) {
|
|
2017
|
+
return "getReader" in source && typeof source.getReader === "function";
|
|
2018
|
+
}
|
|
2019
|
+
function abortReason$1(signal) {
|
|
2020
|
+
return signal?.reason ?? createAbortError("The operation was aborted");
|
|
2021
|
+
}
|
|
2022
|
+
function createAbortError(message) {
|
|
2023
|
+
const error = new Error(message);
|
|
2024
|
+
error.name = "AbortError";
|
|
2025
|
+
return error;
|
|
2026
|
+
}
|
|
2027
|
+
function now() {
|
|
2028
|
+
return typeof performance === "undefined" ? Date.now() : performance.now();
|
|
2029
|
+
}
|
|
2030
|
+
function nodeMarkupSize(nodes) {
|
|
2031
|
+
let size = 0;
|
|
2032
|
+
for (const node of nodes) {
|
|
2033
|
+
size += node.content?.length ?? 0;
|
|
2034
|
+
size += node.html?.length ?? 0;
|
|
2035
|
+
if (node.children) size += nodeMarkupSize(node.children);
|
|
2036
|
+
}
|
|
2037
|
+
return size;
|
|
2038
|
+
}
|
|
472
2039
|
|
|
473
2040
|
//#endregion
|
|
474
2041
|
//#region src/stream-router.ts
|
|
475
2042
|
const DEFAULT_CHANNEL = "content";
|
|
476
|
-
/**
|
|
477
|
-
* Demultiplexes one incoming stream into multiple named channels so different
|
|
478
|
-
* UI regions can update independently (e.g. `progress` + `content` from a single
|
|
479
|
-
* SSE). Framing is auto-detected per line and supports:
|
|
480
|
-
*
|
|
481
|
-
* 1. JSON envelope: `{"ch":"<channel>","delta":"text"}` (a text delta) or
|
|
482
|
-
* `{"ch":"<channel>","data":<any>}` (a structured value). May appear bare or
|
|
483
|
-
* after a `data: ` prefix.
|
|
484
|
-
* 2. SSE `event: <name>` sets the channel for the following `data:` line(s).
|
|
485
|
-
* 3. A plain `data: <text>` line with no `ch` and no preceding `event:` is a
|
|
486
|
-
* text delta on the default `content` channel.
|
|
487
|
-
*/
|
|
2043
|
+
/** Demultiplex JSON-lines and standards-compliant SSE into named channels. */
|
|
488
2044
|
var StreamRouter = class {
|
|
489
2045
|
sinks = new Map();
|
|
490
2046
|
handlers = new Map();
|
|
491
|
-
/**
|
|
2047
|
+
/** Most recently received SSE `id` field. */
|
|
2048
|
+
lastEventId = "";
|
|
2049
|
+
/** Most recently received valid non-negative SSE reconnection delay. */
|
|
2050
|
+
retry;
|
|
492
2051
|
channel(name, sink) {
|
|
493
2052
|
this.sinks.set(name, sink);
|
|
494
2053
|
return this;
|
|
495
2054
|
}
|
|
496
|
-
/** Bind a handler to a channel: data values (and deltas as strings) call it. */
|
|
497
2055
|
on(name, handler) {
|
|
498
2056
|
this.handlers.set(name, handler);
|
|
499
2057
|
return this;
|
|
500
2058
|
}
|
|
501
|
-
/**
|
|
502
|
-
* Consume a stream to completion, dispatching each parsed line to its channel.
|
|
503
|
-
* Accepts an async iterable of strings, or a `ReadableStream` of either strings
|
|
504
|
-
* or `Uint8Array` bytes (decoded as UTF-8).
|
|
505
|
-
*/
|
|
506
2059
|
async feed(source) {
|
|
507
2060
|
let buffer = "";
|
|
508
|
-
let
|
|
2061
|
+
let eventName = "";
|
|
2062
|
+
let dataLines = [];
|
|
2063
|
+
const decoder = new TextDecoder();
|
|
2064
|
+
const dispatchEvent = () => {
|
|
2065
|
+
if (dataLines.length === 0) {
|
|
2066
|
+
eventName = "";
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
const payload = dataLines.join("\n");
|
|
2070
|
+
const channel = eventName || DEFAULT_CHANNEL;
|
|
2071
|
+
dataLines = [];
|
|
2072
|
+
eventName = "";
|
|
2073
|
+
this.dispatchPayload(channel, payload, channel === DEFAULT_CHANNEL);
|
|
2074
|
+
};
|
|
2075
|
+
const processLine = (rawLine) => {
|
|
2076
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
2077
|
+
if (line === "") {
|
|
2078
|
+
dispatchEvent();
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
if (line.startsWith(":")) return;
|
|
2082
|
+
const bareJson = tryParseJson(line);
|
|
2083
|
+
if (isEnvelope(bareJson)) {
|
|
2084
|
+
this.dispatchEnvelope(bareJson);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
const colon = line.indexOf(":");
|
|
2088
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
2089
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
2090
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
2091
|
+
if (field === "event") eventName = value;
|
|
2092
|
+
else if (field === "data") {
|
|
2093
|
+
const envelope = tryParseJson(value);
|
|
2094
|
+
if (!eventName && isEnvelope(envelope)) this.dispatchEnvelope(envelope);
|
|
2095
|
+
else dataLines.push(value);
|
|
2096
|
+
} else if (field === "id") {
|
|
2097
|
+
if (!value.includes("\0")) this.lastEventId = value;
|
|
2098
|
+
} else if (field === "retry") {
|
|
2099
|
+
if (/^\d+$/.test(value)) this.retry = Number(value);
|
|
2100
|
+
} else if (colon === -1) dataLines.push(line);
|
|
2101
|
+
};
|
|
509
2102
|
const consume = (chunk) => {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
buffer
|
|
515
|
-
|
|
2103
|
+
const text = typeof chunk === "string" ? decoder.decode() + chunk : decoder.decode(chunk, { stream: true });
|
|
2104
|
+
buffer += text;
|
|
2105
|
+
let newline;
|
|
2106
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
2107
|
+
processLine(buffer.slice(0, newline));
|
|
2108
|
+
buffer = buffer.slice(newline + 1);
|
|
516
2109
|
}
|
|
517
2110
|
};
|
|
518
|
-
if (
|
|
2111
|
+
if (isReadableStream$1(source)) {
|
|
519
2112
|
const reader = source.getReader();
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
2113
|
+
try {
|
|
2114
|
+
for (;;) {
|
|
2115
|
+
const { done, value } = await reader.read();
|
|
2116
|
+
if (done) break;
|
|
2117
|
+
if (value != null) consume(value);
|
|
2118
|
+
}
|
|
2119
|
+
} finally {
|
|
2120
|
+
reader.releaseLock();
|
|
526
2121
|
}
|
|
527
|
-
const tail = decoder.decode();
|
|
528
|
-
if (tail) consume(tail);
|
|
529
2122
|
} else for await (const chunk of source) consume(chunk);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if ("delta" in parsed) this.routeDelta(parsed.ch, String(parsed.delta));
|
|
546
|
-
else this.routeData(parsed.ch, parsed.data);
|
|
547
|
-
return currentEvent;
|
|
548
|
-
}
|
|
549
|
-
if (parsed !== void 0) {
|
|
550
|
-
this.routeData(currentEvent ?? DEFAULT_CHANNEL, parsed);
|
|
551
|
-
return currentEvent;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
if (currentEvent !== void 0) {
|
|
555
|
-
const parsed = tryParseJson(payload);
|
|
556
|
-
this.routeData(currentEvent, parsed !== void 0 ? parsed : payload);
|
|
557
|
-
return currentEvent;
|
|
558
|
-
}
|
|
559
|
-
this.routeDelta(DEFAULT_CHANNEL, payload);
|
|
560
|
-
return currentEvent;
|
|
2123
|
+
const tail = decoder.decode();
|
|
2124
|
+
if (tail) consume(tail);
|
|
2125
|
+
if (buffer.length > 0) processLine(buffer);
|
|
2126
|
+
dispatchEvent();
|
|
2127
|
+
}
|
|
2128
|
+
dispatchPayload(channel, payload, defaultIsDelta) {
|
|
2129
|
+
const parsed = tryParseJson(payload);
|
|
2130
|
+
if (isEnvelope(parsed)) this.dispatchEnvelope(parsed);
|
|
2131
|
+
else if (defaultIsDelta) this.routeDelta(channel, payload);
|
|
2132
|
+
else this.routeData(channel, parsed !== void 0 ? parsed : payload);
|
|
2133
|
+
}
|
|
2134
|
+
dispatchEnvelope(envelope) {
|
|
2135
|
+
const channel = envelope.ch;
|
|
2136
|
+
if ("delta" in envelope) this.routeDelta(channel, String(envelope.delta));
|
|
2137
|
+
else this.routeData(channel, envelope.data);
|
|
561
2138
|
}
|
|
562
|
-
/** Route a text delta: to a bound sink and/or an on() handler (either/both). */
|
|
563
2139
|
routeDelta(channel, text) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (sink) sink.push(text);
|
|
567
|
-
if (handler) handler(text);
|
|
2140
|
+
this.sinks.get(channel)?.push(text);
|
|
2141
|
+
this.handlers.get(channel)?.(text);
|
|
568
2142
|
}
|
|
569
|
-
/** Route a structured value: to the handler, or a string value to a lone sink. */
|
|
570
2143
|
routeData(channel, value) {
|
|
571
2144
|
const handler = this.handlers.get(channel);
|
|
572
|
-
if (handler)
|
|
573
|
-
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
const sink = this.sinks.get(channel);
|
|
577
|
-
if (sink && typeof value === "string") sink.push(value);
|
|
2145
|
+
if (handler) handler(value);
|
|
2146
|
+
else if (typeof value === "string") this.sinks.get(channel)?.push(value);
|
|
578
2147
|
}
|
|
579
2148
|
};
|
|
580
2149
|
function tryParseJson(text) {
|
|
@@ -584,8 +2153,11 @@ function tryParseJson(text) {
|
|
|
584
2153
|
return void 0;
|
|
585
2154
|
}
|
|
586
2155
|
}
|
|
587
|
-
function
|
|
588
|
-
return typeof value === "object" && value !== null;
|
|
2156
|
+
function isEnvelope(value) {
|
|
2157
|
+
return typeof value === "object" && value !== null && typeof value.ch === "string";
|
|
2158
|
+
}
|
|
2159
|
+
function isReadableStream$1(source) {
|
|
2160
|
+
return "getReader" in source && typeof source.getReader === "function";
|
|
589
2161
|
}
|
|
590
2162
|
|
|
591
2163
|
//#endregion
|
|
@@ -599,13 +2171,294 @@ function buildSystemPrompt(options = {}) {
|
|
|
599
2171
|
const parts = [];
|
|
600
2172
|
if (options.base) parts.push(options.base);
|
|
601
2173
|
const cardSpec = options.registry?.toPromptSpec();
|
|
602
|
-
if (
|
|
2174
|
+
if (cardSpec) parts.push(cardSpec);
|
|
603
2175
|
for (const p of options.plugins ?? []) if (p.promptSpec) parts.push(p.promptSpec);
|
|
604
2176
|
return parts.join("\n\n");
|
|
605
2177
|
}
|
|
606
|
-
|
|
607
|
-
|
|
2178
|
+
|
|
2179
|
+
//#endregion
|
|
2180
|
+
//#region src/model-stream.ts
|
|
2181
|
+
async function* parseSSE(source, options = {}) {
|
|
2182
|
+
let buffer = "";
|
|
2183
|
+
let eventName;
|
|
2184
|
+
let id;
|
|
2185
|
+
let retry;
|
|
2186
|
+
let data = [];
|
|
2187
|
+
const doneData = options.doneData === void 0 ? "[DONE]" : options.doneData;
|
|
2188
|
+
const dispatch = () => {
|
|
2189
|
+
if (data.length === 0) {
|
|
2190
|
+
eventName = void 0;
|
|
2191
|
+
retry = void 0;
|
|
2192
|
+
return void 0;
|
|
2193
|
+
}
|
|
2194
|
+
const raw = data.join("\n");
|
|
2195
|
+
data = [];
|
|
2196
|
+
const result = { data: raw };
|
|
2197
|
+
if (eventName !== void 0) result.event = eventName;
|
|
2198
|
+
if (id !== void 0) result.id = id;
|
|
2199
|
+
if (retry !== void 0) result.retry = retry;
|
|
2200
|
+
eventName = void 0;
|
|
2201
|
+
retry = void 0;
|
|
2202
|
+
if (doneData !== false && raw === doneData) return "done";
|
|
2203
|
+
if (options.parseJSON) try {
|
|
2204
|
+
result.data = JSON.parse(raw);
|
|
2205
|
+
} catch (cause) {
|
|
2206
|
+
const error = malformedError("Invalid SSE JSON", raw, cause);
|
|
2207
|
+
if (options.onMalformed?.(error, raw) === "skip") return void 0;
|
|
2208
|
+
throw error;
|
|
2209
|
+
}
|
|
2210
|
+
return result;
|
|
2211
|
+
};
|
|
2212
|
+
const processLine = (line) => {
|
|
2213
|
+
if (line === "") return dispatch();
|
|
2214
|
+
if (line.startsWith(":")) return void 0;
|
|
2215
|
+
const colon = line.indexOf(":");
|
|
2216
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
2217
|
+
let value = colon < 0 ? "" : line.slice(colon + 1);
|
|
2218
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
2219
|
+
if (field === "data") data.push(value);
|
|
2220
|
+
else if (field === "event") eventName = value;
|
|
2221
|
+
else if (field === "id" && !value.includes("\0")) id = value;
|
|
2222
|
+
else if (field === "retry" && /^\d+$/.test(value)) retry = Number(value);
|
|
2223
|
+
return void 0;
|
|
2224
|
+
};
|
|
2225
|
+
const drainLines = function* (eof = false) {
|
|
2226
|
+
for (;;) {
|
|
2227
|
+
const cr = buffer.indexOf("\r");
|
|
2228
|
+
const lf = buffer.indexOf("\n");
|
|
2229
|
+
let newline = cr < 0 ? lf : lf < 0 ? cr : Math.min(cr, lf);
|
|
2230
|
+
if (newline < 0) break;
|
|
2231
|
+
if (!eof && buffer[newline] === "\r" && newline === buffer.length - 1) break;
|
|
2232
|
+
const width = buffer[newline] === "\r" && buffer[newline + 1] === "\n" ? 2 : 1;
|
|
2233
|
+
const line = buffer.slice(0, newline);
|
|
2234
|
+
buffer = buffer.slice(newline + width);
|
|
2235
|
+
const event$1 = processLine(line);
|
|
2236
|
+
if (event$1) yield event$1;
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
for await (const chunk of decodedChunks(source, options.signal)) {
|
|
2240
|
+
buffer += chunk;
|
|
2241
|
+
for (const event$1 of drainLines()) {
|
|
2242
|
+
if (event$1 === "done") return;
|
|
2243
|
+
yield event$1;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
for (const event$1 of drainLines(true)) {
|
|
2247
|
+
if (event$1 === "done") return;
|
|
2248
|
+
yield event$1;
|
|
2249
|
+
}
|
|
2250
|
+
if (buffer) processLine(buffer);
|
|
2251
|
+
const event = dispatch();
|
|
2252
|
+
if (event && event !== "done") yield event;
|
|
2253
|
+
}
|
|
2254
|
+
async function* jsonLines(source, options = {}) {
|
|
2255
|
+
let buffer = "";
|
|
2256
|
+
for await (const chunk of decodedChunks(source, options.signal)) {
|
|
2257
|
+
buffer += chunk;
|
|
2258
|
+
for (;;) {
|
|
2259
|
+
const newline = buffer.indexOf("\n");
|
|
2260
|
+
if (newline < 0) break;
|
|
2261
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
2262
|
+
buffer = buffer.slice(newline + 1);
|
|
2263
|
+
const value$1 = parseJSONLine(line, options);
|
|
2264
|
+
if (value$1 !== SKIP) yield value$1;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
const value = parseJSONLine(buffer.replace(/\r$/, ""), options);
|
|
2268
|
+
if (value !== SKIP) yield value;
|
|
2269
|
+
}
|
|
2270
|
+
const ndjson = jsonLines;
|
|
2271
|
+
async function* textLines(source, options = {}) {
|
|
2272
|
+
let buffer = "";
|
|
2273
|
+
for await (const chunk of decodedChunks(source, options.signal)) {
|
|
2274
|
+
buffer += chunk;
|
|
2275
|
+
for (;;) {
|
|
2276
|
+
const newline = buffer.indexOf("\n");
|
|
2277
|
+
if (newline < 0) break;
|
|
2278
|
+
yield buffer.slice(0, newline).replace(/\r$/, "");
|
|
2279
|
+
buffer = buffer.slice(newline + 1);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
if (buffer) yield buffer.replace(/\r$/, "");
|
|
2283
|
+
}
|
|
2284
|
+
async function* contentDeltas(events) {
|
|
2285
|
+
for await (const event of events) if (event.type === "content") yield event.delta;
|
|
2286
|
+
}
|
|
2287
|
+
async function* mockModelStream(events, options = {}) {
|
|
2288
|
+
for await (const event of events) {
|
|
2289
|
+
throwIfAborted(options.signal);
|
|
2290
|
+
if (options.delayMs && options.delayMs > 0) await delay(options.delayMs, options.signal);
|
|
2291
|
+
yield event;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function readableBytes(chunks) {
|
|
2295
|
+
const iterator = toAsyncIterator(chunks);
|
|
2296
|
+
const encoder = new TextEncoder();
|
|
2297
|
+
return new ReadableStream({
|
|
2298
|
+
async pull(controller) {
|
|
2299
|
+
try {
|
|
2300
|
+
const result = await iterator.next();
|
|
2301
|
+
if (result.done) controller.close();
|
|
2302
|
+
else controller.enqueue(typeof result.value === "string" ? encoder.encode(result.value) : result.value);
|
|
2303
|
+
} catch (error) {
|
|
2304
|
+
controller.error(error);
|
|
2305
|
+
}
|
|
2306
|
+
},
|
|
2307
|
+
async cancel(reason) {
|
|
2308
|
+
await iterator.return?.(reason);
|
|
2309
|
+
}
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
async function* decodedChunks(source, signal) {
|
|
2313
|
+
throwIfAborted(signal);
|
|
2314
|
+
const decoder = new TextDecoder();
|
|
2315
|
+
const body = isResponse(source) ? source.body : source;
|
|
2316
|
+
if (!body) throw new TypeError("Response body is null");
|
|
2317
|
+
if (isReadableStream(body)) {
|
|
2318
|
+
const reader = body.getReader();
|
|
2319
|
+
let done$1 = false;
|
|
2320
|
+
let primaryError$1;
|
|
2321
|
+
let cancelPromise;
|
|
2322
|
+
const cancel = (reason) => {
|
|
2323
|
+
if (done$1) return Promise.resolve();
|
|
2324
|
+
cancelPromise ??= Promise.resolve().then(() => reader.cancel(reason));
|
|
2325
|
+
return cancelPromise;
|
|
2326
|
+
};
|
|
2327
|
+
const abort = () => {
|
|
2328
|
+
cancel(abortReason(signal)).catch(() => {});
|
|
2329
|
+
};
|
|
2330
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2331
|
+
try {
|
|
2332
|
+
for (;;) {
|
|
2333
|
+
throwIfAborted(signal);
|
|
2334
|
+
const result = await reader.read();
|
|
2335
|
+
throwIfAborted(signal);
|
|
2336
|
+
if (result.done) {
|
|
2337
|
+
done$1 = true;
|
|
2338
|
+
break;
|
|
2339
|
+
}
|
|
2340
|
+
const text = decoder.decode(result.value, { stream: true });
|
|
2341
|
+
if (text) yield text;
|
|
2342
|
+
}
|
|
2343
|
+
throwIfAborted(signal);
|
|
2344
|
+
const tail = decoder.decode();
|
|
2345
|
+
if (tail) yield tail;
|
|
2346
|
+
} catch (error) {
|
|
2347
|
+
primaryError$1 = error;
|
|
2348
|
+
throw error;
|
|
2349
|
+
} finally {
|
|
2350
|
+
signal?.removeEventListener("abort", abort);
|
|
2351
|
+
try {
|
|
2352
|
+
if (!done$1) await cancel(abortReason(signal, "Stream iteration stopped"));
|
|
2353
|
+
} catch (error) {
|
|
2354
|
+
if (primaryError$1 === void 0) throw error;
|
|
2355
|
+
} finally {
|
|
2356
|
+
reader.releaseLock();
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
const iterator = body[Symbol.asyncIterator]();
|
|
2362
|
+
let done = false;
|
|
2363
|
+
let primaryError;
|
|
2364
|
+
try {
|
|
2365
|
+
for (;;) {
|
|
2366
|
+
throwIfAborted(signal);
|
|
2367
|
+
const result = await nextWithAbort(iterator, signal);
|
|
2368
|
+
throwIfAborted(signal);
|
|
2369
|
+
if (result.done) {
|
|
2370
|
+
done = true;
|
|
2371
|
+
break;
|
|
2372
|
+
}
|
|
2373
|
+
const text = typeof result.value === "string" ? decoder.decode() + result.value : decoder.decode(result.value, { stream: true });
|
|
2374
|
+
if (text) yield text;
|
|
2375
|
+
}
|
|
2376
|
+
const tail = decoder.decode();
|
|
2377
|
+
if (tail) yield tail;
|
|
2378
|
+
} catch (error) {
|
|
2379
|
+
primaryError = error;
|
|
2380
|
+
throw error;
|
|
2381
|
+
} finally {
|
|
2382
|
+
if (!done) try {
|
|
2383
|
+
await iterator.return?.();
|
|
2384
|
+
} catch (error) {
|
|
2385
|
+
if (primaryError === void 0) throw error;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
async function nextWithAbort(iterator, signal) {
|
|
2390
|
+
if (!signal) return iterator.next();
|
|
2391
|
+
throwIfAborted(signal);
|
|
2392
|
+
const next = Promise.resolve(iterator.next());
|
|
2393
|
+
let abort;
|
|
2394
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
2395
|
+
abort = () => reject(abortReason(signal));
|
|
2396
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
2397
|
+
});
|
|
2398
|
+
try {
|
|
2399
|
+
return await Promise.race([next, aborted]);
|
|
2400
|
+
} finally {
|
|
2401
|
+
signal.removeEventListener("abort", abort);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
const SKIP = Symbol("skip");
|
|
2405
|
+
function parseJSONLine(line, options) {
|
|
2406
|
+
if (!line.trim()) return SKIP;
|
|
2407
|
+
try {
|
|
2408
|
+
return JSON.parse(line);
|
|
2409
|
+
} catch (cause) {
|
|
2410
|
+
const error = malformedError("Invalid JSON line", line, cause);
|
|
2411
|
+
if (options.onMalformed?.(error, line) === "skip") return SKIP;
|
|
2412
|
+
throw error;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function malformedError(message, input, cause) {
|
|
2416
|
+
const error = new SyntaxError(`${message}: ${input}`);
|
|
2417
|
+
error.cause = cause;
|
|
2418
|
+
return error;
|
|
2419
|
+
}
|
|
2420
|
+
function isResponse(value) {
|
|
2421
|
+
return typeof Response !== "undefined" && value instanceof Response;
|
|
2422
|
+
}
|
|
2423
|
+
function isReadableStream(value) {
|
|
2424
|
+
return typeof value === "object" && value !== null && "getReader" in value && typeof value.getReader === "function";
|
|
2425
|
+
}
|
|
2426
|
+
function toAsyncIterator(source) {
|
|
2427
|
+
if (Symbol.asyncIterator in source) return source[Symbol.asyncIterator]();
|
|
2428
|
+
const iterator = source[Symbol.iterator]();
|
|
2429
|
+
return {
|
|
2430
|
+
next: async () => iterator.next(),
|
|
2431
|
+
return: iterator.return ? async (value) => iterator.return(value) : void 0
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
function throwIfAborted(signal) {
|
|
2435
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
2436
|
+
}
|
|
2437
|
+
function abortReason(signal, fallback = "The operation was aborted") {
|
|
2438
|
+
if (signal?.reason !== void 0) return signal.reason;
|
|
2439
|
+
const error = new Error(fallback);
|
|
2440
|
+
error.name = "AbortError";
|
|
2441
|
+
return error;
|
|
2442
|
+
}
|
|
2443
|
+
function delay(ms, signal) {
|
|
2444
|
+
return new Promise((resolve, reject) => {
|
|
2445
|
+
if (signal?.aborted) {
|
|
2446
|
+
reject(abortReason(signal));
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
const finish = () => {
|
|
2450
|
+
signal?.removeEventListener("abort", abort);
|
|
2451
|
+
resolve();
|
|
2452
|
+
};
|
|
2453
|
+
const timer = setTimeout(finish, ms);
|
|
2454
|
+
const abort = () => {
|
|
2455
|
+
clearTimeout(timer);
|
|
2456
|
+
signal?.removeEventListener("abort", abort);
|
|
2457
|
+
reject(abortReason(signal));
|
|
2458
|
+
};
|
|
2459
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2460
|
+
});
|
|
608
2461
|
}
|
|
609
2462
|
|
|
610
2463
|
//#endregion
|
|
611
|
-
export { CardRegistry, Renderer, StreamRouter, buildSystemPrompt, collectNodeRenderers, createParser, diffAst, parsePartialJSON, pluginNodeTypes, repairMarkdown, sanitizeHtml };
|
|
2464
|
+
export { ActionAbortedError, ActionAlreadyRegisteredError, ActionDestroyedError, ActionExecutionError, ActionNotFoundError, ActionRegistry, ActionRuntime, ActionRuntimeError, ActionTimeoutError, ActionValidationError, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardJSONError, CardLimitError, CardNotFoundError, CardRegistry, CardRevisionConflictError, CardSnapshotError, CardStore, CardStoreError, CardTypeConflictError, CardValidationError, DebugEmitter, Renderer, StreamRouter, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };
|