wu-framework 1.1.6 → 1.1.8

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.
Files changed (90) hide show
  1. package/README.md +511 -977
  2. package/dist/wu-framework.cjs.js +3 -1
  3. package/dist/wu-framework.cjs.js.map +1 -0
  4. package/dist/wu-framework.dev.js +7533 -2761
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js +3 -0
  7. package/dist/wu-framework.esm.js.map +1 -0
  8. package/dist/wu-framework.umd.js +3 -1
  9. package/dist/wu-framework.umd.js.map +1 -0
  10. package/integrations/astro/README.md +127 -0
  11. package/integrations/astro/WuApp.astro +63 -0
  12. package/integrations/astro/WuShell.astro +39 -0
  13. package/integrations/astro/index.js +68 -0
  14. package/integrations/astro/package.json +38 -0
  15. package/integrations/astro/types.d.ts +53 -0
  16. package/package.json +94 -74
  17. package/src/adapters/angular/ai.js +30 -0
  18. package/src/adapters/angular/index.d.ts +154 -0
  19. package/src/adapters/angular/index.js +932 -0
  20. package/src/adapters/angular.d.ts +3 -154
  21. package/src/adapters/angular.js +3 -813
  22. package/src/adapters/index.js +35 -24
  23. package/src/adapters/lit/ai.js +20 -0
  24. package/src/adapters/lit/index.d.ts +120 -0
  25. package/src/adapters/lit/index.js +721 -0
  26. package/src/adapters/lit.d.ts +3 -120
  27. package/src/adapters/lit.js +3 -726
  28. package/src/adapters/preact/ai.js +33 -0
  29. package/src/adapters/preact/index.d.ts +108 -0
  30. package/src/adapters/preact/index.js +661 -0
  31. package/src/adapters/preact.d.ts +3 -108
  32. package/src/adapters/preact.js +3 -665
  33. package/src/adapters/react/ai.js +135 -0
  34. package/src/adapters/react/index.d.ts +246 -0
  35. package/src/adapters/react/index.js +689 -0
  36. package/src/adapters/react.d.ts +3 -212
  37. package/src/adapters/react.js +3 -513
  38. package/src/adapters/shared.js +64 -0
  39. package/src/adapters/solid/ai.js +32 -0
  40. package/src/adapters/solid/index.d.ts +101 -0
  41. package/src/adapters/solid/index.js +586 -0
  42. package/src/adapters/solid.d.ts +3 -101
  43. package/src/adapters/solid.js +3 -591
  44. package/src/adapters/svelte/ai.js +31 -0
  45. package/src/adapters/svelte/index.d.ts +166 -0
  46. package/src/adapters/svelte/index.js +798 -0
  47. package/src/adapters/svelte.d.ts +3 -166
  48. package/src/adapters/svelte.js +3 -803
  49. package/src/adapters/vanilla/ai.js +30 -0
  50. package/src/adapters/vanilla/index.d.ts +179 -0
  51. package/src/adapters/vanilla/index.js +785 -0
  52. package/src/adapters/vanilla.d.ts +3 -179
  53. package/src/adapters/vanilla.js +3 -791
  54. package/src/adapters/vue/ai.js +52 -0
  55. package/src/adapters/vue/index.d.ts +299 -0
  56. package/src/adapters/vue/index.js +608 -0
  57. package/src/adapters/vue.d.ts +3 -299
  58. package/src/adapters/vue.js +3 -611
  59. package/src/ai/wu-ai-actions.js +261 -0
  60. package/src/ai/wu-ai-browser.js +663 -0
  61. package/src/ai/wu-ai-context.js +332 -0
  62. package/src/ai/wu-ai-conversation.js +554 -0
  63. package/src/ai/wu-ai-permissions.js +381 -0
  64. package/src/ai/wu-ai-provider.js +605 -0
  65. package/src/ai/wu-ai-schema.js +225 -0
  66. package/src/ai/wu-ai-triggers.js +396 -0
  67. package/src/ai/wu-ai.js +474 -0
  68. package/src/core/wu-app.js +50 -8
  69. package/src/core/wu-cache.js +1 -1
  70. package/src/core/wu-core.js +645 -677
  71. package/src/core/wu-html-parser.js +121 -211
  72. package/src/core/wu-iframe-sandbox.js +328 -0
  73. package/src/core/wu-mcp-bridge.js +647 -0
  74. package/src/core/wu-overrides.js +510 -0
  75. package/src/core/wu-prefetch.js +414 -0
  76. package/src/core/wu-proxy-sandbox.js +398 -75
  77. package/src/core/wu-sandbox.js +86 -268
  78. package/src/core/wu-script-executor.js +79 -182
  79. package/src/core/wu-snapshot-sandbox.js +149 -106
  80. package/src/core/wu-strategies.js +13 -0
  81. package/src/core/wu-style-bridge.js +0 -2
  82. package/src/index.js +139 -665
  83. package/dist/wu-framework.hex.js +0 -23
  84. package/dist/wu-framework.min.js +0 -1
  85. package/dist/wu-framework.obf.js +0 -1
  86. package/scripts/build-protected.js +0 -366
  87. package/scripts/build.js +0 -212
  88. package/scripts/rollup-plugin-hex.js +0 -143
  89. package/src/core/wu-registry.js +0 -60
  90. package/src/core/wu-sandbox-pool.js +0 -390
