@ai-gui/core 0.2.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 +1616 -40
- package/dist/index.d.cts +411 -6
- package/dist/index.d.ts +411 -6
- package/dist/index.js +1579 -40
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -239,6 +239,261 @@ function sanitizeHtml(html, options = {}) {
|
|
|
239
239
|
return dompurify.default.sanitize(html);
|
|
240
240
|
}
|
|
241
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
|
+
|
|
242
497
|
//#endregion
|
|
243
498
|
//#region src/card-registry.ts
|
|
244
499
|
var CardRegistry = class {
|
|
@@ -260,17 +515,21 @@ var CardRegistry = class {
|
|
|
260
515
|
complete,
|
|
261
516
|
valid: false
|
|
262
517
|
};
|
|
263
|
-
const valid = complete && this.
|
|
518
|
+
const valid = complete && this.validateDefinition(def, data);
|
|
264
519
|
return {
|
|
265
520
|
data,
|
|
266
521
|
complete,
|
|
267
522
|
valid
|
|
268
523
|
};
|
|
269
524
|
}
|
|
270
|
-
validate(
|
|
525
|
+
validate(type, data) {
|
|
526
|
+
const def = this.cards.get(type);
|
|
527
|
+
return def ? this.validateDefinition(def, data) : false;
|
|
528
|
+
}
|
|
529
|
+
validateDefinition(def, data) {
|
|
271
530
|
try {
|
|
272
531
|
if (def.validate) return def.validate(data);
|
|
273
|
-
if (def.schema) return
|
|
532
|
+
if (def.schema) return validateJSONSchema(def.schema, data).valid;
|
|
274
533
|
return true;
|
|
275
534
|
} catch {
|
|
276
535
|
return false;
|
|
@@ -301,42 +560,904 @@ var CardRegistry = class {
|
|
|
301
560
|
};
|
|
302
561
|
}
|
|
303
562
|
};
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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);
|
|
311
670
|
return true;
|
|
312
671
|
}
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
|
|
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
|
+
});
|
|
316
685
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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`);
|
|
1277
|
+
}
|
|
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 {}
|
|
321
1383
|
}
|
|
322
1384
|
|
|
323
1385
|
//#endregion
|
|
324
1386
|
//#region src/plugins.ts
|
|
325
1387
|
/** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
|
|
326
|
-
function collectNodeRenderers(plugins = []) {
|
|
1388
|
+
function collectNodeRenderers(plugins = [], debugOptions = {}) {
|
|
327
1389
|
const map = {};
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
+
}
|
|
332
1453
|
return map;
|
|
333
1454
|
}
|
|
334
1455
|
/** The set of node types claimed by the given plugins. */
|
|
335
1456
|
function pluginNodeTypes(plugins = []) {
|
|
336
1457
|
return new Set(Object.keys(collectNodeRenderers(plugins)));
|
|
337
1458
|
}
|
|
338
|
-
function
|
|
339
|
-
return value
|
|
1459
|
+
function isPromise(value) {
|
|
1460
|
+
return typeof value?.then === "function";
|
|
340
1461
|
}
|
|
341
1462
|
|
|
342
1463
|
//#endregion
|
|
@@ -390,13 +1511,15 @@ function createParserWithMetadata(options = {}) {
|
|
|
390
1511
|
const cardType = info.slice(5);
|
|
391
1512
|
const res = options.registry.parse(cardType, t.content);
|
|
392
1513
|
const complete = res.complete && isClosedFence(rawSrc, t.map, t.markup);
|
|
1514
|
+
const idResult = complete && res.valid ? getCardId(res.data) : void 0;
|
|
393
1515
|
addNode({
|
|
394
1516
|
type: "card",
|
|
395
1517
|
card: {
|
|
396
1518
|
type: cardType,
|
|
397
1519
|
data: res.data,
|
|
398
1520
|
complete,
|
|
399
|
-
valid: complete && res.valid
|
|
1521
|
+
valid: complete && res.valid && idResult !== false,
|
|
1522
|
+
...typeof idResult === "string" ? { id: idResult } : {}
|
|
400
1523
|
}
|
|
401
1524
|
}, t.map);
|
|
402
1525
|
} else if (pluginTypes.has(info)) {
|
|
@@ -502,6 +1625,11 @@ function createParserWithMetadata(options = {}) {
|
|
|
502
1625
|
};
|
|
503
1626
|
};
|
|
504
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
|
+
}
|
|
505
1633
|
function getLineOffsets(src) {
|
|
506
1634
|
const offsets = [0];
|
|
507
1635
|
for (let i = 0; i < src.length; i++) if (src[i] === "\r") {
|
|
@@ -612,6 +1740,7 @@ function nodeEqual(a, b) {
|
|
|
612
1740
|
* the resulting patches via `onPatch`.
|
|
613
1741
|
*/
|
|
614
1742
|
var Renderer = class {
|
|
1743
|
+
debugSource = "renderer";
|
|
615
1744
|
buffer = "";
|
|
616
1745
|
prevAst = [];
|
|
617
1746
|
parse;
|
|
@@ -622,8 +1751,10 @@ var Renderer = class {
|
|
|
622
1751
|
activeFeed;
|
|
623
1752
|
renderScheduled = false;
|
|
624
1753
|
scheduleGeneration = 0;
|
|
1754
|
+
debug;
|
|
625
1755
|
constructor(options = {}) {
|
|
626
1756
|
this.options = options;
|
|
1757
|
+
this.debug = new DebugEmitter(this.debugSource, options);
|
|
627
1758
|
this.sanitize = options.sanitize === false ? false : typeof options.sanitize === "object" ? options.sanitize : {};
|
|
628
1759
|
if (options.registry) for (const plugin of options.plugins ?? []) for (const card of plugin.cards ?? []) options.registry.register(card);
|
|
629
1760
|
this.parse = createParserWithMetadata({
|
|
@@ -631,21 +1762,36 @@ var Renderer = class {
|
|
|
631
1762
|
plugins: options.plugins
|
|
632
1763
|
});
|
|
633
1764
|
}
|
|
1765
|
+
get debugEnabled() {
|
|
1766
|
+
return this.debug.available;
|
|
1767
|
+
}
|
|
1768
|
+
emitDebug(type, data = {}) {
|
|
1769
|
+
this.debug.emit(type, data);
|
|
1770
|
+
}
|
|
634
1771
|
push(chunk) {
|
|
635
1772
|
this.buffer += chunk;
|
|
1773
|
+
if (this.debug.active) this.debug.emit("chunk-received", {
|
|
1774
|
+
chunk,
|
|
1775
|
+
length: chunk.length,
|
|
1776
|
+
bufferLength: this.buffer.length
|
|
1777
|
+
});
|
|
636
1778
|
this.scheduleRender();
|
|
637
1779
|
}
|
|
1780
|
+
subscribeDebug(listener) {
|
|
1781
|
+
return this.debug.subscribe(listener);
|
|
1782
|
+
}
|
|
638
1783
|
async feed(source, options = {}) {
|
|
639
1784
|
const generation = ++this.generation;
|
|
640
1785
|
this.cancelActiveFeed(createAbortError("Superseded by a newer feed"));
|
|
1786
|
+
if (this.debug.active) this.debug.emit("feed-started", { generation });
|
|
641
1787
|
const decoder = new TextDecoder();
|
|
642
1788
|
const signal = options.signal;
|
|
643
|
-
if (signal?.aborted) throw abortReason(signal);
|
|
1789
|
+
if (signal?.aborted) throw abortReason$1(signal);
|
|
644
1790
|
let aborted = false;
|
|
645
1791
|
let abortError;
|
|
646
1792
|
const abort = () => {
|
|
647
1793
|
aborted = true;
|
|
648
|
-
abortError = abortReason(signal);
|
|
1794
|
+
abortError = abortReason$1(signal);
|
|
649
1795
|
if (this.activeFeed?.generation === generation) this.cancelActiveFeed(abortError);
|
|
650
1796
|
};
|
|
651
1797
|
signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -655,7 +1801,7 @@ var Renderer = class {
|
|
|
655
1801
|
if (text) this.push(text);
|
|
656
1802
|
};
|
|
657
1803
|
try {
|
|
658
|
-
if (isReadableStream$
|
|
1804
|
+
if (isReadableStream$2(source)) {
|
|
659
1805
|
const reader = source.getReader();
|
|
660
1806
|
let cancelled = false;
|
|
661
1807
|
this.activeFeed = {
|
|
@@ -704,8 +1850,22 @@ var Renderer = class {
|
|
|
704
1850
|
if (tail) this.push(tail);
|
|
705
1851
|
this.flush();
|
|
706
1852
|
}
|
|
707
|
-
if (aborted)
|
|
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
|
+
});
|
|
708
1864
|
} finally {
|
|
1865
|
+
if (!aborted && generation !== this.generation && this.debug.active) this.debug.emit("feed-cancelled", {
|
|
1866
|
+
generation,
|
|
1867
|
+
reason: "superseded-or-reset"
|
|
1868
|
+
});
|
|
709
1869
|
signal?.removeEventListener("abort", abort);
|
|
710
1870
|
if (this.activeFeed?.generation === generation) this.activeFeed = void 0;
|
|
711
1871
|
}
|
|
@@ -721,6 +1881,7 @@ var Renderer = class {
|
|
|
721
1881
|
this.prevAst = [];
|
|
722
1882
|
this.parsed = void 0;
|
|
723
1883
|
this.options.onPatch?.(patches, []);
|
|
1884
|
+
if (this.debug.active) this.debug.emit("renderer-reset", { patches });
|
|
724
1885
|
}
|
|
725
1886
|
/** Immediately render pending buffered input, bypassing the scheduler. */
|
|
726
1887
|
flush() {
|
|
@@ -749,16 +1910,34 @@ var Renderer = class {
|
|
|
749
1910
|
Promise.resolve(active.cancel(reason)).catch(() => {});
|
|
750
1911
|
}
|
|
751
1912
|
render() {
|
|
1913
|
+
const debug = this.debug.active;
|
|
1914
|
+
const renderStarted = debug ? now() : 0;
|
|
752
1915
|
const previous = this.parsed;
|
|
753
1916
|
let next;
|
|
1917
|
+
let mode = "full";
|
|
754
1918
|
if (previous?.incrementalSafe && previous.blocks.length > 1) {
|
|
755
1919
|
const mutable = previous.blocks.at(-1);
|
|
756
1920
|
const stableBlocks = previous.blocks.slice(0, -1);
|
|
757
1921
|
const stableNodeEnd = mutable.nodeStart;
|
|
758
1922
|
const rawTail = this.buffer.slice(mutable.start);
|
|
759
|
-
const
|
|
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
|
+
});
|
|
760
1938
|
if (tail.incrementalSafe) {
|
|
761
|
-
|
|
1939
|
+
mode = "mutable-tail";
|
|
1940
|
+
if (this.sanitize !== false) this.sanitizeNodesWithDebug(tail.nodes);
|
|
762
1941
|
next = {
|
|
763
1942
|
nodes: [...previous.nodes.slice(0, stableNodeEnd), ...tail.nodes],
|
|
764
1943
|
blocks: [...stableBlocks, ...tail.blocks.map((block) => ({
|
|
@@ -768,19 +1947,83 @@ var Renderer = class {
|
|
|
768
1947
|
}))],
|
|
769
1948
|
incrementalSafe: true
|
|
770
1949
|
};
|
|
1950
|
+
if (debug) this.debug.emit("stable-prefix-committed", {
|
|
1951
|
+
blocks: stableBlocks.length,
|
|
1952
|
+
nodes: stableNodeEnd
|
|
1953
|
+
});
|
|
771
1954
|
} else {
|
|
772
|
-
|
|
773
|
-
if (this.
|
|
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);
|
|
774
1970
|
}
|
|
775
1971
|
} else {
|
|
776
|
-
|
|
777
|
-
if (this.
|
|
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);
|
|
778
1987
|
}
|
|
779
1988
|
const nextAst = next.nodes;
|
|
1989
|
+
const diffStarted = debug ? now() : 0;
|
|
780
1990
|
const patches = diffAst(this.prevAst, nextAst);
|
|
1991
|
+
const diffDurationMs = debug ? now() - diffStarted : 0;
|
|
781
1992
|
this.parsed = next;
|
|
782
1993
|
this.prevAst = nextAst;
|
|
1994
|
+
const patchDispatchStarted = debug ? now() : 0;
|
|
783
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
|
+
});
|
|
784
2027
|
}
|
|
785
2028
|
};
|
|
786
2029
|
/**
|
|
@@ -794,10 +2037,10 @@ function sanitizeNodes(nodes, options) {
|
|
|
794
2037
|
if (node.children) sanitizeNodes(node.children, options);
|
|
795
2038
|
}
|
|
796
2039
|
}
|
|
797
|
-
function isReadableStream$
|
|
2040
|
+
function isReadableStream$2(source) {
|
|
798
2041
|
return "getReader" in source && typeof source.getReader === "function";
|
|
799
2042
|
}
|
|
800
|
-
function abortReason(signal) {
|
|
2043
|
+
function abortReason$1(signal) {
|
|
801
2044
|
return signal?.reason ?? createAbortError("The operation was aborted");
|
|
802
2045
|
}
|
|
803
2046
|
function createAbortError(message) {
|
|
@@ -805,6 +2048,18 @@ function createAbortError(message) {
|
|
|
805
2048
|
error.name = "AbortError";
|
|
806
2049
|
return error;
|
|
807
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
|
+
}
|
|
808
2063
|
|
|
809
2064
|
//#endregion
|
|
810
2065
|
//#region src/stream-router.ts
|
|
@@ -877,7 +2132,7 @@ var StreamRouter = class {
|
|
|
877
2132
|
buffer = buffer.slice(newline + 1);
|
|
878
2133
|
}
|
|
879
2134
|
};
|
|
880
|
-
if (isReadableStream(source)) {
|
|
2135
|
+
if (isReadableStream$1(source)) {
|
|
881
2136
|
const reader = source.getReader();
|
|
882
2137
|
try {
|
|
883
2138
|
for (;;) {
|
|
@@ -925,7 +2180,7 @@ function tryParseJson(text) {
|
|
|
925
2180
|
function isEnvelope(value) {
|
|
926
2181
|
return typeof value === "object" && value !== null && typeof value.ch === "string";
|
|
927
2182
|
}
|
|
928
|
-
function isReadableStream(source) {
|
|
2183
|
+
function isReadableStream$1(source) {
|
|
929
2184
|
return "getReader" in source && typeof source.getReader === "function";
|
|
930
2185
|
}
|
|
931
2186
|
|
|
@@ -946,16 +2201,337 @@ function buildSystemPrompt(options = {}) {
|
|
|
946
2201
|
}
|
|
947
2202
|
|
|
948
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
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
|
|
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
|
|
949
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
|
|
950
2513
|
exports.Renderer = Renderer
|
|
951
2514
|
exports.StreamRouter = StreamRouter
|
|
952
2515
|
exports.applyPatches = applyPatches
|
|
953
2516
|
exports.buildSystemPrompt = buildSystemPrompt
|
|
954
2517
|
exports.collectNodeRenderers = collectNodeRenderers
|
|
2518
|
+
exports.contentDeltas = contentDeltas
|
|
2519
|
+
exports.createActionRuntime = createActionRuntime
|
|
955
2520
|
exports.createParser = createParser
|
|
956
2521
|
exports.createParserWithMetadata = createParserWithMetadata
|
|
957
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
|
|
958
2529
|
exports.parsePartialJSON = parsePartialJSON
|
|
2530
|
+
exports.parseSSE = parseSSE
|
|
959
2531
|
exports.pluginNodeTypes = pluginNodeTypes
|
|
2532
|
+
exports.readableBytes = readableBytes
|
|
960
2533
|
exports.repairMarkdown = repairMarkdown
|
|
961
|
-
exports.
|
|
2534
|
+
exports.safeDebugValue = safeDebugValue
|
|
2535
|
+
exports.sanitizeHtml = sanitizeHtml
|
|
2536
|
+
exports.textLines = textLines
|
|
2537
|
+
exports.validateJSONSchema = validateJSONSchema
|