ai-functions 0.4.0 → 2.0.2

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.
@@ -0,0 +1,731 @@
1
+ /**
2
+ * RPC Transport Layer
3
+ *
4
+ * Unified transport abstraction supporting:
5
+ * - HTTP batch requests
6
+ * - WebSocket persistent connections
7
+ * - postMessage for iframe/worker communication
8
+ * - Bidirectional RPC callbacks
9
+ * - Async iterators for streaming
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ // =============================================================================
14
+ // Callback Registry
15
+ // =============================================================================
16
+ /**
17
+ * Generate a cryptographically random ID
18
+ */
19
+ function generateSecureId() {
20
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
21
+ return crypto.randomUUID();
22
+ }
23
+ // Fallback for environments without crypto.randomUUID
24
+ const array = new Uint8Array(16);
25
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
26
+ crypto.getRandomValues(array);
27
+ }
28
+ else {
29
+ // Last resort fallback (less secure)
30
+ for (let i = 0; i < 16; i++) {
31
+ array[i] = Math.floor(Math.random() * 256);
32
+ }
33
+ }
34
+ return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
35
+ }
36
+ /**
37
+ * Registry for tracking callbacks that can be invoked remotely
38
+ *
39
+ * Security features:
40
+ * - Cryptographically random callback IDs
41
+ * - Automatic expiration (TTL)
42
+ * - Invocation limits
43
+ */
44
+ export class CallbackRegistry {
45
+ callbacks = new Map();
46
+ cleanupInterval = null;
47
+ defaultTtl;
48
+ constructor(options = {}) {
49
+ this.defaultTtl = options.defaultTtl ?? 300000; // 5 minutes default
50
+ // Periodic cleanup of expired callbacks
51
+ this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
52
+ }
53
+ /**
54
+ * Register a callback and get its ID
55
+ */
56
+ register(fn, options = {}) {
57
+ const id = `cb_${generateSecureId()}`;
58
+ const ttl = options.ttl ?? this.defaultTtl;
59
+ this.callbacks.set(id, {
60
+ fn,
61
+ invocations: 0,
62
+ maxInvocations: options.maxInvocations ?? Infinity,
63
+ expiresAt: Date.now() + ttl,
64
+ });
65
+ return id;
66
+ }
67
+ /**
68
+ * Invoke a callback by ID
69
+ */
70
+ async invoke(id, args) {
71
+ const entry = this.callbacks.get(id);
72
+ if (!entry) {
73
+ throw new Error(`Callback not found: ${id}`);
74
+ }
75
+ // Check expiration
76
+ if (Date.now() > entry.expiresAt) {
77
+ this.callbacks.delete(id);
78
+ throw new Error(`Callback expired: ${id}`);
79
+ }
80
+ // Check invocation limit
81
+ if (entry.invocations >= entry.maxInvocations) {
82
+ this.callbacks.delete(id);
83
+ throw new Error(`Callback invocation limit reached: ${id}`);
84
+ }
85
+ entry.invocations++;
86
+ // Auto-cleanup if max invocations reached
87
+ if (entry.invocations >= entry.maxInvocations) {
88
+ this.callbacks.delete(id);
89
+ }
90
+ return entry.fn(...args);
91
+ }
92
+ /**
93
+ * Remove a callback
94
+ */
95
+ unregister(id) {
96
+ return this.callbacks.delete(id);
97
+ }
98
+ /**
99
+ * Remove expired callbacks
100
+ */
101
+ cleanup() {
102
+ const now = Date.now();
103
+ for (const [id, entry] of this.callbacks) {
104
+ if (now > entry.expiresAt) {
105
+ this.callbacks.delete(id);
106
+ }
107
+ }
108
+ }
109
+ /**
110
+ * Stop the cleanup interval
111
+ */
112
+ destroy() {
113
+ if (this.cleanupInterval) {
114
+ clearInterval(this.cleanupInterval);
115
+ this.cleanupInterval = null;
116
+ }
117
+ this.callbacks.clear();
118
+ }
119
+ /**
120
+ * Check if a value contains callbacks and serialize them
121
+ */
122
+ serializeWithCallbacks(value, options = {}) {
123
+ const callbacks = new Map();
124
+ const serialize = (v, path) => {
125
+ if (typeof v === 'function') {
126
+ const id = this.register(v, options);
127
+ callbacks.set(path, id);
128
+ return { __callback__: id };
129
+ }
130
+ if (Array.isArray(v)) {
131
+ return v.map((item, i) => serialize(item, `${path}[${i}]`));
132
+ }
133
+ if (v && typeof v === 'object') {
134
+ const result = {};
135
+ for (const [key, val] of Object.entries(v)) {
136
+ result[key] = serialize(val, `${path}.${key}`);
137
+ }
138
+ return result;
139
+ }
140
+ return v;
141
+ };
142
+ return {
143
+ value: serialize(value, ''),
144
+ callbacks,
145
+ };
146
+ }
147
+ }
148
+ /**
149
+ * HTTP batch transport - groups multiple calls into single requests
150
+ */
151
+ export class HTTPTransport {
152
+ url;
153
+ headers;
154
+ timeout;
155
+ batchDelay;
156
+ maxBatchSize;
157
+ pendingRequests = [];
158
+ flushScheduled = false;
159
+ subscribers = new Set();
160
+ callbackRegistry = new CallbackRegistry();
161
+ state = 'connected';
162
+ constructor(options) {
163
+ this.url = options.url;
164
+ this.headers = options.headers ?? {};
165
+ this.timeout = options.timeout ?? 30000;
166
+ this.batchDelay = options.batchDelay ?? 0;
167
+ this.maxBatchSize = options.maxBatchSize ?? 100;
168
+ }
169
+ send(message) {
170
+ this.request(message).catch(() => {
171
+ // Fire and forget
172
+ });
173
+ }
174
+ async request(message) {
175
+ return new Promise((resolve, reject) => {
176
+ // Serialize any callbacks in params
177
+ if (message.params) {
178
+ const { value } = this.callbackRegistry.serializeWithCallbacks(message.params);
179
+ message = { ...message, params: value };
180
+ }
181
+ this.pendingRequests.push({ message, resolve, reject });
182
+ this.scheduleFlush();
183
+ });
184
+ }
185
+ scheduleFlush() {
186
+ if (this.flushScheduled)
187
+ return;
188
+ this.flushScheduled = true;
189
+ if (this.batchDelay === 0) {
190
+ queueMicrotask(() => this.flush());
191
+ }
192
+ else {
193
+ setTimeout(() => this.flush(), this.batchDelay);
194
+ }
195
+ }
196
+ async flush() {
197
+ this.flushScheduled = false;
198
+ if (this.pendingRequests.length === 0)
199
+ return;
200
+ const batch = this.pendingRequests.splice(0, this.maxBatchSize);
201
+ // Schedule next flush if more pending
202
+ if (this.pendingRequests.length > 0) {
203
+ this.scheduleFlush();
204
+ }
205
+ try {
206
+ const controller = new AbortController();
207
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
208
+ const response = await fetch(this.url, {
209
+ method: 'POST',
210
+ headers: {
211
+ 'Content-Type': 'application/json',
212
+ ...this.headers,
213
+ },
214
+ body: JSON.stringify(batch.map(p => p.message)),
215
+ signal: controller.signal,
216
+ });
217
+ clearTimeout(timeoutId);
218
+ if (!response.ok) {
219
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
220
+ }
221
+ const results = (await response.json());
222
+ const resultMap = new Map(results.map(r => [r.id, r]));
223
+ for (const pending of batch) {
224
+ const result = resultMap.get(pending.message.id);
225
+ if (!result) {
226
+ pending.reject(new Error(`No response for message ${pending.message.id}`));
227
+ }
228
+ else {
229
+ // Return both success and error responses - let caller handle errors
230
+ pending.resolve(result);
231
+ }
232
+ }
233
+ }
234
+ catch (error) {
235
+ for (const pending of batch) {
236
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
237
+ }
238
+ }
239
+ }
240
+ subscribe(handler) {
241
+ this.subscribers.add(handler);
242
+ return () => this.subscribers.delete(handler);
243
+ }
244
+ close() {
245
+ this.state = 'disconnected';
246
+ // Reject any pending requests
247
+ for (const pending of this.pendingRequests) {
248
+ pending.reject(new Error('Transport closed'));
249
+ }
250
+ this.pendingRequests = [];
251
+ }
252
+ }
253
+ /**
254
+ * WebSocket transport - persistent connection with bidirectional communication
255
+ */
256
+ export class WebSocketTransport {
257
+ url;
258
+ ws = null;
259
+ reconnect;
260
+ reconnectDelay;
261
+ maxReconnectAttempts;
262
+ pingInterval;
263
+ reconnectAttempts = 0;
264
+ pingTimer = null;
265
+ pendingRequests = new Map();
266
+ streamHandlers = new Map();
267
+ subscribers = new Set();
268
+ callbackRegistry = new CallbackRegistry();
269
+ state = 'disconnected';
270
+ constructor(options) {
271
+ this.url = options.url;
272
+ this.reconnect = options.reconnect ?? true;
273
+ this.reconnectDelay = options.reconnectDelay ?? 1000;
274
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
275
+ this.pingInterval = options.pingInterval ?? 30000;
276
+ }
277
+ /**
278
+ * Connect to the WebSocket server
279
+ */
280
+ async connect() {
281
+ if (this.state === 'connected')
282
+ return;
283
+ this.state = 'connecting';
284
+ return new Promise((resolve, reject) => {
285
+ this.ws = new WebSocket(this.url);
286
+ this.ws.onopen = () => {
287
+ this.state = 'connected';
288
+ this.reconnectAttempts = 0;
289
+ this.startPing();
290
+ resolve();
291
+ };
292
+ this.ws.onerror = (event) => {
293
+ if (this.state === 'connecting') {
294
+ reject(new Error('WebSocket connection failed'));
295
+ }
296
+ };
297
+ this.ws.onclose = () => {
298
+ this.state = 'disconnected';
299
+ this.stopPing();
300
+ this.handleDisconnect();
301
+ };
302
+ this.ws.onmessage = (event) => {
303
+ try {
304
+ const message = JSON.parse(event.data);
305
+ this.handleMessage(message);
306
+ }
307
+ catch (error) {
308
+ console.error('Invalid WebSocket message:', error);
309
+ }
310
+ };
311
+ });
312
+ }
313
+ handleMessage(message) {
314
+ // Handle pending request responses
315
+ const pending = this.pendingRequests.get(message.id);
316
+ if (pending) {
317
+ this.pendingRequests.delete(message.id);
318
+ // Return both success and error responses - let caller handle errors
319
+ pending.resolve(message);
320
+ return;
321
+ }
322
+ // Handle stream messages
323
+ if (message.type === 'stream' || message.type === 'stream-end') {
324
+ const handler = this.streamHandlers.get(message.id);
325
+ if (handler) {
326
+ handler(message.chunk, message.type === 'stream-end');
327
+ if (message.type === 'stream-end') {
328
+ this.streamHandlers.delete(message.id);
329
+ }
330
+ }
331
+ return;
332
+ }
333
+ // Handle callback invocations
334
+ if (message.type === 'callback' && message.callbackId) {
335
+ this.handleCallback(message);
336
+ return;
337
+ }
338
+ // Notify subscribers
339
+ for (const sub of this.subscribers) {
340
+ sub(message);
341
+ }
342
+ }
343
+ async handleCallback(message) {
344
+ try {
345
+ const result = await this.callbackRegistry.invoke(message.callbackId, message.params ?? []);
346
+ this.send({
347
+ id: message.id,
348
+ type: 'result',
349
+ result,
350
+ });
351
+ }
352
+ catch (error) {
353
+ this.send({
354
+ id: message.id,
355
+ type: 'error',
356
+ error: {
357
+ message: error instanceof Error ? error.message : String(error),
358
+ },
359
+ });
360
+ }
361
+ }
362
+ handleDisconnect() {
363
+ // Reject all pending requests
364
+ for (const [id, pending] of this.pendingRequests) {
365
+ pending.reject(new Error('WebSocket disconnected'));
366
+ }
367
+ this.pendingRequests.clear();
368
+ // End all streams
369
+ for (const [id, handler] of this.streamHandlers) {
370
+ handler(undefined, true);
371
+ }
372
+ this.streamHandlers.clear();
373
+ // Attempt reconnect
374
+ if (this.reconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
375
+ this.reconnectAttempts++;
376
+ setTimeout(() => this.connect(), this.reconnectDelay * this.reconnectAttempts);
377
+ }
378
+ }
379
+ startPing() {
380
+ this.pingTimer = setInterval(() => {
381
+ if (this.ws?.readyState === WebSocket.OPEN) {
382
+ this.ws.send(JSON.stringify({ type: 'ping' }));
383
+ }
384
+ }, this.pingInterval);
385
+ }
386
+ stopPing() {
387
+ if (this.pingTimer) {
388
+ clearInterval(this.pingTimer);
389
+ this.pingTimer = null;
390
+ }
391
+ }
392
+ send(message) {
393
+ if (this.ws?.readyState !== WebSocket.OPEN) {
394
+ throw new Error('WebSocket not connected');
395
+ }
396
+ // Serialize callbacks
397
+ if (message.params) {
398
+ const { value } = this.callbackRegistry.serializeWithCallbacks(message.params);
399
+ message = { ...message, params: value };
400
+ }
401
+ this.ws.send(JSON.stringify(message));
402
+ }
403
+ async request(message) {
404
+ if (this.state !== 'connected') {
405
+ await this.connect();
406
+ }
407
+ return new Promise((resolve, reject) => {
408
+ this.pendingRequests.set(message.id, { resolve, reject });
409
+ this.send(message);
410
+ });
411
+ }
412
+ /**
413
+ * Create an async iterator for streaming results
414
+ */
415
+ async *stream(message) {
416
+ if (this.state !== 'connected') {
417
+ await this.connect();
418
+ }
419
+ const chunks = [];
420
+ let done = false;
421
+ let error = null;
422
+ let resolve = null;
423
+ this.streamHandlers.set(message.id, (chunk, isDone) => {
424
+ if (isDone) {
425
+ done = true;
426
+ }
427
+ else if (chunk !== undefined) {
428
+ chunks.push(chunk);
429
+ }
430
+ resolve?.();
431
+ });
432
+ this.send(message);
433
+ try {
434
+ while (!done) {
435
+ if (chunks.length > 0) {
436
+ yield chunks.shift();
437
+ }
438
+ else {
439
+ await new Promise(r => { resolve = r; });
440
+ }
441
+ }
442
+ // Yield any remaining chunks
443
+ while (chunks.length > 0) {
444
+ yield chunks.shift();
445
+ }
446
+ }
447
+ finally {
448
+ this.streamHandlers.delete(message.id);
449
+ }
450
+ }
451
+ subscribe(handler) {
452
+ this.subscribers.add(handler);
453
+ return () => this.subscribers.delete(handler);
454
+ }
455
+ close() {
456
+ this.reconnect = false;
457
+ this.stopPing();
458
+ this.ws?.close();
459
+ this.state = 'disconnected';
460
+ }
461
+ }
462
+ // =============================================================================
463
+ // postMessage Transport
464
+ // =============================================================================
465
+ /** Valid RPC message types */
466
+ const VALID_MESSAGE_TYPES = new Set([
467
+ 'call', 'result', 'error', 'callback', 'stream', 'stream-end', 'cancel', 'ping', 'pong'
468
+ ]);
469
+ /**
470
+ * Validate that a message conforms to RPCMessage schema
471
+ */
472
+ function isValidRPCMessage(data) {
473
+ if (!data || typeof data !== 'object')
474
+ return false;
475
+ const msg = data;
476
+ // Required: id must be a string
477
+ if (typeof msg.id !== 'string' || msg.id.length === 0)
478
+ return false;
479
+ // Required: type must be valid
480
+ if (typeof msg.type !== 'string' || !VALID_MESSAGE_TYPES.has(msg.type))
481
+ return false;
482
+ // Optional field validation
483
+ if (msg.method !== undefined && typeof msg.method !== 'string')
484
+ return false;
485
+ if (msg.params !== undefined && !Array.isArray(msg.params))
486
+ return false;
487
+ if (msg.callbackId !== undefined && typeof msg.callbackId !== 'string')
488
+ return false;
489
+ return true;
490
+ }
491
+ /**
492
+ * postMessage transport - for iframe/worker communication
493
+ *
494
+ * Security features:
495
+ * - Mandatory origin validation for Window targets
496
+ * - Message schema validation
497
+ * - Request timeouts
498
+ * - Max pending requests limit
499
+ * - Optional HMAC message signing
500
+ */
501
+ export class PostMessageTransport {
502
+ target;
503
+ targetOrigin;
504
+ sourceOrigin;
505
+ isWindowTarget;
506
+ timeout;
507
+ maxPendingRequests;
508
+ secret;
509
+ pendingRequests = new Map();
510
+ subscribers = new Set();
511
+ callbackRegistry = new CallbackRegistry();
512
+ messageHandler;
513
+ state = 'connected';
514
+ constructor(options) {
515
+ this.target = options.target;
516
+ this.timeout = options.timeout ?? 30000;
517
+ this.maxPendingRequests = options.maxPendingRequests ?? 1000;
518
+ this.secret = options.secret;
519
+ // Determine if target is a Window
520
+ this.isWindowTarget = typeof Window !== 'undefined' && this.target instanceof Window;
521
+ // Security: For Window targets, require explicit origins
522
+ if (this.isWindowTarget) {
523
+ if (!options.targetOrigin || options.targetOrigin === '*') {
524
+ if (!options.allowUnsafeOrigin) {
525
+ throw new Error('PostMessageTransport: targetOrigin is required for Window targets. ' +
526
+ 'Using "*" is insecure. Set allowUnsafeOrigin: true if you understand the risks.');
527
+ }
528
+ }
529
+ if (!options.sourceOrigin) {
530
+ if (!options.allowUnsafeOrigin) {
531
+ throw new Error('PostMessageTransport: sourceOrigin is required for Window targets. ' +
532
+ 'Without origin validation, any origin can send messages. ' +
533
+ 'Set allowUnsafeOrigin: true if you understand the risks.');
534
+ }
535
+ }
536
+ }
537
+ this.targetOrigin = options.targetOrigin ?? '*';
538
+ this.sourceOrigin = options.sourceOrigin;
539
+ this.messageHandler = this.handleMessage.bind(this);
540
+ // Subscribe to messages
541
+ if ('addEventListener' in this.target) {
542
+ this.target.addEventListener('message', this.messageHandler);
543
+ }
544
+ }
545
+ handleMessage(event) {
546
+ // Validate origin for Window targets
547
+ if (this.isWindowTarget && this.sourceOrigin && event.origin !== this.sourceOrigin) {
548
+ console.warn(`PostMessageTransport: Rejected message from untrusted origin: ${event.origin}`);
549
+ return;
550
+ }
551
+ // Validate message schema
552
+ if (!isValidRPCMessage(event.data)) {
553
+ // Silently ignore invalid messages (could be from other sources)
554
+ return;
555
+ }
556
+ const message = event.data;
557
+ // Verify HMAC signature if secret is configured
558
+ if (this.secret) {
559
+ const providedSig = message.__sig__;
560
+ if (!providedSig || !this.verifySignature(message, providedSig)) {
561
+ console.warn('PostMessageTransport: Rejected message with invalid signature');
562
+ return;
563
+ }
564
+ }
565
+ // Handle pending request responses
566
+ const pending = this.pendingRequests.get(message.id);
567
+ if (pending) {
568
+ clearTimeout(pending.timeoutId);
569
+ this.pendingRequests.delete(message.id);
570
+ pending.resolve(message);
571
+ return;
572
+ }
573
+ // Handle callback invocations
574
+ if (message.type === 'callback' && message.callbackId) {
575
+ this.handleCallback(message);
576
+ return;
577
+ }
578
+ // Notify subscribers
579
+ for (const sub of this.subscribers) {
580
+ sub(message);
581
+ }
582
+ }
583
+ async handleCallback(message) {
584
+ try {
585
+ const result = await this.callbackRegistry.invoke(message.callbackId, message.params ?? []);
586
+ this.send({
587
+ id: message.id,
588
+ type: 'result',
589
+ result,
590
+ });
591
+ }
592
+ catch (error) {
593
+ this.send({
594
+ id: message.id,
595
+ type: 'error',
596
+ error: {
597
+ message: error instanceof Error ? error.message : String(error),
598
+ },
599
+ });
600
+ }
601
+ }
602
+ /**
603
+ * Sign a message with HMAC-SHA256
604
+ */
605
+ async signMessage(message) {
606
+ if (!this.secret)
607
+ return '';
608
+ const data = JSON.stringify({ id: message.id, type: message.type, method: message.method });
609
+ const encoder = new TextEncoder();
610
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
611
+ const key = await crypto.subtle.importKey('raw', encoder.encode(this.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
612
+ const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
613
+ return Array.from(new Uint8Array(signature), b => b.toString(16).padStart(2, '0')).join('');
614
+ }
615
+ // Fallback: simple hash (less secure, but better than nothing)
616
+ let hash = 0;
617
+ const combined = this.secret + data;
618
+ for (let i = 0; i < combined.length; i++) {
619
+ hash = ((hash << 5) - hash) + combined.charCodeAt(i);
620
+ hash = hash & hash;
621
+ }
622
+ return Math.abs(hash).toString(16);
623
+ }
624
+ /**
625
+ * Verify message signature
626
+ */
627
+ verifySignature(message, providedSig) {
628
+ // For sync verification, we use the simple hash fallback
629
+ // A more complete implementation would use async verification
630
+ if (!this.secret)
631
+ return true;
632
+ const data = JSON.stringify({ id: message.id, type: message.type, method: message.method });
633
+ let hash = 0;
634
+ const combined = this.secret + data;
635
+ for (let i = 0; i < combined.length; i++) {
636
+ hash = ((hash << 5) - hash) + combined.charCodeAt(i);
637
+ hash = hash & hash;
638
+ }
639
+ const expectedSig = Math.abs(hash).toString(16);
640
+ return providedSig === expectedSig;
641
+ }
642
+ send(message) {
643
+ // Serialize callbacks
644
+ if (message.params) {
645
+ const { value } = this.callbackRegistry.serializeWithCallbacks(message.params);
646
+ message = { ...message, params: value };
647
+ }
648
+ // Add signature if secret is configured (sync version for send)
649
+ if (this.secret) {
650
+ const data = JSON.stringify({ id: message.id, type: message.type, method: message.method });
651
+ let hash = 0;
652
+ const combined = this.secret + data;
653
+ for (let i = 0; i < combined.length; i++) {
654
+ hash = ((hash << 5) - hash) + combined.charCodeAt(i);
655
+ hash = hash & hash;
656
+ }
657
+ ;
658
+ message.__sig__ = Math.abs(hash).toString(16);
659
+ }
660
+ if ('postMessage' in this.target) {
661
+ if (this.isWindowTarget) {
662
+ this.target.postMessage(message, this.targetOrigin);
663
+ }
664
+ else {
665
+ this.target.postMessage(message);
666
+ }
667
+ }
668
+ }
669
+ async request(message) {
670
+ if (this.state === 'disconnected') {
671
+ return Promise.reject(new Error('Transport is closed'));
672
+ }
673
+ if (this.pendingRequests.size >= this.maxPendingRequests) {
674
+ return Promise.reject(new Error('Too many pending requests'));
675
+ }
676
+ return new Promise((resolve, reject) => {
677
+ const timeoutId = setTimeout(() => {
678
+ this.pendingRequests.delete(message.id);
679
+ reject(new Error(`Request timeout: ${message.id}`));
680
+ }, this.timeout);
681
+ this.pendingRequests.set(message.id, { resolve, reject, timeoutId });
682
+ this.send(message);
683
+ });
684
+ }
685
+ subscribe(handler) {
686
+ this.subscribers.add(handler);
687
+ return () => this.subscribers.delete(handler);
688
+ }
689
+ close() {
690
+ if ('removeEventListener' in this.target) {
691
+ this.target.removeEventListener('message', this.messageHandler);
692
+ }
693
+ this.state = 'disconnected';
694
+ // Clear all pending requests
695
+ for (const pending of this.pendingRequests.values()) {
696
+ clearTimeout(pending.timeoutId);
697
+ pending.reject(new Error('Transport closed'));
698
+ }
699
+ this.pendingRequests.clear();
700
+ // Cleanup callback registry
701
+ this.callbackRegistry.destroy();
702
+ }
703
+ }
704
+ // =============================================================================
705
+ // Factory Functions
706
+ // =============================================================================
707
+ /**
708
+ * Create an HTTP transport
709
+ */
710
+ export function createHTTPTransport(options) {
711
+ return new HTTPTransport(options);
712
+ }
713
+ /**
714
+ * Create a WebSocket transport
715
+ */
716
+ export function createWebSocketTransport(options) {
717
+ return new WebSocketTransport(options);
718
+ }
719
+ /**
720
+ * Create a postMessage transport
721
+ */
722
+ export function createPostMessageTransport(options) {
723
+ return new PostMessageTransport(options);
724
+ }
725
+ /**
726
+ * Generate a unique message ID
727
+ */
728
+ export function generateMessageId() {
729
+ return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
730
+ }
731
+ //# sourceMappingURL=transport.js.map