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