@ai-gui/core 0.1.0 → 0.3.0

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