@ai-gui/core 0.2.0 → 0.4.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/dist/index.js CHANGED
@@ -215,6 +215,261 @@ function sanitizeHtml(html, options = {}) {
215
215
  return DOMPurify.sanitize(html);
216
216
  }
217
217
 
218
+ //#endregion
219
+ //#region src/debug-events.ts
220
+ const DEFAULT_MAX_STRING_LENGTH = 2048;
221
+ const DEFAULT_MAX_DEPTH = 8;
222
+ const DEFAULT_MAX_NODES = 1e3;
223
+ const SENSITIVE_KEY_PARTS = [
224
+ "authorization",
225
+ "auth",
226
+ "accesstoken",
227
+ "refreshtoken",
228
+ "clientsecret",
229
+ "credentials",
230
+ "cookie",
231
+ "setcookie",
232
+ "proxyauthorization",
233
+ "password",
234
+ "passwd",
235
+ "apikey",
236
+ "secret",
237
+ "token"
238
+ ];
239
+ var DebugEmitter = class {
240
+ enabled;
241
+ source;
242
+ limits;
243
+ listeners = new Set();
244
+ sequence = 0;
245
+ constructor(source, options = {}) {
246
+ this.source = source;
247
+ this.enabled = options.debug === true;
248
+ this.limits = {
249
+ maxStringLength: positiveInteger(options.maxStringLength, DEFAULT_MAX_STRING_LENGTH, "maxStringLength"),
250
+ maxDepth: positiveInteger(options.maxDepth, DEFAULT_MAX_DEPTH, "maxDepth"),
251
+ maxNodes: positiveInteger(options.maxNodes, DEFAULT_MAX_NODES, "maxNodes"),
252
+ redact: options.redact
253
+ };
254
+ if (this.enabled && options.onDebugEvent) this.listeners.add(options.onDebugEvent);
255
+ }
256
+ get active() {
257
+ return this.enabled && this.listeners.size > 0;
258
+ }
259
+ get available() {
260
+ return this.enabled;
261
+ }
262
+ subscribe(listener) {
263
+ if (!this.enabled) return () => {};
264
+ this.listeners.add(listener);
265
+ return () => this.listeners.delete(listener);
266
+ }
267
+ emit(type, data = {}) {
268
+ if (!this.active) return;
269
+ const event = Object.freeze({
270
+ type,
271
+ source: this.source,
272
+ timestamp: Date.now(),
273
+ sequence: ++this.sequence,
274
+ data: safeDebugValue(data, this.limits)
275
+ });
276
+ for (const listener of this.listeners) try {
277
+ listener(event);
278
+ } catch {}
279
+ }
280
+ };
281
+ function safeDebugValue(value, options = {}) {
282
+ const maxStringLength = positiveInteger(options.maxStringLength, DEFAULT_MAX_STRING_LENGTH, "maxStringLength");
283
+ const maxDepth = positiveInteger(options.maxDepth, DEFAULT_MAX_DEPTH, "maxDepth");
284
+ const maxNodes = positiveInteger(options.maxNodes, DEFAULT_MAX_NODES, "maxNodes");
285
+ const seen = new WeakSet();
286
+ let nodes = 0;
287
+ const visit = (input, key = "", path = [], depth = 0) => {
288
+ if (++nodes > maxNodes) return "[MAX_NODES]";
289
+ if (isSensitiveKey(key) || options.redact?.({
290
+ key,
291
+ path,
292
+ value: input
293
+ })) return "[REDACTED]";
294
+ if (typeof input === "string") return limitString(redactText(input), maxStringLength);
295
+ if (input === null || typeof input === "boolean") return input;
296
+ if (typeof input === "number") return Number.isFinite(input) ? input : String(input);
297
+ if (typeof input === "bigint" || typeof input === "symbol" || typeof input === "function" || typeof input === "undefined") return String(input);
298
+ if (input instanceof Error) return {
299
+ name: limitString(input.name || "Error", maxStringLength),
300
+ message: limitString(redactText(input.message), maxStringLength)
301
+ };
302
+ if (depth >= maxDepth) return "[MAX_DEPTH]";
303
+ if (seen.has(input)) return "[CIRCULAR]";
304
+ seen.add(input);
305
+ try {
306
+ if (Array.isArray(input)) return input.map((item, index) => visit(item, "", [...path, index], depth + 1));
307
+ const output = {};
308
+ for (const [childKey, child] of Object.entries(input)) output[childKey] = visit(child, childKey, [...path, childKey], depth + 1);
309
+ return output;
310
+ } finally {
311
+ seen.delete(input);
312
+ }
313
+ };
314
+ return visit(value);
315
+ }
316
+ function isSensitiveKey(key) {
317
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
318
+ return normalized !== "" && SENSITIVE_KEY_PARTS.some((part) => normalized.includes(part));
319
+ }
320
+ function redactText(value) {
321
+ return value.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [REDACTED]").replace(/([?&](?:access[_-]?token|refresh[_-]?token|client[_-]?secret|api[_-]?key|password|passwd|token|secret)=)[^&#\s]*/gi, "$1[REDACTED]").replace(/\b((?:access[_-]?token|refresh[_-]?token|client[_-]?secret|api[_-]?key|password|passwd|token|secret)\s*[:=]\s*)[^\s,;]+/gi, "$1[REDACTED]");
322
+ }
323
+ function limitString(value, maxLength) {
324
+ if (value.includes("[REDACTED]") && maxLength < 20) return `[REDACTED][TRUNCATED length=${value.length}]`;
325
+ return value.length > maxLength ? `${value.slice(0, maxLength)}[TRUNCATED length=${value.length}]` : value;
326
+ }
327
+ function positiveInteger(value, fallback, name) {
328
+ if (value === void 0) return fallback;
329
+ if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive integer`);
330
+ return value;
331
+ }
332
+
333
+ //#endregion
334
+ //#region src/json-schema.ts
335
+ /** Small, dependency-free validator for the JSON Schema subset used by AIGUI definitions. */
336
+ function validateJSONSchema(schema, value) {
337
+ const issues = [];
338
+ validate(schema, value, "$", issues);
339
+ return {
340
+ valid: issues.length === 0,
341
+ issues
342
+ };
343
+ }
344
+ function validate(schema, value, path, issues) {
345
+ if (Object.hasOwn(schema, "const") && !jsonEqual(value, schema.const)) {
346
+ issues.push(`${path} must equal ${describeJSONValue(schema.const)}`);
347
+ return;
348
+ }
349
+ if (schema.enum && !schema.enum.some((candidate) => jsonEqual(candidate, value))) {
350
+ issues.push(`${path} must be one of the allowed values`);
351
+ return;
352
+ }
353
+ if (schema.type === "object") {
354
+ if (!isJSONObject(value)) {
355
+ issues.push(`${path} must be an object`);
356
+ return;
357
+ }
358
+ for (const required of schema.required ?? []) if (!Object.hasOwn(value, required)) issues.push(`${path}.${required} is required`);
359
+ for (const [key, childSchema] of Object.entries(schema.properties ?? {})) if (Object.hasOwn(value, key)) validate(childSchema, value[key], `${path}.${key}`, issues);
360
+ if (schema.additionalProperties === false) {
361
+ const allowed = new Set(Object.keys(schema.properties ?? {}));
362
+ for (const key of Object.keys(value)) if (!allowed.has(key)) issues.push(`${path}.${key} is not allowed`);
363
+ } else if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) {
364
+ const allowed = new Set(Object.keys(schema.properties ?? {}));
365
+ for (const key of Object.keys(value)) if (!allowed.has(key)) validate(schema.additionalProperties, value[key], `${path}.${key}`, issues);
366
+ }
367
+ return;
368
+ }
369
+ if (schema.type === "array") {
370
+ if (!Array.isArray(value)) {
371
+ issues.push(`${path} must be an array`);
372
+ return;
373
+ }
374
+ if (schema.minItems !== void 0 && value.length < schema.minItems) issues.push(`${path} must contain at least ${schema.minItems} items`);
375
+ if (schema.maxItems !== void 0 && value.length > schema.maxItems) issues.push(`${path} must contain at most ${schema.maxItems} items`);
376
+ if (schema.items) value.forEach((item, index) => validate(schema.items, item, `${path}[${index}]`, issues));
377
+ return;
378
+ }
379
+ if (schema.type === "string") {
380
+ if (typeof value !== "string") {
381
+ issues.push(`${path} must be a string`);
382
+ return;
383
+ }
384
+ if (schema.minLength !== void 0 && value.length < schema.minLength) issues.push(`${path} must contain at least ${schema.minLength} characters`);
385
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) issues.push(`${path} must contain at most ${schema.maxLength} characters`);
386
+ if (schema.pattern !== void 0) try {
387
+ if (!new RegExp(schema.pattern).test(value)) issues.push(`${path} must match ${schema.pattern}`);
388
+ } catch {
389
+ issues.push(`${path} has an invalid schema pattern`);
390
+ }
391
+ return;
392
+ }
393
+ if (schema.type === "number" || schema.type === "integer") {
394
+ if (typeof value !== "number" || !Number.isFinite(value) || schema.type === "integer" && !Number.isInteger(value)) {
395
+ issues.push(`${path} must be ${schema.type === "integer" ? "an integer" : "a number"}`);
396
+ return;
397
+ }
398
+ if (schema.minimum !== void 0 && value < schema.minimum) issues.push(`${path} must be at least ${schema.minimum}`);
399
+ if (schema.maximum !== void 0 && value > schema.maximum) issues.push(`${path} must be at most ${schema.maximum}`);
400
+ return;
401
+ }
402
+ if (schema.type === "boolean" && typeof value !== "boolean") issues.push(`${path} must be a boolean`);
403
+ if (schema.type === "null" && value !== null) issues.push(`${path} must be null`);
404
+ }
405
+ function isJSONObject(value) {
406
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
407
+ const prototype = Object.getPrototypeOf(value);
408
+ if (prototype !== Object.prototype && prototype !== null) return false;
409
+ if (Object.getOwnPropertySymbols(value).length > 0) return false;
410
+ return Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => descriptor.enumerable && "value" in descriptor);
411
+ }
412
+ function jsonEqual(left, right) {
413
+ return compareJSON(left, right, new WeakSet(), new WeakSet());
414
+ }
415
+ function compareJSON(left, right, leftPath, rightPath) {
416
+ if (left === null || right === null) return left === right;
417
+ if (typeof left !== typeof right) return false;
418
+ if (typeof left === "string" || typeof left === "boolean") return left === right;
419
+ if (typeof left === "number") return Number.isFinite(left) && Number.isFinite(right) && left === right;
420
+ if (typeof left !== "object" || typeof right !== "object") return false;
421
+ if (leftPath.has(left) || rightPath.has(right)) return false;
422
+ const leftArray = Array.isArray(left);
423
+ if (leftArray !== Array.isArray(right)) return false;
424
+ if (leftArray) {
425
+ if (!isJSONArray(left) || !isJSONArray(right)) return false;
426
+ } else if (!isJSONObject(left) || !isJSONObject(right)) return false;
427
+ leftPath.add(left);
428
+ rightPath.add(right);
429
+ try {
430
+ if (leftArray) {
431
+ const rightArray = right;
432
+ if (left.length !== rightArray.length) return false;
433
+ for (let index = 0; index < left.length; index++) if (!compareJSON(left[index], rightArray[index], leftPath, rightPath)) return false;
434
+ return true;
435
+ }
436
+ const leftRecord = left;
437
+ const rightRecord = right;
438
+ const leftKeys = Object.keys(leftRecord);
439
+ const rightKeys = Object.keys(rightRecord);
440
+ if (leftKeys.length !== rightKeys.length) return false;
441
+ for (const key of leftKeys) {
442
+ if (!Object.hasOwn(rightRecord, key)) return false;
443
+ if (!compareJSON(leftRecord[key], rightRecord[key], leftPath, rightPath)) return false;
444
+ }
445
+ return true;
446
+ } finally {
447
+ leftPath.delete(left);
448
+ rightPath.delete(right);
449
+ }
450
+ }
451
+ function describeJSONValue(value) {
452
+ try {
453
+ const serialized = JSON.stringify(value);
454
+ return serialized === void 0 ? "a valid JSON value" : serialized;
455
+ } catch {
456
+ return "a valid JSON value";
457
+ }
458
+ }
459
+ function isJSONArray(value) {
460
+ if (Object.getOwnPropertySymbols(value).length > 0) return false;
461
+ const descriptors = Object.getOwnPropertyDescriptors(value);
462
+ for (const [key, descriptor] of Object.entries(descriptors)) {
463
+ if (key === "length") continue;
464
+ if (!isArrayIndex(key) || !descriptor.enumerable || !("value" in descriptor)) return false;
465
+ }
466
+ return Object.keys(value).length === value.length;
467
+ }
468
+ function isArrayIndex(key) {
469
+ const index = Number(key);
470
+ return Number.isInteger(index) && index >= 0 && index < 2 ** 32 - 1 && String(index) === key;
471
+ }
472
+
218
473
  //#endregion
219
474
  //#region src/card-registry.ts
220
475
  var CardRegistry = class {
@@ -228,6 +483,12 @@ var CardRegistry = class {
228
483
  getRender(type) {
229
484
  return this.cards.get(type)?.render;
230
485
  }
486
+ get(type) {
487
+ return this.cards.get(type);
488
+ }
489
+ list() {
490
+ return [...this.cards.values()];
491
+ }
231
492
  parse(type, rawJson) {
232
493
  const def = this.cards.get(type);
233
494
  const { data, complete } = parsePartialJSON(rawJson);
@@ -236,17 +497,21 @@ var CardRegistry = class {
236
497
  complete,
237
498
  valid: false
238
499
  };
239
- const valid = complete && this.validate(def, data);
500
+ const valid = complete && this.validateDefinition(def, data);
240
501
  return {
241
502
  data,
242
503
  complete,
243
504
  valid
244
505
  };
245
506
  }
246
- validate(def, data) {
507
+ validate(type, data) {
508
+ const def = this.cards.get(type);
509
+ return def ? this.validateDefinition(def, data) : false;
510
+ }
511
+ validateDefinition(def, data) {
247
512
  try {
248
513
  if (def.validate) return def.validate(data);
249
- if (def.schema) return validateSchema(def.schema, data);
514
+ if (def.schema) return validateJSONSchema(def.schema, data).valid;
250
515
  return true;
251
516
  } catch {
252
517
  return false;
@@ -277,42 +542,908 @@ var CardRegistry = class {
277
542
  };
278
543
  }
279
544
  };
280
- /** Minimal JSON Schema validation: covers type / required / properties, enough for cards. */
281
- function validateSchema(schema, data) {
282
- if (schema.type === "object") {
283
- if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
284
- const obj = data;
285
- for (const req of schema.required ?? []) if (!(req in obj)) return false;
286
- for (const [key, sub] of Object.entries(schema.properties ?? {})) if (key in obj && !validateSchema(sub, obj[key])) return false;
545
+
546
+ //#endregion
547
+ //#region src/card-store.ts
548
+ const CARD_ID_MAX_LENGTH = 256;
549
+ const CARD_JSON_MAX_DEPTH = 32;
550
+ const CARD_JSON_MAX_NODES = 1e4;
551
+ const CARD_PATCH_BATCH_MAX_SIZE = 100;
552
+ var CardStore = class {
553
+ debugSource = "card-store";
554
+ registry;
555
+ cards = new Map();
556
+ lastMutationEpoch = new Map();
557
+ mutationEpoch = 0;
558
+ listeners = new Map();
559
+ allListeners = new Set();
560
+ debug;
561
+ constructor(options = {}) {
562
+ this.registry = options.registry;
563
+ this.debug = new DebugEmitter(this.debugSource, options);
564
+ }
565
+ register(input) {
566
+ validateCardId(input.id);
567
+ validateCardType(input.type);
568
+ assertRecordId(input.id, input.data);
569
+ const existing = this.cards.get(input.id);
570
+ if (existing) {
571
+ if (existing.type !== input.type) throw new CardTypeConflictError(input.id, existing.type, input.type);
572
+ return existing;
573
+ }
574
+ const data = cloneJSON(input.data);
575
+ this.assertValid(input.type, data);
576
+ const record = makeRecord(input.id, input.type, data, 0, { status: "idle" });
577
+ this.cards.set(input.id, record);
578
+ this.lastMutationEpoch.set(input.id, this.nextMutationEpoch());
579
+ this.notify(input.id);
580
+ return record;
581
+ }
582
+ get(id) {
583
+ return this.cards.get(id);
584
+ }
585
+ list() {
586
+ return [...this.cards.values()];
587
+ }
588
+ subscribe(id, listener) {
589
+ let listeners = this.listeners.get(id);
590
+ if (!listeners) {
591
+ listeners = new Set();
592
+ this.listeners.set(id, listeners);
593
+ }
594
+ listeners.add(listener);
595
+ return () => {
596
+ listeners?.delete(listener);
597
+ if (listeners?.size === 0) this.listeners.delete(id);
598
+ };
599
+ }
600
+ subscribeAll(listener) {
601
+ this.allListeners.add(listener);
602
+ return () => this.allListeners.delete(listener);
603
+ }
604
+ subscribeDebug(listener) {
605
+ return this.debug.subscribe(listener);
606
+ }
607
+ apply(patch) {
608
+ const next = this.preparePatch(this.cards, patch);
609
+ this.cards.set(next.id, next);
610
+ this.lastMutationEpoch.set(next.id, this.nextMutationEpoch());
611
+ this.notify(next.id);
612
+ if (this.debug.active) this.debug.emit("card-store-patch", {
613
+ patch,
614
+ cards: this.list()
615
+ });
616
+ return next;
617
+ }
618
+ applyAll(patches) {
619
+ if (patches.length > CARD_PATCH_BATCH_MAX_SIZE) throw new CardLimitError(`Card patch batches cannot exceed ${CARD_PATCH_BATCH_MAX_SIZE} operations`);
620
+ const nextCards = new Map(this.cards);
621
+ const changed = new Map();
622
+ for (const patch of patches) {
623
+ const next = this.preparePatch(nextCards, patch);
624
+ nextCards.set(next.id, next);
625
+ changed.set(next.id, next);
626
+ }
627
+ this.cards = nextCards;
628
+ for (const id of changed.keys()) this.lastMutationEpoch.set(id, this.nextMutationEpoch());
629
+ for (const id of changed.keys()) this.notifyOne(id);
630
+ if (changed.size) this.notifyAll();
631
+ if (changed.size && this.debug.active) {
632
+ const cards = this.list();
633
+ this.debug.emit("card-store-change", {
634
+ cardIds: [...changed.keys()],
635
+ cards
636
+ });
637
+ this.debug.emit("card-store-patch", {
638
+ patch: {
639
+ op: "batch",
640
+ patches
641
+ },
642
+ cards
643
+ });
644
+ }
645
+ return [...changed.values()];
646
+ }
647
+ delete(id) {
648
+ if (!this.cards.delete(id)) return false;
649
+ this.lastMutationEpoch.delete(id);
650
+ this.nextMutationEpoch();
651
+ this.notify(id);
287
652
  return true;
288
653
  }
289
- if (schema.type === "array") {
290
- if (!Array.isArray(data)) return false;
291
- return schema.items ? data.every((d) => validateSchema(schema.items, d)) : true;
654
+ clear() {
655
+ if (this.cards.size === 0) return;
656
+ const ids = [...this.cards.keys()];
657
+ this.cards.clear();
658
+ this.lastMutationEpoch.clear();
659
+ this.nextMutationEpoch();
660
+ for (const id of ids) this.notifyOne(id);
661
+ this.notifyAll();
662
+ if (this.debug.active) this.debug.emit("card-store-change", {
663
+ operation: "clear",
664
+ cardIds: ids,
665
+ cards: []
666
+ });
667
+ }
668
+ snapshot() {
669
+ return {
670
+ version: 1,
671
+ cards: this.list().map(({ id, type, data, revision }) => ({
672
+ id,
673
+ type,
674
+ data: cloneJSON(data),
675
+ revision
676
+ }))
677
+ };
678
+ }
679
+ restore(snapshot) {
680
+ if (!snapshot || snapshot.version !== 1 || !Array.isArray(snapshot.cards)) throw new CardSnapshotError("Unsupported card snapshot");
681
+ const restored = new Map();
682
+ for (const input of snapshot.cards) {
683
+ if (!input || typeof input !== "object") throw new CardSnapshotError("Invalid card snapshot record");
684
+ validateCardId(input.id);
685
+ validateCardType(input.type);
686
+ if (!Number.isSafeInteger(input.revision) || input.revision < 0) throw new CardSnapshotError(`Invalid revision for card "${input.id}"`);
687
+ if (restored.has(input.id)) throw new CardSnapshotError(`Duplicate card id "${input.id}" in snapshot`);
688
+ const data = cloneJSON(input.data);
689
+ try {
690
+ assertRecordId(input.id, data);
691
+ } catch (cause) {
692
+ throw new CardSnapshotError(`Card snapshot data id does not match record id "${input.id}"`, { cause });
693
+ }
694
+ this.assertValid(input.type, data);
695
+ restored.set(input.id, makeRecord(input.id, input.type, data, input.revision, { status: "idle" }));
696
+ }
697
+ const ids = new Set([...this.cards.keys(), ...restored.keys()]);
698
+ this.cards = restored;
699
+ this.lastMutationEpoch = new Map([...restored.keys()].map((id) => [id, this.nextMutationEpoch()]));
700
+ if (restored.size === 0 && ids.size > 0) this.nextMutationEpoch();
701
+ for (const id of ids) this.notifyOne(id);
702
+ if (ids.size) this.notifyAll();
703
+ if (ids.size && this.debug.active) this.debug.emit("card-store-change", {
704
+ operation: "restore",
705
+ cardIds: [...ids],
706
+ cards: this.list()
707
+ });
708
+ }
709
+ beginAction(id, actionId) {
710
+ return this.setAction(id, actionId, {
711
+ status: "loading",
712
+ actionId
713
+ }, true);
714
+ }
715
+ succeedAction(id, actionId) {
716
+ return this.setAction(id, actionId, {
717
+ status: "success",
718
+ actionId
719
+ });
720
+ }
721
+ failAction(id, actionId, error) {
722
+ return this.setAction(id, actionId, {
723
+ status: "error",
724
+ actionId,
725
+ error: normalizeCardActionError(error)
726
+ });
727
+ }
728
+ cancelAction(id, actionId) {
729
+ return this.setAction(id, actionId, { status: "idle" });
730
+ }
731
+ isActionCurrent(id, actionId) {
732
+ const action = this.cards.get(id)?.action;
733
+ return action?.status === "loading" && action.actionId === actionId;
734
+ }
735
+ revisions() {
736
+ return new Map([...this.cards].map(([id, card]) => [id, card.revision]));
737
+ }
738
+ captureMutationEpoch() {
739
+ return this.mutationEpoch;
740
+ }
741
+ applyActionResult(result, startedEpoch) {
742
+ const patches = result.op === "batch" ? result.patches : [result];
743
+ if (patches.length > CARD_PATCH_BATCH_MAX_SIZE) throw new CardLimitError(`Card patch batches cannot exceed ${CARD_PATCH_BATCH_MAX_SIZE} operations`);
744
+ for (const cardId of new Set(patches.map((patch) => patch.cardId))) {
745
+ const current = this.cards.get(cardId);
746
+ if (!current) throw new CardNotFoundError(cardId);
747
+ const lastMutationEpoch = this.lastMutationEpoch.get(cardId);
748
+ if (lastMutationEpoch === void 0 || lastMutationEpoch > startedEpoch) throw new CardRevisionConflictError(cardId, startedEpoch, lastMutationEpoch ?? this.mutationEpoch);
749
+ }
750
+ return result.op === "batch" ? this.applyAll(result.patches) : [this.apply(result)];
751
+ }
752
+ preparePatch(cards, patch) {
753
+ assertPatch(patch);
754
+ const current = cards.get(patch.cardId);
755
+ if (!current) throw new CardNotFoundError(patch.cardId);
756
+ if (patch.revision !== void 0 && patch.revision !== current.revision) throw new CardRevisionConflictError(patch.cardId, patch.revision, current.revision);
757
+ const patchData = cloneJSON(patch.data);
758
+ assertPatchId(current.id, patchData);
759
+ const data = patch.op === "replace" ? patchData : mergeJSON(current.data, patchData);
760
+ assertStableId(current.id, current.data, data);
761
+ this.assertValid(current.type, data);
762
+ return makeRecord(current.id, current.type, data, current.revision + 1, current.action);
763
+ }
764
+ assertValid(type, data) {
765
+ if (this.registry && !this.registry.validate(type, data)) throw new CardValidationError(type);
766
+ }
767
+ setAction(id, actionId, action, start = false) {
768
+ const current = this.cards.get(id);
769
+ if (!current) throw new CardNotFoundError(id);
770
+ if (!start && (current.action.status !== "loading" || current.action.actionId !== actionId)) return false;
771
+ this.cards.set(id, makeRecord(current.id, current.type, current.data, current.revision, action));
772
+ this.notify(id);
773
+ return true;
774
+ }
775
+ nextMutationEpoch() {
776
+ return ++this.mutationEpoch;
777
+ }
778
+ notify(id) {
779
+ this.notifyOne(id);
780
+ this.notifyAll();
781
+ if (this.debug.active) this.debug.emit("card-store-change", {
782
+ cardId: id,
783
+ cards: this.list()
784
+ });
785
+ }
786
+ notifyOne(id) {
787
+ const card = this.cards.get(id);
788
+ for (const listener of this.listeners.get(id) ?? []) safeCall(listener, card);
789
+ }
790
+ notifyAll() {
791
+ const cards = this.list();
792
+ for (const listener of this.allListeners) safeCall(listener, cards);
793
+ }
794
+ };
795
+ var CardStoreError = class extends Error {
796
+ constructor(message, options) {
797
+ super(message, options);
798
+ this.name = new.target.name;
799
+ }
800
+ };
801
+ var CardNotFoundError = class extends CardStoreError {
802
+ constructor(id) {
803
+ super(`Card "${id}" was not found`);
804
+ }
805
+ };
806
+ var CardTypeConflictError = class extends CardStoreError {
807
+ constructor(id, currentType, nextType) {
808
+ super(`Card "${id}" is already registered as "${currentType}", not "${nextType}"`);
809
+ }
810
+ };
811
+ var CardValidationError = class extends CardStoreError {
812
+ constructor(type) {
813
+ super(`Card data is invalid for type "${type}"`);
814
+ }
815
+ };
816
+ var CardRevisionConflictError = class extends CardStoreError {
817
+ constructor(id, expected, actual) {
818
+ super(`Card "${id}" revision conflict: expected ${expected}, actual ${actual}`);
819
+ }
820
+ };
821
+ var CardJSONError = class extends CardStoreError {};
822
+ var CardLimitError = class extends CardStoreError {};
823
+ var CardSnapshotError = class extends CardStoreError {};
824
+ function isCardPatchResult(value) {
825
+ if (!isPlainObject(value) || typeof value.op !== "string") return false;
826
+ if (value.op === "batch") return Array.isArray(value.patches) && value.patches.every(isCardPatch);
827
+ return isCardPatch(value);
828
+ }
829
+ function isCardPatch(value) {
830
+ 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));
831
+ }
832
+ function assertPatch(patch) {
833
+ if (!isCardPatch(patch)) throw new CardStoreError("Invalid card patch");
834
+ validateCardId(patch.cardId);
835
+ if (patch.revision !== void 0 && patch.revision < 0) throw new CardStoreError("Card patch revision must be non-negative");
836
+ }
837
+ function validateCardId(id) {
838
+ 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`);
839
+ }
840
+ function validateCardType(type) {
841
+ if (typeof type !== "string" || type.trim().length === 0) throw new CardStoreError("Card type must be a non-empty string");
842
+ }
843
+ function cloneJSON(value) {
844
+ let nodes = 0;
845
+ const path = new WeakSet();
846
+ const clone = (input, depth) => {
847
+ nodes++;
848
+ if (nodes > CARD_JSON_MAX_NODES) throw new CardLimitError(`Card JSON cannot exceed ${CARD_JSON_MAX_NODES} nodes`);
849
+ if (depth > CARD_JSON_MAX_DEPTH) throw new CardLimitError(`Card JSON cannot exceed depth ${CARD_JSON_MAX_DEPTH}`);
850
+ if (input === null || typeof input === "string" || typeof input === "boolean") return input;
851
+ if (typeof input === "number" && Number.isFinite(input)) return input;
852
+ if (typeof input !== "object") throw new CardJSONError("Card data must contain only JSON values");
853
+ if (path.has(input)) throw new CardJSONError("Card data cannot contain cycles");
854
+ path.add(input);
855
+ try {
856
+ if (Array.isArray(input)) {
857
+ if (Object.keys(input).length !== input.length) throw new CardJSONError("Card arrays must not be sparse");
858
+ return Object.freeze(input.map((item) => clone(item, depth + 1)));
859
+ }
860
+ if (!isPlainObject(input)) throw new CardJSONError("Card objects must be plain JSON objects");
861
+ const output = {};
862
+ for (const key of Object.keys(input)) {
863
+ if (isDangerousKey(key)) throw new CardJSONError(`Card data contains dangerous key "${key}"`);
864
+ Object.defineProperty(output, key, {
865
+ value: clone(input[key], depth + 1),
866
+ enumerable: true,
867
+ writable: false,
868
+ configurable: false
869
+ });
870
+ }
871
+ return Object.freeze(output);
872
+ } finally {
873
+ path.delete(input);
874
+ }
875
+ };
876
+ return clone(value, 0);
877
+ }
878
+ function mergeJSON(current, patch) {
879
+ if (!isPlainObject(current) || !isPlainObject(patch)) return patch;
880
+ const output = {};
881
+ for (const key of Object.keys(current)) output[key] = current[key];
882
+ for (const key of Object.keys(patch)) output[key] = mergeJSON(current[key], patch[key]);
883
+ return cloneJSON(output);
884
+ }
885
+ function assertPatchId(cardId, data) {
886
+ if (isPlainObject(data) && Object.hasOwn(data, "id") && data.id !== cardId) throw new CardStoreError(`Card patch cannot change id from "${cardId}"`);
887
+ }
888
+ function assertRecordId(cardId, data) {
889
+ if (isPlainObject(data) && Object.hasOwn(data, "id") && data.id !== cardId) throw new CardStoreError(`Card data id must match record id "${cardId}"`);
890
+ }
891
+ function assertStableId(cardId, current, next) {
892
+ const currentHasId = isPlainObject(current) && Object.hasOwn(current, "id");
893
+ const nextHasId = isPlainObject(next) && Object.hasOwn(next, "id");
894
+ if (currentHasId !== nextHasId || nextHasId && next.id !== cardId) throw new CardStoreError(`Card patch cannot change id from "${cardId}"`);
895
+ }
896
+ function makeRecord(id, type, data, revision, action) {
897
+ return Object.freeze({
898
+ id,
899
+ type,
900
+ data,
901
+ revision,
902
+ action: Object.freeze(action)
903
+ });
904
+ }
905
+ function normalizeCardActionError(error) {
906
+ if (error instanceof Error) return Object.freeze({
907
+ name: error.name || "Error",
908
+ message: error.message
909
+ });
910
+ return Object.freeze({
911
+ name: "Error",
912
+ message: typeof error === "string" ? error : "Unknown error"
913
+ });
914
+ }
915
+ function isPlainObject(value) {
916
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
917
+ const prototype = Object.getPrototypeOf(value);
918
+ if (prototype !== Object.prototype && prototype !== null) return false;
919
+ if (Object.getOwnPropertySymbols(value).length) return false;
920
+ return Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => descriptor.enumerable && "value" in descriptor);
921
+ }
922
+ function isDangerousKey(key) {
923
+ return key === "__proto__" || key === "prototype" || key === "constructor";
924
+ }
925
+ function safeCall(listener, ...args) {
926
+ try {
927
+ listener(...args);
928
+ } catch {}
929
+ }
930
+
931
+ //#endregion
932
+ //#region src/actions.ts
933
+ let nextRuntimeId = 0;
934
+ var ActionRegistry = class {
935
+ actions = new Map();
936
+ register(definition, options = {}) {
937
+ if (typeof definition?.type !== "string" || definition.type.trim() === "") throw new TypeError("Action type must be a non-empty string");
938
+ if (typeof definition.run !== "function") throw new TypeError(`Action "${definition.type}" run must be a function`);
939
+ if (this.actions.has(definition.type) && !options.override) throw new ActionAlreadyRegisteredError(definition.type);
940
+ this.actions.set(definition.type, definition);
941
+ }
942
+ has(type) {
943
+ return this.actions.has(type);
944
+ }
945
+ get(type) {
946
+ return this.actions.get(type);
947
+ }
948
+ list() {
949
+ return [...this.actions.values()];
950
+ }
951
+ };
952
+ var ActionRuntime = class {
953
+ debugSource = "action-runtime";
954
+ registry;
955
+ cardStore;
956
+ defaultTimeoutMs;
957
+ onActionStart;
958
+ onActionSuccess;
959
+ onActionError;
960
+ states = new Map();
961
+ pending = new Map();
962
+ listeners = new Set();
963
+ defaultOwner = {};
964
+ runtimeId = ++nextRuntimeId;
965
+ generation = 0;
966
+ nextActionId = 0;
967
+ destroyed = false;
968
+ debug;
969
+ constructor(options) {
970
+ this.debug = new DebugEmitter(this.debugSource, options);
971
+ this.registry = options.registry;
972
+ this.cardStore = options.cardStore;
973
+ this.defaultTimeoutMs = options.timeoutMs;
974
+ this.onActionStart = options.onActionStart;
975
+ this.onActionSuccess = options.onActionSuccess;
976
+ this.onActionError = options.onActionError;
977
+ }
978
+ dispatch(request, options = {}) {
979
+ if (this.destroyed) return Promise.reject(new ActionDestroyedError());
980
+ const key = getActionKey(request.type, request.cardType, request.cardId);
981
+ const owner = options.owner ?? this.defaultOwner;
982
+ const existing = this.pending.get(key)?.get(owner);
983
+ if (existing) return existing.promise;
984
+ const actionId = `r${this.runtimeId}:${request.type}:${++this.nextActionId}`;
985
+ const event = {
986
+ key,
987
+ type: request.type,
988
+ params: request.params,
989
+ actionId,
990
+ cardType: request.cardType,
991
+ cardId: request.cardId
992
+ };
993
+ if (this.debug.active) this.debug.emit("action-dispatched", { ...event });
994
+ const definition = this.registry.get(request.type);
995
+ if (!definition) return this.rejectPreflight(event, new ActionNotFoundError(request.type));
996
+ if (definition.schema !== void 0) try {
997
+ const validation = validateJSONSchema(definition.schema, request.params);
998
+ if (!validation.valid) return this.rejectPreflight(event, new ActionValidationError(request.type, validation.issues));
999
+ } catch (cause) {
1000
+ return this.rejectPreflight(event, new ActionExecutionError(request.type, cause));
1001
+ }
1002
+ let startedMutationEpoch;
1003
+ try {
1004
+ startedMutationEpoch = this.cardStore?.captureMutationEpoch();
1005
+ } catch (cause) {
1006
+ return this.rejectPreflight(event, new ActionExecutionError(request.type, cause));
1007
+ }
1008
+ if (request.cardId && this.cardStore) try {
1009
+ this.cardStore.beginAction(request.cardId, actionId);
1010
+ } catch (cause) {
1011
+ return this.rejectPreflight(event, new ActionExecutionError(request.type, cause));
1012
+ }
1013
+ const generation = this.generation;
1014
+ const controller = new AbortController();
1015
+ const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
1016
+ let timer;
1017
+ let timedOut = false;
1018
+ let settled = false;
1019
+ let resolvePromise;
1020
+ let rejectPromise;
1021
+ const promise = new Promise((resolve, reject) => {
1022
+ resolvePromise = resolve;
1023
+ rejectPromise = reject;
1024
+ });
1025
+ const state = {
1026
+ status: "pending",
1027
+ key,
1028
+ type: request.type,
1029
+ cardType: request.cardType,
1030
+ cardId: request.cardId,
1031
+ actionId
1032
+ };
1033
+ const pending = {
1034
+ controller,
1035
+ promise
1036
+ };
1037
+ const runtime = this;
1038
+ let cleanupExternalSignal = () => {};
1039
+ let cleanupAbortSignal = () => {};
1040
+ let owners = this.pending.get(key);
1041
+ if (!owners) {
1042
+ owners = new Map();
1043
+ this.pending.set(key, owners);
1044
+ }
1045
+ owners.set(owner, pending);
1046
+ const cleanup = () => {
1047
+ if (timer !== void 0) clearTimeout(timer);
1048
+ cleanupExternalSignal();
1049
+ cleanupAbortSignal();
1050
+ const currentOwners = this.pending.get(key);
1051
+ if (currentOwners?.get(owner) === pending) {
1052
+ currentOwners.delete(owner);
1053
+ if (currentOwners.size === 0) this.pending.delete(key);
1054
+ }
1055
+ };
1056
+ const finishSuccess = (result) => {
1057
+ if (settled) return;
1058
+ const wasPublic = this.isPublic(key, actionId, generation);
1059
+ const canAffectCard = !request.cardId || !this.cardStore || this.cardStore.isActionCurrent(request.cardId, actionId);
1060
+ const canApplyResult = request.cardId ? canAffectCard : wasPublic;
1061
+ if (canApplyResult && this.cardStore && startedMutationEpoch !== void 0 && isCardPatchResult(result)) try {
1062
+ this.cardStore.applyActionResult(result, startedMutationEpoch);
1063
+ } catch (error) {
1064
+ finishError(error);
1065
+ return;
1066
+ }
1067
+ settled = true;
1068
+ cleanup();
1069
+ if (wasPublic) {
1070
+ this.commit({
1071
+ status: "success",
1072
+ key,
1073
+ type: request.type,
1074
+ cardType: request.cardType,
1075
+ cardId: request.cardId,
1076
+ actionId,
1077
+ result
1078
+ });
1079
+ callEventHandler(this.onActionSuccess, {
1080
+ ...event,
1081
+ result
1082
+ });
1083
+ if (this.debug.active) this.debug.emit("action-success", {
1084
+ ...event,
1085
+ result
1086
+ });
1087
+ }
1088
+ if (request.cardId && this.cardStore && canAffectCard) settleCardAction(() => this.cardStore?.succeedAction(request.cardId, actionId));
1089
+ resolvePromise(result);
1090
+ };
1091
+ function finishError(cause) {
1092
+ if (settled) return;
1093
+ settled = true;
1094
+ const wasPublic = runtime.isPublic(key, actionId, generation);
1095
+ cleanup();
1096
+ const error = normalizeError(request.type, cause, controller.signal.aborted, timedOut, timeoutMs);
1097
+ if (request.cardId && runtime.cardStore) settleCardAction(() => error instanceof ActionAbortedError ? runtime.cardStore?.cancelAction(request.cardId, actionId) : runtime.cardStore?.failAction(request.cardId, actionId, error));
1098
+ if (wasPublic) {
1099
+ const state$1 = error instanceof ActionAbortedError ? {
1100
+ status: "cancelled",
1101
+ key,
1102
+ type: request.type,
1103
+ cardType: request.cardType,
1104
+ actionId,
1105
+ error
1106
+ } : {
1107
+ status: "error",
1108
+ key,
1109
+ type: request.type,
1110
+ cardType: request.cardType,
1111
+ cardId: request.cardId,
1112
+ actionId,
1113
+ error
1114
+ };
1115
+ if (request.cardId) state$1.cardId = request.cardId;
1116
+ runtime.commit(state$1);
1117
+ callEventHandler(runtime.onActionError, {
1118
+ ...event,
1119
+ error
1120
+ });
1121
+ if (runtime.debug.active) runtime.debug.emit("action-error", {
1122
+ ...event,
1123
+ error
1124
+ });
1125
+ }
1126
+ rejectPromise(error);
1127
+ }
1128
+ this.commit(state);
1129
+ callEventHandler(this.onActionStart, event);
1130
+ cleanupExternalSignal = forwardAbort(options.signal, controller);
1131
+ cleanupAbortSignal = listenForAbort(controller.signal, (cause) => finishError(cause));
1132
+ if (timeoutMs !== void 0 && timeoutMs > 0 && !settled) timer = setTimeout(() => {
1133
+ timedOut = true;
1134
+ controller.abort(new ActionTimeoutError(request.type, timeoutMs));
1135
+ }, timeoutMs);
1136
+ if (controller.signal.aborted || this.generation !== generation || this.destroyed) {
1137
+ finishError(controller.signal.reason ?? new ActionAbortedError(request.type));
1138
+ return promise;
1139
+ }
1140
+ try {
1141
+ const result = definition.run(request.params, {
1142
+ signal: controller.signal,
1143
+ actionId,
1144
+ cardType: request.cardType,
1145
+ cardId: request.cardId
1146
+ });
1147
+ Promise.resolve(result).then(finishSuccess, finishError);
1148
+ } catch (error) {
1149
+ finishError(error);
1150
+ }
1151
+ return promise;
1152
+ }
1153
+ getState(key) {
1154
+ return this.states.get(key) ?? getIdleActionState(key);
1155
+ }
1156
+ /** Check the runtime allowlist without exposing executable action definitions. */
1157
+ hasAction(type) {
1158
+ return !this.destroyed && this.registry.has(type);
1159
+ }
1160
+ /** List registered action names without exposing executable definitions. */
1161
+ listActionTypes() {
1162
+ return this.destroyed ? [] : Object.freeze(this.registry.list().map((action) => action.type));
1163
+ }
1164
+ subscribe(listener) {
1165
+ if (this.destroyed) return () => {};
1166
+ this.listeners.add(listener);
1167
+ return () => this.listeners.delete(listener);
1168
+ }
1169
+ subscribeDebug(listener) {
1170
+ return this.debug.subscribe(listener);
1171
+ }
1172
+ cancel(key) {
1173
+ const actions = this.pending.get(key);
1174
+ if (!actions?.size) return false;
1175
+ const type = this.states.get(key)?.type ?? getIdleActionState(key).type;
1176
+ for (const action of [...actions.values()]) action.controller.abort(new ActionAbortedError(type));
1177
+ return true;
1178
+ }
1179
+ reset() {
1180
+ const oldStates = [...this.states.values()];
1181
+ this.generation++;
1182
+ const actions = [...this.pending.values()].flatMap((owners) => [...owners.values()]);
1183
+ for (const action of actions) action.controller.abort(new ActionAbortedError("Action"));
1184
+ this.pending.clear();
1185
+ this.states.clear();
1186
+ for (const state of oldStates) this.notify(idleStateFrom(state));
1187
+ }
1188
+ destroy() {
1189
+ if (this.destroyed) return;
1190
+ this.reset();
1191
+ this.destroyed = true;
1192
+ this.listeners.clear();
1193
+ }
1194
+ rejectPreflight(event, error) {
1195
+ const errorState = {
1196
+ status: "error",
1197
+ key: event.key,
1198
+ type: event.type,
1199
+ cardType: event.cardType,
1200
+ cardId: event.cardId,
1201
+ actionId: event.actionId,
1202
+ error
1203
+ };
1204
+ this.commit(errorState);
1205
+ callEventHandler(this.onActionError, {
1206
+ ...event,
1207
+ error
1208
+ });
1209
+ if (this.debug.active) this.debug.emit("action-error", {
1210
+ ...event,
1211
+ error
1212
+ });
1213
+ return Promise.reject(error);
1214
+ }
1215
+ isPublic(key, actionId, generation) {
1216
+ return !this.destroyed && this.generation === generation && this.states.get(key)?.actionId === actionId;
1217
+ }
1218
+ commit(state) {
1219
+ this.states.set(state.key, state);
1220
+ if (this.debug.active) this.debug.emit("action-state", { ...state });
1221
+ this.notify(state);
1222
+ }
1223
+ notify(state) {
1224
+ for (const listener of this.listeners) try {
1225
+ listener(state);
1226
+ } catch {}
1227
+ }
1228
+ };
1229
+ function createActionRuntime(options) {
1230
+ return new ActionRuntime(options);
1231
+ }
1232
+ var ActionRuntimeError = class extends Error {
1233
+ constructor(message, options) {
1234
+ super(message, options);
1235
+ this.name = new.target.name;
1236
+ }
1237
+ };
1238
+ var ActionAlreadyRegisteredError = class extends ActionRuntimeError {
1239
+ constructor(type) {
1240
+ super(`Action "${type}" is already registered`);
1241
+ }
1242
+ };
1243
+ var ActionNotFoundError = class extends ActionRuntimeError {
1244
+ constructor(type) {
1245
+ super(`Action "${type}" is not registered`);
1246
+ }
1247
+ };
1248
+ var ActionValidationError = class extends ActionRuntimeError {
1249
+ constructor(actionType, issues) {
1250
+ super(`Action "${actionType}" parameters are invalid: ${issues.join(", ")}`);
1251
+ this.actionType = actionType;
1252
+ this.issues = issues;
1253
+ }
1254
+ };
1255
+ var ActionExecutionError = class extends ActionRuntimeError {
1256
+ constructor(type, cause) {
1257
+ super(`Action "${type}" failed`, { cause });
1258
+ }
1259
+ };
1260
+ var ActionAbortedError = class extends ActionRuntimeError {
1261
+ constructor(type) {
1262
+ super(`Action "${type}" was cancelled`);
1263
+ }
1264
+ };
1265
+ var ActionTimeoutError = class extends ActionRuntimeError {
1266
+ constructor(type, timeoutMs) {
1267
+ super(`Action "${type}" timed out after ${timeoutMs}ms`);
1268
+ this.timeoutMs = timeoutMs;
1269
+ }
1270
+ };
1271
+ var ActionDestroyedError = class extends ActionRuntimeError {
1272
+ constructor() {
1273
+ super("Action runtime has been destroyed");
1274
+ }
1275
+ };
1276
+ function getActionKey(type, cardType, cardId) {
1277
+ if (cardId !== void 0) return `::${JSON.stringify([
1278
+ cardType ?? null,
1279
+ type,
1280
+ cardId
1281
+ ])}`;
1282
+ if (!type.includes(":") && !cardType?.includes(":")) return cardType ? `${cardType}:${type}` : type;
1283
+ return `::${JSON.stringify([cardType ?? null, type])}`;
1284
+ }
1285
+ function getIdleActionState(key) {
1286
+ if (key.startsWith("::")) try {
1287
+ const [cardType, type, cardId] = JSON.parse(key.slice(2));
1288
+ if (cardId !== void 0) return cardType === null ? {
1289
+ status: "idle",
1290
+ key,
1291
+ type,
1292
+ cardId
1293
+ } : {
1294
+ status: "idle",
1295
+ key,
1296
+ cardType,
1297
+ type,
1298
+ cardId
1299
+ };
1300
+ return cardType === null ? {
1301
+ status: "idle",
1302
+ key,
1303
+ type
1304
+ } : {
1305
+ status: "idle",
1306
+ key,
1307
+ cardType,
1308
+ type
1309
+ };
1310
+ } catch {
1311
+ return {
1312
+ status: "idle",
1313
+ key,
1314
+ type: key
1315
+ };
292
1316
  }
293
- if (schema.type === "string") return typeof data === "string";
294
- if (schema.type === "number") return typeof data === "number";
295
- if (schema.type === "boolean") return typeof data === "boolean";
296
- return true;
1317
+ const separator = key.indexOf(":");
1318
+ return separator === -1 ? {
1319
+ status: "idle",
1320
+ key,
1321
+ type: key
1322
+ } : {
1323
+ status: "idle",
1324
+ key,
1325
+ cardType: key.slice(0, separator),
1326
+ type: key.slice(separator + 1)
1327
+ };
1328
+ }
1329
+ function idleStateFrom(state) {
1330
+ return {
1331
+ status: "idle",
1332
+ key: state.key,
1333
+ type: state.type,
1334
+ ...state.cardType === void 0 ? {} : { cardType: state.cardType },
1335
+ ...state.cardId === void 0 ? {} : { cardId: state.cardId }
1336
+ };
1337
+ }
1338
+ function listenForAbort(signal, onAbort) {
1339
+ const abort = () => onAbort(signal.reason);
1340
+ if (signal.aborted) abort();
1341
+ else signal.addEventListener("abort", abort, { once: true });
1342
+ return () => signal.removeEventListener("abort", abort);
1343
+ }
1344
+ function forwardAbort(signal, controller) {
1345
+ if (!signal) return () => {};
1346
+ const abort = () => controller.abort(signal.reason);
1347
+ if (signal.aborted) abort();
1348
+ else signal.addEventListener("abort", abort, { once: true });
1349
+ return () => signal.removeEventListener("abort", abort);
1350
+ }
1351
+ function normalizeError(type, cause, aborted, timedOut, timeoutMs) {
1352
+ if (cause instanceof ActionRuntimeError) return cause;
1353
+ if (timedOut) return new ActionTimeoutError(type, timeoutMs ?? 0);
1354
+ if (aborted || isAbortError(cause)) return new ActionAbortedError(type);
1355
+ return new ActionExecutionError(type, cause);
1356
+ }
1357
+ function isAbortError(error) {
1358
+ return error instanceof DOMException && error.name === "AbortError";
1359
+ }
1360
+ function callEventHandler(handler, event) {
1361
+ try {
1362
+ handler?.(event);
1363
+ } catch {}
1364
+ }
1365
+ function settleCardAction(settle) {
1366
+ try {
1367
+ settle();
1368
+ } catch {}
297
1369
  }