@@ -0,0 +1,554 @@
1
+ /**
2
+ * WU-AI-CONVERSATION: Multi-turn conversation manager
3
+ *
4
+ * Manages conversation state per namespace. Each namespace maintains
5
+ * its own message history, enabling multiple independent AI conversations
6
+ * (e.g., chat widget, background triggers, admin panel).
7
+ *
8
+ * Key features:
9
+ * - Multi-turn per namespace (isolated histories)
10
+ * - Streaming with async generator passthrough
11
+ * - Tool call loop (max rounds configurable, default 5)
12
+ * - Abort support via AbortController
13
+ * - Automatic context injection before each send
14
+ * - Token-aware history truncation
15
+ */
16
+
17
+ import { logger } from '../core/wu-logger.js';
18
+ import { sanitizeForPrompt, interpolate } from './wu-ai-schema.js';
19
+
20
+ // ─── Default Config ──────────────────────────────────────────────
21
+
22
+ const DEFAULT_CONFIG = {
23
+ maxHistoryMessages: 50, // per namespace
24
+ maxToolRounds: 5, // tool call loop limit
25
+ defaultNamespace: 'default',
26
+ systemPrompt: null, // string or function returning string
27
+ temperature: undefined,
28
+ maxTokens: undefined,
29
+ };
30
+
31
+ // ─── Conversation Namespace ──────────────────────────────────────
32
+
33
+ class ConversationNamespace {
34
+ constructor(name) {
35
+ this.name = name;
36
+ this.messages = [];
37
+ this.createdAt = Date.now();
38
+ this.lastActivity = Date.now();
39
+ this._abortController = null;
40
+ }
41
+
42
+ addMessage(msg) {
43
+ this.messages.push({ ...msg, _ts: Date.now() });
44
+ this.lastActivity = Date.now();
45
+ }
46
+
47
+ getMessages() {
48
+ return this.messages.map(({ _ts, ...rest }) => rest);
49
+ }
50
+
51
+ truncate(maxMessages) {
52
+ if (this.messages.length <= maxMessages) return;
53
+
54
+ // Keep system messages + last N messages
55
+ const system = this.messages.filter(m => m.role === 'system');
56
+ const nonSystem = this.messages.filter(m => m.role !== 'system');
57
+ const kept = nonSystem.slice(-maxMessages);
58
+ this.messages = [...system, ...kept];
59
+ }
60
+
61
+ clear() {
62
+ this.messages = [];
63
+ this.lastActivity = Date.now();
64
+ }
65
+
66
+ abort() {
67
+ if (this._abortController) {
68
+ this._abortController.abort();
69
+ this._abortController = null;
70
+ }
71
+ }
72
+
73
+ createAbortController() {
74
+ this.abort(); // cancel previous if any
75
+ this._abortController = new AbortController();
76
+ return this._abortController;
77
+ }
78
+ }
79
+
80
+ // ─── Main Conversation Manager ───────────────────────────────────
81
+
82
+ export class WuAIConversation {
83
+ constructor({ provider, actions, context, permissions, eventBus }) {
84
+ this._provider = provider;
85
+ this._actions = actions;
86
+ this._context = context;
87
+ this._permissions = permissions;
88
+ this._eventBus = eventBus;
89
+
90
+ this._config = { ...DEFAULT_CONFIG };
91
+ this._namespaces = new Map();
92
+ this._activeRequests = new Map(); // namespace → promise
93
+ }
94
+
95
+ /**
96
+ * Configure conversation defaults.
97
+ */
98
+ configure(config) {
99
+ Object.assign(this._config, config);
100
+ }
101
+
102
+ /**
103
+ * Send a message and get a complete response.
104
+ * Handles multi-turn tool call loops automatically.
105
+ *
106
+ * @param {string} message - User message
107
+ * @param {object} [options]
108
+ * @param {string} [options.namespace='default'] - Conversation namespace
109
+ * @param {string} [options.systemPrompt] - Override system prompt
110
+ * @param {object} [options.templateVars] - Variables for template interpolation
111
+ * @param {number} [options.temperature] - Override temperature
112
+ * @param {number} [options.maxTokens] - Override max tokens
113
+ * @param {AbortSignal} [options.signal] - External abort signal
114
+ * @returns {Promise<{ content: string, tool_results?: Array, usage?: object, namespace: string }>}
115
+ */
116
+ async send(message, options = {}) {
117
+ const ns = this._getOrCreateNamespace(options.namespace);
118
+ const traceId = this._permissions.loopProtection.createTraceId();
119
+ const meta = { namespace: ns.name, depth: 0, traceId };
120
+
121
+ // Preflight checks
122
+ const preflight = this._permissions.preflight(meta);
123
+ if (!preflight.allowed) {
124
+ return { content: `[blocked] ${preflight.reason}`, namespace: ns.name };
125
+ }
126
+
127
+ // Build system prompt
128
+ const systemPrompt = await this._buildSystemPrompt(options);
129
+
130
+ // Ensure system message is set/updated
131
+ this._setSystemMessage(ns, systemPrompt);
132
+
133
+ // Add user message
134
+ const processedMessage = this._processMessage(message, options.templateVars);
135
+ ns.addMessage({ role: 'user', content: processedMessage });
136
+
137
+ // Truncate history if needed
138
+ ns.truncate(this._config.maxHistoryMessages);
139
+
140
+ // Create abort controller (merges with external signal)
141
+ const controller = ns.createAbortController();
142
+ const signal = this._mergeSignals(controller.signal, options.signal);
143
+
144
+ // Rate limit tracking
145
+ this._permissions.rateLimiter.recordStart(ns.name);
146
+
147
+ try {
148
+ // Tool call loop
149
+ const toolResults = [];
150
+ let rounds = 0;
151
+ const maxRounds = this._config.maxToolRounds;
152
+
153
+ while (rounds <= maxRounds) {
154
+ // Get tools if actions are registered
155
+ const tools = this._actions.getToolSchemas();
156
+
157
+ const response = await this._provider.send(ns.getMessages(), {
158
+ tools: tools.length > 0 ? tools : undefined,
159
+ temperature: options.temperature ?? this._config.temperature,
160
+ maxTokens: options.maxTokens ?? this._config.maxTokens,
161
+ signal,
162
+ });
163
+
164
+ // Add assistant response to history
165
+ const assistantMsg = { role: 'assistant', content: response.content || '' };
166
+ if (response.tool_calls) assistantMsg.tool_calls = response.tool_calls;
167
+ ns.addMessage(assistantMsg);
168
+
169
+ // No tool calls → we're done
170
+ if (!response.tool_calls || response.tool_calls.length === 0) {
171
+ this._permissions.circuitBreaker.recordSuccess();
172
+
173
+ this._eventBus.emit('ai:response', {
174
+ namespace: ns.name,
175
+ content: response.content,
176
+ toolResults: toolResults.length > 0 ? toolResults : undefined,
177
+ usage: response.usage,
178
+ traceId,
179
+ }, { appName: 'wu-ai' });
180
+
181
+ return {
182
+ content: response.content || '',
183
+ tool_results: toolResults.length > 0 ? toolResults : undefined,
184
+ usage: response.usage,
185
+ namespace: ns.name,
186
+ };
187
+ }
188
+
189
+ // Execute tool calls
190
+ rounds++;
191
+ if (rounds > maxRounds) {
192
+ const msg = `[wu-ai] Tool call loop limit (${maxRounds}) reached in namespace '${ns.name}'`;
193
+ logger.wuWarn(msg);
194
+ ns.addMessage({ role: 'assistant', content: msg });
195
+ return { content: msg, tool_results: toolResults, namespace: ns.name };
196
+ }
197
+
198
+ // Loop protection
199
+ this._permissions.loopProtection.enter(traceId);
200
+
201
+ for (const toolCall of response.tool_calls) {
202
+ const result = await this._actions.execute(toolCall.name, toolCall.arguments, {
203
+ traceId,
204
+ depth: rounds,
205
+ callId: toolCall.id,
206
+ });
207
+
208
+ toolResults.push({
209
+ tool: toolCall.name,
210
+ params: toolCall.arguments,
211
+ result: result.result,
212
+ success: result.success,
213
+ });
214
+
215
+ // Add tool result to conversation
216
+ ns.addMessage({
217
+ role: 'tool',
218
+ content: JSON.stringify(result.success ? result.result : { error: result.reason }),
219
+ tool_call_id: toolCall.id,
220
+ });
221
+ }
222
+
223
+ this._permissions.loopProtection.exit(traceId);
224
+ }
225
+
226
+ // Should not reach here, but safety net
227
+ return { content: '', tool_results: toolResults, namespace: ns.name };
228
+
229
+ } catch (err) {
230
+ this._permissions.circuitBreaker.recordFailure();
231
+
232
+ if (err.name === 'AbortError') {
233
+ return { content: '[aborted]', namespace: ns.name };
234
+ }
235
+
236
+ this._eventBus.emit('ai:error', {
237
+ namespace: ns.name,
238
+ error: err.message,
239
+ traceId,
240
+ }, { appName: 'wu-ai' });
241
+
242
+ throw err;
243
+ } finally {
244
+ this._permissions.rateLimiter.recordEnd();
245
+ ns._abortController = null;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Send a message with streaming response.
251
+ * Returns an async generator that yields chunks.
252
+ *
253
+ * @param {string} message - User message
254
+ * @param {object} [options] - Same as send()
255
+ * @yields {{ type: 'text'|'tool_call'|'done'|'error', content?: string, tool_call?: object }}
256
+ */
257
+ async *stream(message, options = {}) {
258
+ const ns = this._getOrCreateNamespace(options.namespace);
259
+ const traceId = this._permissions.loopProtection.createTraceId();
260
+ const meta = { namespace: ns.name, depth: 0, traceId };
261
+
262
+ // Preflight checks
263
+ const preflight = this._permissions.preflight(meta);
264
+ if (!preflight.allowed) {
265
+ yield { type: 'error', error: preflight.reason };
266
+ return;
267
+ }
268
+
269
+ // Build system prompt and set it
270
+ const systemPrompt = await this._buildSystemPrompt(options);
271
+ this._setSystemMessage(ns, systemPrompt);
272
+
273
+ // Add user message
274
+ const processedMessage = this._processMessage(message, options.templateVars);
275
+ ns.addMessage({ role: 'user', content: processedMessage });
276
+ ns.truncate(this._config.maxHistoryMessages);
277
+
278
+ // Abort controller
279
+ const controller = ns.createAbortController();
280
+ const signal = this._mergeSignals(controller.signal, options.signal);
281
+
282
+ this._permissions.rateLimiter.recordStart(ns.name);
283
+
284
+ try {
285
+ const tools = this._actions.getToolSchemas();
286
+ let fullContent = '';
287
+
288
+ // Accumulator for streaming tool call deltas
289
+ const toolCallAccumulator = new Map(); // index → { id, name, args }
290
+
291
+ for await (const chunk of this._provider.stream(ns.getMessages(), {
292
+ tools: tools.length > 0 ? tools : undefined,
293
+ temperature: options.temperature ?? this._config.temperature,
294
+ maxTokens: options.maxTokens ?? this._config.maxTokens,
295
+ signal,
296
+ })) {
297
+ if (chunk.type === 'text') {
298
+ fullContent += chunk.content;
299
+ yield chunk;
300
+ } else if (chunk.type === 'tool_call_start') {
301
+ // Start accumulating a new tool call
302
+ toolCallAccumulator.set(toolCallAccumulator.size, {
303
+ id: chunk.id,
304
+ name: chunk.name,
305
+ args: '',
306
+ });
307
+ } else if (chunk.type === 'tool_call_delta') {
308
+ // Accumulate arguments delta
309
+ const idx = chunk.index ?? (toolCallAccumulator.size - 1);
310
+ const acc = toolCallAccumulator.get(idx);
311
+ if (acc) {
312
+ if (chunk.id) acc.id = chunk.id;
313
+ if (chunk.name) acc.name = chunk.name;
314
+ acc.args += chunk.argumentsDelta || '';
315
+ } else {
316
+ // New tool call via OpenAI format
317
+ toolCallAccumulator.set(idx, {
318
+ id: chunk.id || `tc_${idx}`,
319
+ name: chunk.name || '',
320
+ args: chunk.argumentsDelta || '',
321
+ });
322
+ }
323
+ } else if (chunk.type === 'done') {
324
+ // If we accumulated tool calls, execute them
325
+ if (toolCallAccumulator.size > 0) {
326
+ const toolCalls = [];
327
+ for (const [, acc] of toolCallAccumulator) {
328
+ let parsedArgs = {};
329
+ try { parsedArgs = JSON.parse(acc.args); } catch { /* empty args */ }
330
+ toolCalls.push({ id: acc.id, name: acc.name, arguments: parsedArgs });
331
+ }
332
+
333
+ // Add assistant message with tool calls
334
+ ns.addMessage({ role: 'assistant', content: fullContent, tool_calls: toolCalls });
335
+
336
+ // Execute each tool call
337
+ this._permissions.loopProtection.enter(traceId);
338
+ for (const tc of toolCalls) {
339
+ const result = await this._actions.execute(tc.name, tc.arguments, {
340
+ traceId, depth: 1, callId: tc.id,
341
+ });
342
+
343
+ yield {
344
+ type: 'tool_result',
345
+ tool: tc.name,
346
+ result: result.success ? result.result : { error: result.reason },
347
+ success: result.success,
348
+ };
349
+
350
+ ns.addMessage({
351
+ role: 'tool',
352
+ content: JSON.stringify(result.success ? result.result : { error: result.reason }),
353
+ tool_call_id: tc.id,
354
+ });
355
+ }
356
+ this._permissions.loopProtection.exit(traceId);
357
+
358
+ // After tool execution in streaming, yield done — caller can re-stream if needed
359
+ yield { type: 'tool_calls_done', count: toolCalls.length };
360
+ } else {
361
+ // No tool calls — add assistant message and finish
362
+ ns.addMessage({ role: 'assistant', content: fullContent });
363
+ }
364
+
365
+ this._permissions.circuitBreaker.recordSuccess();
366
+ yield { type: 'done' };
367
+ return;
368
+ } else if (chunk.type === 'usage') {
369
+ yield chunk;
370
+ } else if (chunk.type === 'error') {
371
+ yield chunk;
372
+ }
373
+ }
374
+
375
+ // Stream ended without explicit 'done' chunk
376
+ if (fullContent) {
377
+ ns.addMessage({ role: 'assistant', content: fullContent });
378
+ }
379
+ this._permissions.circuitBreaker.recordSuccess();
380
+ yield { type: 'done' };
381
+
382
+ } catch (err) {
383
+ this._permissions.circuitBreaker.recordFailure();
384
+
385
+ if (err.name === 'AbortError') {
386
+ yield { type: 'error', error: 'aborted' };
387
+ return;
388
+ }
389
+
390
+ yield { type: 'error', error: err.message };
391
+
392
+ this._eventBus.emit('ai:error', {
393
+ namespace: ns.name,
394
+ error: err.message,
395
+ traceId,
396
+ }, { appName: 'wu-ai' });
397
+ } finally {
398
+ this._permissions.rateLimiter.recordEnd();
399
+ ns._abortController = null;
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Add a message to a namespace without sending to LLM.
405
+ * Useful for injecting context or tool results manually.
406
+ */
407
+ inject(role, content, options = {}) {
408
+ const ns = this._getOrCreateNamespace(options.namespace);
409
+ ns.addMessage({ role, content });
410
+ }
411
+
412
+ /**
413
+ * Get conversation history for a namespace.
414
+ */
415
+ getHistory(namespace) {
416
+ const ns = this._namespaces.get(namespace || this._config.defaultNamespace);
417
+ return ns ? ns.getMessages() : [];
418
+ }
419
+
420
+ /**
421
+ * Clear conversation history for a namespace.
422
+ */
423
+ clear(namespace) {
424
+ const ns = this._namespaces.get(namespace || this._config.defaultNamespace);
425
+ if (ns) ns.clear();
426
+ }
427
+
428
+ /**
429
+ * Clear all namespaces.
430
+ */
431
+ clearAll() {
432
+ for (const ns of this._namespaces.values()) {
433
+ ns.clear();
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Abort an active request in a namespace.
439
+ */
440
+ abort(namespace) {
441
+ const ns = this._namespaces.get(namespace || this._config.defaultNamespace);
442
+ if (ns) ns.abort();
443
+ }
444
+
445
+ /**
446
+ * Abort all active requests.
447
+ */
448
+ abortAll() {
449
+ for (const ns of this._namespaces.values()) {
450
+ ns.abort();
451
+ }
452
+ }
453
+
454
+ /**
455
+ * List active namespaces.
456
+ */
457
+ getNamespaces() {
458
+ return [...this._namespaces.keys()];
459
+ }
460
+
461
+ /**
462
+ * Delete a namespace entirely.
463
+ */
464
+ deleteNamespace(namespace) {
465
+ const ns = this._namespaces.get(namespace);
466
+ if (ns) {
467
+ ns.abort();
468
+ this._namespaces.delete(namespace);
469
+ }
470
+ }
471
+
472
+ // ── Private ──
473
+
474
+ _getOrCreateNamespace(name) {
475
+ const nsName = name || this._config.defaultNamespace;
476
+ if (!this._namespaces.has(nsName)) {
477
+ this._namespaces.set(nsName, new ConversationNamespace(nsName));
478
+ }
479
+ return this._namespaces.get(nsName);
480
+ }
481
+
482
+ async _buildSystemPrompt(options) {
483
+ // 1. Explicit override
484
+ if (options.systemPrompt) {
485
+ return typeof options.systemPrompt === 'function'
486
+ ? await options.systemPrompt()
487
+ : options.systemPrompt;
488
+ }
489
+
490
+ // 2. Config-level system prompt
491
+ if (this._config.systemPrompt) {
492
+ return typeof this._config.systemPrompt === 'function'
493
+ ? await this._config.systemPrompt()
494
+ : this._config.systemPrompt;
495
+ }
496
+
497
+ // 3. Auto-generate from context
498
+ if (this._context) {
499
+ await this._context.collect();
500
+ const tools = this._actions.getToolSchemas();
501
+ return this._context.toSystemPrompt({ tools });
502
+ }
503
+
504
+ return 'You are an AI assistant connected to a web application via Wu Framework.';
505
+ }
506
+
507
+ _setSystemMessage(ns, systemPrompt) {
508
+ // Replace or insert system message at the beginning
509
+ if (ns.messages.length > 0 && ns.messages[0].role === 'system') {
510
+ ns.messages[0].content = systemPrompt;
511
+ ns.messages[0]._ts = Date.now();
512
+ } else {
513
+ ns.messages.unshift({ role: 'system', content: systemPrompt, _ts: Date.now() });
514
+ }
515
+ }
516
+
517
+ _processMessage(message, templateVars) {
518
+ if (!templateVars) return message;
519
+ try {
520
+ const contextVars = this._context?.getInterpolationContext() || {};
521
+ return interpolate(message, { ...contextVars, ...templateVars });
522
+ } catch {
523
+ return message;
524
+ }
525
+ }
526
+
527
+ _mergeSignals(internalSignal, externalSignal) {
528
+ if (!externalSignal) return internalSignal;
529
+
530
+ // If AbortSignal.any is available (modern browsers), use it
531
+ if (typeof AbortSignal !== 'undefined' && AbortSignal.any) {
532
+ return AbortSignal.any([internalSignal, externalSignal]);
533
+ }
534
+
535
+ // Fallback: create a new controller that listens to both
536
+ const merged = new AbortController();
537
+ const onAbort = () => merged.abort();
538
+ internalSignal.addEventListener('abort', onAbort, { once: true });
539
+ externalSignal.addEventListener('abort', onAbort, { once: true });
540
+ return merged.signal;
541
+ }
542
+
543
+ getStats() {
544
+ const namespaces = {};
545
+ for (const [name, ns] of this._namespaces) {
546
+ namespaces[name] = {
547
+ messageCount: ns.messages.length,
548
+ lastActivity: ns.lastActivity,
549
+ hasActiveRequest: !!ns._abortController,
550
+ };
551
+ }
552
+ return { namespaces, config: { ...this._config } };
553
+ }
554
+ }