298
1370
 
299
1371
  //#endregion
300
1372
  //#region src/plugins.ts
301
1373
  /** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
302
- function collectNodeRenderers(plugins = []) {
1374
+ function collectNodeRenderers(plugins = [], debugOptions = {}) {
303
1375
  const map = {};
304
- for (const p of plugins) for (const [k, render] of Object.entries(p.nodeRenderers ?? {})) map[k] = (node) => node.complete === false ? {
305
- kind: "html",
306
- html: `<div data-aigui-block-loading="" data-block-type="${escapeAttr(node.type)}"></div>`
307
- } : render(node);
1376
+ const target = debugOptions.debugTarget;
1377
+ const debug = target ? void 0 : debugOptions.debug === true ? new DebugEmitter("renderer", debugOptions) : void 0;
1378
+ for (const p of plugins) for (const [k, render] of Object.entries(p.nodeRenderers ?? {})) {
1379
+ if (!(target?.debugEnabled || debugOptions.debug === true)) {
1380
+ map[k] = render;
1381
+ continue;
1382
+ }
1383
+ map[k] = (node) => {
1384
+ const emit = (type, data) => target?.emitDebug(type, data) ?? debug?.emit(type, data);
1385
+ emit("plugin-render-started", {
1386
+ plugin: p.name,
1387
+ nodeType: node.type,
1388
+ nodeKey: node.key
1389
+ });
1390
+ try {
1391
+ const output = render(node);
1392
+ if (isPromise(output)) return output.then((resolved) => {
1393
+ emit("async-output-resolved", {
1394
+ plugin: p.name,
1395
+ nodeType: node.type,
1396
+ nodeKey: node.key,
1397
+ outputKind: resolved.kind
1398
+ });
1399
+ emit("plugin-render-completed", {
1400
+ plugin: p.name,
1401
+ nodeType: node.type,
1402
+ nodeKey: node.key,
1403
+ outputKind: resolved.kind
1404
+ });
1405
+ return resolved;
1406
+ }, (error) => {
1407
+ emit("async-output-rejected", {
1408
+ plugin: p.name,
1409
+ nodeType: node.type,
1410
+ nodeKey: node.key,
1411
+ error
1412
+ });
1413
+ emit("plugin-render-failed", {
1414
+ plugin: p.name,
1415
+ nodeType: node.type,
1416
+ nodeKey: node.key,
1417
+ error
1418
+ });
1419
+ throw error;
1420
+ });
1421
+ emit("plugin-render-completed", {
1422
+ plugin: p.name,
1423
+ nodeType: node.type,
1424
+ nodeKey: node.key,
1425
+ outputKind: output.kind
1426
+ });
1427
+ return output;
1428
+ } catch (error) {
1429
+ emit("plugin-render-failed", {
1430
+ plugin: p.name,
1431
+ nodeType: node.type,
1432
+ nodeKey: node.key,
1433
+ error
1434
+ });
1435
+ throw error;
1436
+ }
1437
+ };
1438
+ }
308
1439
  return map;
309
1440
  }
310
1441
  /** The set of node types claimed by the given plugins. */
311
1442
  function pluginNodeTypes(plugins = []) {
312
1443
  return new Set(Object.keys(collectNodeRenderers(plugins)));
313
1444
  }
314
- function escapeAttr(value) {
315
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1445
+ function isPromise(value) {
1446
+ return typeof value?.then === "function";
316
1447
  }
317
1448
 
318
1449
  //#endregion
@@ -366,13 +1497,15 @@ function createParserWithMetadata(options = {}) {
366
1497
  const cardType = info.slice(5);
367
1498
  const res = options.registry.parse(cardType, t.content);
368
1499
  const complete = res.complete && isClosedFence(rawSrc, t.map, t.markup);
1500
+ const idResult = complete && res.valid ? getCardId(res.data) : void 0;
369
1501
  addNode({
370
1502
  type: "card",
371
1503
  card: {
372
1504
  type: cardType,
373
1505
  data: res.data,
374
1506
  complete,
375
- valid: complete && res.valid
1507
+ valid: complete && res.valid && idResult !== false,
1508
+ ...typeof idResult === "string" ? { id: idResult } : {}
376
1509
  }
377
1510
  }, t.map);
378
1511
  } else if (pluginTypes.has(info)) {
@@ -478,6 +1611,11 @@ function createParserWithMetadata(options = {}) {
478
1611
  };
479
1612
  };
480
1613
  }
1614
+ function getCardId(data) {
1615
+ if (typeof data !== "object" || data === null || Array.isArray(data) || !Object.hasOwn(data, "id")) return void 0;
1616
+ const id = data.id;
1617
+ return typeof id === "string" && id.trim().length > 0 && id.length <= 256 ? id : false;
1618
+ }
481
1619
  function getLineOffsets(src) {
482
1620
  const offsets = [0];
483
1621
  for (let i = 0; i < src.length; i++) if (src[i] === "\r") {
@@ -588,6 +1726,7 @@ function nodeEqual(a, b) {
588
1726
  * the resulting patches via `onPatch`.
589
1727
  */
590
1728
  var Renderer = class {
1729
+ debugSource = "renderer";
591
1730
  buffer = "";
592
1731
  prevAst = [];
593
1732
  parse;
@@ -598,8 +1737,10 @@ var Renderer = class {
598
1737
  activeFeed;
599
1738
  renderScheduled = false;
600
1739
  scheduleGeneration = 0;
1740
+ debug;
601
1741
  constructor(options = {}) {
602
1742
  this.options = options;
1743
+ this.debug = new DebugEmitter(this.debugSource, options);
603
1744
  this.sanitize = options.sanitize === false ? false : typeof options.sanitize === "object" ? options.sanitize : {};
604
1745
  if (options.registry) for (const plugin of options.plugins ?? []) for (const card of plugin.cards ?? []) options.registry.register(card);
605
1746
  this.parse = createParserWithMetadata({
@@ -607,21 +1748,36 @@ var Renderer = class {
607
1748
  plugins: options.plugins
608
1749
  });
609
1750
  }
1751
+ get debugEnabled() {
1752
+ return this.debug.available;
1753
+ }
1754
+ emitDebug(type, data = {}) {
1755
+ this.debug.emit(type, data);
1756
+ }
610
1757
  push(chunk) {
611
1758
  this.buffer += chunk;
1759
+ if (this.debug.active) this.debug.emit("chunk-received", {
1760
+ chunk,
1761
+ length: chunk.length,
1762
+ bufferLength: this.buffer.length
1763
+ });
612
1764
  this.scheduleRender();
613
1765
  }
1766
+ subscribeDebug(listener) {
1767
+ return this.debug.subscribe(listener);
1768
+ }
614
1769
  async feed(source, options = {}) {
615
1770
  const generation = ++this.generation;
616
1771
  this.cancelActiveFeed(createAbortError("Superseded by a newer feed"));
1772
+ if (this.debug.active) this.debug.emit("feed-started", { generation });
617
1773
  const decoder = new TextDecoder();
618
1774
  const signal = options.signal;
619
- if (signal?.aborted) throw abortReason(signal);
1775
+ if (signal?.aborted) throw abortReason$1(signal);
620
1776
  let aborted = false;
621
1777
  let abortError;
622
1778
  const abort = () => {
623
1779
  aborted = true;
624
- abortError = abortReason(signal);
1780
+ abortError = abortReason$1(signal);
625
1781
  if (this.activeFeed?.generation === generation) this.cancelActiveFeed(abortError);
626
1782
  };
627
1783
  signal?.addEventListener("abort", abort, { once: true });
@@ -631,7 +1787,7 @@ var Renderer = class {
631
1787
  if (text) this.push(text);
632
1788
  };
633
1789
  try {
634
- if (isReadableStream$1(source)) {
1790
+ if (isReadableStream$2(source)) {
635
1791
  const reader = source.getReader();
636
1792
  let cancelled = false;
637
1793
  this.activeFeed = {
@@ -680,8 +1836,22 @@ var Renderer = class {
680
1836
  if (tail) this.push(tail);
681
1837
  this.flush();
682
1838
  }
683
- if (aborted) throw abortError;
1839
+ if (aborted) {
1840
+ if (this.debug.active) this.debug.emit("feed-cancelled", {
1841
+ generation,
1842
+ error: abortError
1843
+ });
1844
+ throw abortError;
1845
+ }
1846
+ if (generation === this.generation && this.debug.active) this.debug.emit("feed-completed", {
1847
+ generation,
1848
+ bufferLength: this.buffer.length
1849
+ });
684
1850
  } finally {
1851
+ if (!aborted && generation !== this.generation && this.debug.active) this.debug.emit("feed-cancelled", {
1852
+ generation,
1853
+ reason: "superseded-or-reset"
1854
+ });
685
1855
  signal?.removeEventListener("abort", abort);
686
1856
  if (this.activeFeed?.generation === generation) this.activeFeed = void 0;
687
1857
  }
@@ -697,6 +1867,7 @@ var Renderer = class {
697
1867
  this.prevAst = [];
698
1868
  this.parsed = void 0;
699
1869
  this.options.onPatch?.(patches, []);
1870
+ if (this.debug.active) this.debug.emit("renderer-reset", { patches });
700
1871
  }
701
1872
  /** Immediately render pending buffered input, bypassing the scheduler. */
702
1873
  flush() {
@@ -725,16 +1896,34 @@ var Renderer = class {
725
1896
  Promise.resolve(active.cancel(reason)).catch(() => {});
726
1897
  }
727
1898
  render() {
1899
+ const debug = this.debug.active;
1900
+ const renderStarted = debug ? now() : 0;
728
1901
  const previous = this.parsed;
729
1902
  let next;
1903
+ let mode = "full";
730
1904
  if (previous?.incrementalSafe && previous.blocks.length > 1) {
731
1905
  const mutable = previous.blocks.at(-1);
732
1906
  const stableBlocks = previous.blocks.slice(0, -1);
733
1907
  const stableNodeEnd = mutable.nodeStart;
734
1908
  const rawTail = this.buffer.slice(mutable.start);
735
- const tail = this.parse(repairMarkdown(rawTail), rawTail, mutable.start);
1909
+ const repaired = repairMarkdown(rawTail);
1910
+ if (debug) this.debug.emit("markdown-repaired", {
1911
+ mode: "mutable-tail",
1912
+ raw: rawTail,
1913
+ rawLength: rawTail.length,
1914
+ repaired,
1915
+ repairedLength: repaired.length,
1916
+ sourceOffset: mutable.start
1917
+ });
1918
+ const parseStarted = debug ? now() : 0;
1919
+ const tail = this.parse(repaired, rawTail, mutable.start);
1920
+ if (debug) this.debug.emit("mutable-tail-reparsed", {
1921
+ sourceOffset: mutable.start,
1922
+ durationMs: now() - parseStarted
1923
+ });
736
1924
  if (tail.incrementalSafe) {
737
- if (this.sanitize !== false) sanitizeNodes(tail.nodes, this.sanitize);
1925
+ mode = "mutable-tail";
1926
+ if (this.sanitize !== false) this.sanitizeNodesWithDebug(tail.nodes);
738
1927
  next = {
739
1928
  nodes: [...previous.nodes.slice(0, stableNodeEnd), ...tail.nodes],
740
1929
  blocks: [...stableBlocks, ...tail.blocks.map((block) => ({
@@ -744,19 +1933,100 @@ var Renderer = class {
744
1933
  }))],
745
1934
  incrementalSafe: true
746
1935
  };
1936
+ if (debug) this.debug.emit("stable-prefix-committed", {
1937
+ blocks: stableBlocks.length,
1938
+ nodes: stableNodeEnd
1939
+ });
747
1940
  } else {
748
- next = this.parse(repairMarkdown(this.buffer), this.buffer);
749
- if (this.sanitize !== false) sanitizeNodes(next.nodes, this.sanitize);
1941
+ const fullRepaired = repairMarkdown(this.buffer);
1942
+ if (debug) this.debug.emit("markdown-repaired", {
1943
+ mode: "full",
1944
+ raw: this.buffer,
1945
+ rawLength: this.buffer.length,
1946
+ repaired: fullRepaired,
1947
+ repairedLength: fullRepaired.length
1948
+ });
1949
+ const fullParseStarted = debug ? now() : 0;
1950
+ next = this.parse(fullRepaired, this.buffer);
1951
+ if (debug) this.debug.emit("parse-completed", {
1952
+ mode: "full",
1953
+ durationMs: now() - fullParseStarted
1954
+ });
1955
+ if (this.sanitize !== false) this.sanitizeNodesWithDebug(next.nodes);
750
1956
  }
751
1957
  } else {
752
- next = this.parse(repairMarkdown(this.buffer), this.buffer);
753
- if (this.sanitize !== false) sanitizeNodes(next.nodes, this.sanitize);
1958
+ const repaired = repairMarkdown(this.buffer);
1959
+ if (debug) this.debug.emit("markdown-repaired", {
1960
+ mode: "full",
1961
+ raw: this.buffer,
1962
+ rawLength: this.buffer.length,
1963
+ repaired,
1964
+ repairedLength: repaired.length
1965
+ });
1966
+ const parseStarted = debug ? now() : 0;
1967
+ next = this.parse(repaired, this.buffer);
1968
+ if (debug) this.debug.emit("parse-completed", {
1969
+ mode: "full",
1970
+ durationMs: now() - parseStarted
1971
+ });
1972
+ if (this.sanitize !== false) this.sanitizeNodesWithDebug(next.nodes);
754
1973
  }
755
1974
  const nextAst = next.nodes;
1975
+ for (const plugin of this.options.plugins ?? []) {
1976
+ if (!plugin.onASTCommit) continue;
1977
+ try {
1978
+ plugin.onASTCommit(nextAst, {
1979
+ generation: this.generation,
1980
+ emitDebug: (type, data = {}) => this.debug.emit(type, {
1981
+ plugin: plugin.name,
1982
+ ...data
1983
+ })
1984
+ });
1985
+ } catch (error) {
1986
+ if (debug) this.debug.emit("plugin-commit-failed", {
1987
+ plugin: plugin.name,
1988
+ error
1989
+ });
1990
+ }
1991
+ }
1992
+ const diffStarted = debug ? now() : 0;
756
1993
  const patches = diffAst(this.prevAst, nextAst);
1994
+ const diffDurationMs = debug ? now() - diffStarted : 0;
757
1995
  this.parsed = next;
758
1996
  this.prevAst = nextAst;
1997
+ const patchDispatchStarted = debug ? now() : 0;
759
1998
  if (patches.length > 0) this.options.onPatch?.(patches, nextAst);
1999
+ const patchDispatchDurationMs = debug ? now() - patchDispatchStarted : 0;
2000
+ if (debug) {
2001
+ this.debug.emit("ast-snapshot", {
2002
+ mode,
2003
+ nodes: nextAst
2004
+ });
2005
+ this.debug.emit("ast-patches", {
2006
+ patches,
2007
+ durationMs: diffDurationMs
2008
+ });
2009
+ this.debug.emit("patch-dispatched", {
2010
+ patches: patches.length,
2011
+ durationMs: patchDispatchDurationMs,
2012
+ renderDurationMs: now() - renderStarted
2013
+ });
2014
+ }
2015
+ }
2016
+ sanitizeNodesWithDebug(nodes) {
2017
+ if (this.sanitize === false) return;
2018
+ if (!this.debug.active) {
2019
+ sanitizeNodes(nodes, this.sanitize);
2020
+ return;
2021
+ }
2022
+ const inputSize = nodeMarkupSize(nodes);
2023
+ const started = now();
2024
+ sanitizeNodes(nodes, this.sanitize);
2025
+ this.debug.emit("sanitizer-completed", {
2026
+ inputSize,
2027
+ outputSize: nodeMarkupSize(nodes),
2028
+ durationMs: now() - started
2029
+ });
760
2030
  }
761
2031
  };
762
2032
  /**
@@ -770,10 +2040,10 @@ function sanitizeNodes(nodes, options) {
770
2040
  if (node.children) sanitizeNodes(node.children, options);
771
2041
  }
772
2042
  }
773
- function isReadableStream$1(source) {
2043
+ function isReadableStream$2(source) {
774
2044
  return "getReader" in source && typeof source.getReader === "function";
775
2045
  }
776
- function abortReason(signal) {
2046
+ function abortReason$1(signal) {
777
2047
  return signal?.reason ?? createAbortError("The operation was aborted");
778
2048
  }
779
2049
  function createAbortError(message) {
@@ -781,6 +2051,18 @@ function createAbortError(message) {
781
2051
  error.name = "AbortError";
782
2052
  return error;
783
2053
  }
2054
+ function now() {
2055
+ return typeof performance === "undefined" ? Date.now() : performance.now();
2056
+ }
2057
+ function nodeMarkupSize(nodes) {
2058
+ let size = 0;
2059
+ for (const node of nodes) {
2060
+ size += node.content?.length ?? 0;
2061
+ size += node.html?.length ?? 0;
2062
+ if (node.children) size += nodeMarkupSize(node.children);
2063
+ }
2064
+ return size;
2065
+ }
784
2066
 
785
2067
  //#endregion
786
2068
  //#region src/stream-router.ts
@@ -853,7 +2135,7 @@ var StreamRouter = class {
853
2135
  buffer = buffer.slice(newline + 1);
854
2136
  }
855
2137
  };
856
- if (isReadableStream(source)) {
2138
+ if (isReadableStream$1(source)) {
857
2139
  const reader = source.getReader();
858
2140
  try {
859
2141
  for (;;) {
@@ -901,7 +2183,7 @@ function tryParseJson(text) {
901
2183
  function isEnvelope(value) {
902
2184
  return typeof value === "object" && value !== null && typeof value.ch === "string";
903
2185
  }
904
- function isReadableStream(source) {
2186
+ function isReadableStream$1(source) {
905
2187
  return "getReader" in source && typeof source.getReader === "function";
906
2188
  }
907
2189
 
@@ -917,9 +2199,296 @@ function buildSystemPrompt(options = {}) {
917
2199
  if (options.base) parts.push(options.base);
918
2200
  const cardSpec = options.registry?.toPromptSpec();
919
2201
  if (cardSpec) parts.push(cardSpec);
920
- for (const p of options.plugins ?? []) if (p.promptSpec) parts.push(p.promptSpec);
2202
+ for (const plugin of options.plugins ?? []) {
2203
+ const spec = typeof plugin.promptSpec === "function" ? plugin.promptSpec() : plugin.promptSpec;
2204
+ if (spec) parts.push(spec);
2205
+ }
921
2206
  return parts.join("\n\n");
922
2207
  }
923
2208
 
924
2209
  //#endregion
925
- export { CardRegistry, Renderer, StreamRouter, applyPatches, buildSystemPrompt, collectNodeRenderers, createParser, createParserWithMetadata, diffAst, parsePartialJSON, pluginNodeTypes, repairMarkdown, sanitizeHtml };
2210
+ //#region src/model-stream.ts
2211
+ async function* parseSSE(source, options = {}) {
2212
+ let buffer = "";
2213
+ let eventName;
2214
+ let id;
2215
+ let retry;
2216
+ let data = [];
2217
+ const doneData = options.doneData === void 0 ? "[DONE]" : options.doneData;
2218
+ const dispatch = () => {
2219
+ if (data.length === 0) {
2220
+ eventName = void 0;
2221
+ retry = void 0;
2222
+ return void 0;
2223
+ }
2224
+ const raw = data.join("\n");
2225
+ data = [];
2226
+ const result = { data: raw };
2227
+ if (eventName !== void 0) result.event = eventName;
2228
+ if (id !== void 0) result.id = id;
2229
+ if (retry !== void 0) result.retry = retry;
2230
+ eventName = void 0;
2231
+ retry = void 0;
2232
+ if (doneData !== false && raw === doneData) return "done";
2233
+ if (options.parseJSON) try {
2234
+ result.data = JSON.parse(raw);
2235
+ } catch (cause) {
2236
+ const error = malformedError("Invalid SSE JSON", raw, cause);
2237
+ if (options.onMalformed?.(error, raw) === "skip") return void 0;
2238
+ throw error;
2239
+ }
2240
+ return result;
2241
+ };
2242
+ const processLine = (line) => {
2243
+ if (line === "") return dispatch();
2244
+ if (line.startsWith(":")) return void 0;
2245
+ const colon = line.indexOf(":");
2246
+ const field = colon < 0 ? line : line.slice(0, colon);
2247
+ let value = colon < 0 ? "" : line.slice(colon + 1);
2248
+ if (value.startsWith(" ")) value = value.slice(1);
2249
+ if (field === "data") data.push(value);
2250
+ else if (field === "event") eventName = value;
2251
+ else if (field === "id" && !value.includes("\0")) id = value;
2252
+ else if (field === "retry" && /^\d+$/.test(value)) retry = Number(value);
2253
+ return void 0;
2254
+ };
2255
+ const drainLines = function* (eof = false) {
2256
+ for (;;) {
2257
+ const cr = buffer.indexOf("\r");
2258
+ const lf = buffer.indexOf("\n");
2259
+ let newline = cr < 0 ? lf : lf < 0 ? cr : Math.min(cr, lf);
2260
+ if (newline < 0) break;
2261
+ if (!eof && buffer[newline] === "\r" && newline === buffer.length - 1) break;
2262
+ const width = buffer[newline] === "\r" && buffer[newline + 1] === "\n" ? 2 : 1;
2263
+ const line = buffer.slice(0, newline);
2264
+ buffer = buffer.slice(newline + width);
2265
+ const event$1 = processLine(line);
2266
+ if (event$1) yield event$1;
2267
+ }
2268
+ };
2269
+ for await (const chunk of decodedChunks(source, options.signal)) {
2270
+ buffer += chunk;
2271
+ for (const event$1 of drainLines()) {
2272
+ if (event$1 === "done") return;
2273
+ yield event$1;
2274
+ }
2275
+ }
2276
+ for (const event$1 of drainLines(true)) {
2277
+ if (event$1 === "done") return;
2278
+ yield event$1;
2279
+ }
2280
+ if (buffer) processLine(buffer);
2281
+ const event = dispatch();
2282
+ if (event && event !== "done") yield event;
2283
+ }
2284
+ async function* jsonLines(source, options = {}) {
2285
+ let buffer = "";
2286
+ for await (const chunk of decodedChunks(source, options.signal)) {
2287
+ buffer += chunk;
2288
+ for (;;) {
2289
+ const newline = buffer.indexOf("\n");
2290
+ if (newline < 0) break;
2291
+ const line = buffer.slice(0, newline).replace(/\r$/, "");
2292
+ buffer = buffer.slice(newline + 1);
2293
+ const value$1 = parseJSONLine(line, options);
2294
+ if (value$1 !== SKIP) yield value$1;
2295
+ }
2296
+ }
2297
+ const value = parseJSONLine(buffer.replace(/\r$/, ""), options);
2298
+ if (value !== SKIP) yield value;
2299
+ }
2300
+ const ndjson = jsonLines;
2301
+ async function* textLines(source, options = {}) {
2302
+ let buffer = "";
2303
+ for await (const chunk of decodedChunks(source, options.signal)) {
2304
+ buffer += chunk;
2305
+ for (;;) {
2306
+ const newline = buffer.indexOf("\n");
2307
+ if (newline < 0) break;
2308
+ yield buffer.slice(0, newline).replace(/\r$/, "");
2309
+ buffer = buffer.slice(newline + 1);
2310
+ }
2311
+ }
2312
+ if (buffer) yield buffer.replace(/\r$/, "");
2313
+ }
2314
+ async function* contentDeltas(events) {
2315
+ for await (const event of events) if (event.type === "content") yield event.delta;
2316
+ }
2317
+ async function* mockModelStream(events, options = {}) {
2318
+ for await (const event of events) {
2319
+ throwIfAborted(options.signal);
2320
+ if (options.delayMs && options.delayMs > 0) await delay(options.delayMs, options.signal);
2321
+ yield event;
2322
+ }
2323
+ }
2324
+ function readableBytes(chunks) {
2325
+ const iterator = toAsyncIterator(chunks);
2326
+ const encoder = new TextEncoder();
2327
+ return new ReadableStream({
2328
+ async pull(controller) {
2329
+ try {
2330
+ const result = await iterator.next();
2331
+ if (result.done) controller.close();
2332
+ else controller.enqueue(typeof result.value === "string" ? encoder.encode(result.value) : result.value);
2333
+ } catch (error) {
2334
+ controller.error(error);
2335
+ }
2336
+ },
2337
+ async cancel(reason) {
2338
+ await iterator.return?.(reason);
2339
+ }
2340
+ });
2341
+ }
2342
+ async function* decodedChunks(source, signal) {
2343
+ throwIfAborted(signal);
2344
+ const decoder = new TextDecoder();
2345
+ const body = isResponse(source) ? source.body : source;
2346
+ if (!body) throw new TypeError("Response body is null");
2347
+ if (isReadableStream(body)) {
2348
+ const reader = body.getReader();
2349
+ let done$1 = false;
2350
+ let primaryError$1;
2351
+ let cancelPromise;
2352
+ const cancel = (reason) => {
2353
+ if (done$1) return Promise.resolve();
2354
+ cancelPromise ??= Promise.resolve().then(() => reader.cancel(reason));
2355
+ return cancelPromise;
2356
+ };
2357
+ const abort = () => {
2358
+ cancel(abortReason(signal)).catch(() => {});
2359
+ };
2360
+ signal?.addEventListener("abort", abort, { once: true });
2361
+ try {
2362
+ for (;;) {
2363
+ throwIfAborted(signal);
2364
+ const result = await reader.read();
2365
+ throwIfAborted(signal);
2366
+ if (result.done) {
2367
+ done$1 = true;
2368
+ break;
2369
+ }
2370
+ const text = decoder.decode(result.value, { stream: true });
2371
+ if (text) yield text;
2372
+ }
2373
+ throwIfAborted(signal);
2374
+ const tail = decoder.decode();
2375
+ if (tail) yield tail;
2376
+ } catch (error) {
2377
+ primaryError$1 = error;
2378
+ throw error;
2379
+ } finally {
2380
+ signal?.removeEventListener("abort", abort);
2381
+ try {
2382
+ if (!done$1) await cancel(abortReason(signal, "Stream iteration stopped"));
2383
+ } catch (error) {
2384
+ if (primaryError$1 === void 0) throw error;
2385
+ } finally {
2386
+ reader.releaseLock();
2387
+ }
2388
+ }
2389
+ return;
2390
+ }
2391
+ const iterator = body[Symbol.asyncIterator]();
2392
+ let done = false;
2393
+ let primaryError;
2394
+ try {
2395
+ for (;;) {
2396
+ throwIfAborted(signal);
2397
+ const result = await nextWithAbort(iterator, signal);
2398
+ throwIfAborted(signal);
2399
+ if (result.done) {
2400
+ done = true;
2401
+ break;
2402
+ }
2403
+ const text = typeof result.value === "string" ? decoder.decode() + result.value : decoder.decode(result.value, { stream: true });
2404
+ if (text) yield text;
2405
+ }
2406
+ const tail = decoder.decode();
2407
+ if (tail) yield tail;
2408
+ } catch (error) {
2409
+ primaryError = error;
2410
+ throw error;
2411
+ } finally {
2412
+ if (!done) try {
2413
+ await iterator.return?.();
2414
+ } catch (error) {
2415
+ if (primaryError === void 0) throw error;
2416
+ }
2417
+ }
2418
+ }
2419
+ async function nextWithAbort(iterator, signal) {
2420
+ if (!signal) return iterator.next();
2421
+ throwIfAborted(signal);
2422
+ const next = Promise.resolve(iterator.next());
2423
+ let abort;
2424
+ const aborted = new Promise((_resolve, reject) => {
2425
+ abort = () => reject(abortReason(signal));
2426
+ signal.addEventListener("abort", abort, { once: true });
2427
+ });
2428
+ try {
2429
+ return await Promise.race([next, aborted]);
2430
+ } finally {
2431
+ signal.removeEventListener("abort", abort);
2432
+ }
2433
+ }
2434
+ const SKIP = Symbol("skip");
2435
+ function parseJSONLine(line, options) {
2436
+ if (!line.trim()) return SKIP;
2437
+ try {
2438
+ return JSON.parse(line);
2439
+ } catch (cause) {
2440
+ const error = malformedError("Invalid JSON line", line, cause);
2441
+ if (options.onMalformed?.(error, line) === "skip") return SKIP;
2442
+ throw error;
2443
+ }
2444
+ }
2445
+ function malformedError(message, input, cause) {
2446
+ const error = new SyntaxError(`${message}: ${input}`);
2447
+ error.cause = cause;
2448
+ return error;
2449
+ }
2450
+ function isResponse(value) {
2451
+ return typeof Response !== "undefined" && value instanceof Response;
2452
+ }
2453
+ function isReadableStream(value) {
2454
+ return typeof value === "object" && value !== null && "getReader" in value && typeof value.getReader === "function";
2455
+ }
2456
+ function toAsyncIterator(source) {
2457
+ if (Symbol.asyncIterator in source) return source[Symbol.asyncIterator]();
2458
+ const iterator = source[Symbol.iterator]();
2459
+ return {
2460
+ next: async () => iterator.next(),
2461
+ return: iterator.return ? async (value) => iterator.return(value) : void 0
2462
+ };
2463
+ }
2464
+ function throwIfAborted(signal) {
2465
+ if (signal?.aborted) throw abortReason(signal);
2466
+ }
2467
+ function abortReason(signal, fallback = "The operation was aborted") {
2468
+ if (signal?.reason !== void 0) return signal.reason;
2469
+ const error = new Error(fallback);
2470
+ error.name = "AbortError";
2471
+ return error;
2472
+ }
2473
+ function delay(ms, signal) {
2474
+ return new Promise((resolve, reject) => {
2475
+ if (signal?.aborted) {
2476
+ reject(abortReason(signal));
2477
+ return;
2478
+ }
2479
+ const finish = () => {
2480
+ signal?.removeEventListener("abort", abort);
2481
+ resolve();
2482
+ };
2483
+ const timer = setTimeout(finish, ms);
2484
+ const abort = () => {
2485
+ clearTimeout(timer);
2486
+ signal?.removeEventListener("abort", abort);
2487
+ reject(abortReason(signal));
2488
+ };
2489
+ signal?.addEventListener("abort", abort, { once: true });
2490
+ });
2491
+ }
2492
+
2493
+ //#endregion
2494
+ export { ActionAbortedError, ActionAlreadyRegisteredError, ActionDestroyedError, ActionExecutionError, ActionNotFoundError, ActionRegistry, ActionRuntime, ActionRuntimeError, ActionTimeoutError, ActionValidationError, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardJSONError, CardLimitError, CardNotFoundError, CardRegistry, CardRevisionConflictError, CardSnapshotError, CardStore, CardStoreError, CardTypeConflictError, CardValidationError, DebugEmitter, Renderer, StreamRouter, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };