@sygnl/talon 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @sygnl/talon
2
+
3
+ > Bidirectional event delivery client with WebSocket and Webhook transports.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @sygnl/talon
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### WebSocket (Bidirectional)
14
+
15
+ ```typescript
16
+ import { TalonClient, WebSocketTransport } from '@sygnl/talon';
17
+
18
+ const talon = new TalonClient({
19
+ transport: new WebSocketTransport('wss://edge.example.com'),
20
+ autoConnect: true
21
+ });
22
+
23
+ // Send events TO edge
24
+ await talon.send({ event: 'purchase', order_id: '123', total: 99.99 });
25
+
26
+ // Receive updates FROM edge
27
+ talon.on('attribution_updated', (data) => {
28
+ console.log('Attribution:', data);
29
+ });
30
+
31
+ talon.on('context_synced', (data) => {
32
+ console.log('Context synced:', data);
33
+ });
34
+ ```
35
+
36
+ ### Webhook (Fire-and-forget)
37
+
38
+ ```typescript
39
+ import { TalonClient, WebhookTransport } from '@sygnl/talon';
40
+
41
+ const talon = new TalonClient({
42
+ transport: new WebhookTransport('https://edge.example.com/webhook')
43
+ });
44
+
45
+ await talon.send({ event: 'page_view', url: '/products' });
46
+ ```
47
+
48
+ ## API
49
+
50
+ ### `TalonClient`
51
+
52
+ ```typescript
53
+ const talon = new TalonClient({
54
+ transport: WebSocketTransport | WebhookTransport,
55
+ autoConnect?: boolean,
56
+ debug?: boolean
57
+ });
58
+
59
+ // Methods
60
+ await talon.connect();
61
+ await talon.disconnect();
62
+ await talon.send(event);
63
+ talon.on(eventName, handler);
64
+ talon.off(eventName, handler);
65
+ talon.isConnected(); // boolean
66
+ ```
67
+
68
+ ### Transports
69
+
70
+ **WebSocketTransport** - Bidirectional, auto-reconnect
71
+ ```typescript
72
+ new WebSocketTransport(url, {
73
+ reconnect?: boolean,
74
+ reconnectInterval?: number,
75
+ maxReconnectAttempts?: number
76
+ });
77
+ ```
78
+
79
+ **WebhookTransport** - One-way HTTP POST
80
+ ```typescript
81
+ new WebhookTransport(url, {
82
+ method?: 'POST' | 'PUT',
83
+ retries?: number
84
+ });
85
+ ```
86
+
87
+ ## Connection States
88
+
89
+ - `disconnected` - Not connected
90
+ - `connecting` - Connection in progress
91
+ - `connected` - Ready to send/receive
92
+ - `reconnecting` - Attempting to reconnect
93
+
94
+ ## License
95
+
96
+ Apache-2.0
package/dist/index.cjs ADDED
@@ -0,0 +1,347 @@
1
+ 'use strict';
2
+
3
+ // src/transports/WebSocketTransport.ts
4
+ var WebSocketTransport = class {
5
+ constructor(options) {
6
+ this.ws = null;
7
+ this._state = "disconnected";
8
+ this.reconnectAttempts = 0;
9
+ this.reconnectTimeout = null;
10
+ this.messageHandlers = /* @__PURE__ */ new Set();
11
+ this.stateHandlers = /* @__PURE__ */ new Set();
12
+ this.errorHandlers = /* @__PURE__ */ new Set();
13
+ this.messageQueue = [];
14
+ this.url = options.url;
15
+ this.autoReconnect = options.autoReconnect ?? true;
16
+ this.reconnectDelay = options.reconnectDelay ?? 1e3;
17
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 0;
18
+ this.connectionTimeout = options.connectionTimeout ?? 1e4;
19
+ this.debug = options.debug ?? false;
20
+ }
21
+ get state() {
22
+ return this._state;
23
+ }
24
+ async connect() {
25
+ if (this._state === "connected" || this._state === "connecting") {
26
+ this.log("Already connected or connecting");
27
+ return;
28
+ }
29
+ this.setState("connecting");
30
+ this.log(`Connecting to ${this.url}`);
31
+ return new Promise((resolve, reject) => {
32
+ try {
33
+ this.ws = new WebSocket(this.url);
34
+ const timeout = setTimeout(() => {
35
+ reject(new Error("Connection timeout"));
36
+ this.ws?.close();
37
+ }, this.connectionTimeout);
38
+ this.ws.onopen = () => {
39
+ clearTimeout(timeout);
40
+ this.setState("connected");
41
+ this.reconnectAttempts = 0;
42
+ this.log("Connected");
43
+ this.flushMessageQueue();
44
+ resolve();
45
+ };
46
+ this.ws.onerror = () => {
47
+ clearTimeout(timeout);
48
+ const error = new Error("WebSocket connection error");
49
+ this.log("Connection error:", error);
50
+ this.emitError(error);
51
+ reject(error);
52
+ };
53
+ this.ws.onclose = () => {
54
+ clearTimeout(timeout);
55
+ this.handleDisconnect();
56
+ };
57
+ this.ws.onmessage = (event) => {
58
+ this.handleMessage(event.data);
59
+ };
60
+ } catch (error) {
61
+ this.setState("error");
62
+ const err = error instanceof Error ? error : new Error("Unknown error");
63
+ this.emitError(err);
64
+ reject(err);
65
+ }
66
+ });
67
+ }
68
+ disconnect() {
69
+ this.log("Disconnecting");
70
+ this.autoReconnect = false;
71
+ if (this.reconnectTimeout) {
72
+ clearTimeout(this.reconnectTimeout);
73
+ this.reconnectTimeout = null;
74
+ }
75
+ if (this.ws) {
76
+ this.ws.close();
77
+ this.ws = null;
78
+ }
79
+ this.setState("disconnected");
80
+ }
81
+ async send(message) {
82
+ if (this._state !== "connected" || !this.ws) {
83
+ this.log("Not connected, queueing message");
84
+ this.messageQueue.push(message);
85
+ return;
86
+ }
87
+ try {
88
+ const payload = JSON.stringify(message);
89
+ this.ws.send(payload);
90
+ this.log("Sent message:", message);
91
+ } catch (error) {
92
+ const err = error instanceof Error ? error : new Error("Send failed");
93
+ this.log("Send error:", err);
94
+ this.emitError(err);
95
+ throw err;
96
+ }
97
+ }
98
+ onMessage(handler) {
99
+ this.messageHandlers.add(handler);
100
+ return () => this.messageHandlers.delete(handler);
101
+ }
102
+ onStateChange(handler) {
103
+ this.stateHandlers.add(handler);
104
+ return () => this.stateHandlers.delete(handler);
105
+ }
106
+ onError(handler) {
107
+ this.errorHandlers.add(handler);
108
+ return () => this.errorHandlers.delete(handler);
109
+ }
110
+ /**
111
+ * Handle incoming WebSocket message
112
+ */
113
+ handleMessage(data) {
114
+ try {
115
+ const message = JSON.parse(data);
116
+ this.log("Received message:", message);
117
+ this.messageHandlers.forEach((handler) => {
118
+ try {
119
+ handler(message);
120
+ } catch (error) {
121
+ this.log("Handler error:", error);
122
+ }
123
+ });
124
+ } catch (error) {
125
+ this.log("Failed to parse message:", error);
126
+ this.emitError(new Error("Invalid message format"));
127
+ }
128
+ }
129
+ /**
130
+ * Handle WebSocket disconnect
131
+ */
132
+ handleDisconnect() {
133
+ this.log("Disconnected");
134
+ this.ws = null;
135
+ if (this.autoReconnect && (this.maxReconnectAttempts === 0 || this.reconnectAttempts < this.maxReconnectAttempts)) {
136
+ this.setState("reconnecting");
137
+ this.scheduleReconnect();
138
+ } else {
139
+ this.setState("disconnected");
140
+ }
141
+ }
142
+ /**
143
+ * Schedule reconnection attempt with exponential backoff
144
+ */
145
+ scheduleReconnect() {
146
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
147
+ this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
148
+ this.reconnectTimeout = setTimeout(() => {
149
+ this.reconnectAttempts++;
150
+ this.connect().catch((error) => {
151
+ this.log("Reconnect failed:", error);
152
+ this.handleDisconnect();
153
+ });
154
+ }, delay);
155
+ }
156
+ /**
157
+ * Flush queued messages
158
+ */
159
+ flushMessageQueue() {
160
+ if (this.messageQueue.length === 0) {
161
+ return;
162
+ }
163
+ this.log(`Flushing ${this.messageQueue.length} queued messages`);
164
+ const queue = [...this.messageQueue];
165
+ this.messageQueue = [];
166
+ queue.forEach((message) => {
167
+ this.send(message).catch((error) => {
168
+ this.log("Failed to flush message:", error);
169
+ });
170
+ });
171
+ }
172
+ /**
173
+ * Set connection state
174
+ */
175
+ setState(state) {
176
+ if (this._state === state) {
177
+ return;
178
+ }
179
+ this._state = state;
180
+ this.stateHandlers.forEach((handler) => {
181
+ try {
182
+ handler(state);
183
+ } catch (error) {
184
+ this.log("State handler error:", error);
185
+ }
186
+ });
187
+ }
188
+ /**
189
+ * Emit error to handlers
190
+ */
191
+ emitError(error) {
192
+ this.errorHandlers.forEach((handler) => {
193
+ try {
194
+ handler(error);
195
+ } catch (err) {
196
+ this.log("Error handler error:", err);
197
+ }
198
+ });
199
+ }
200
+ /**
201
+ * Debug logging
202
+ */
203
+ log(message, data) {
204
+ if (this.debug && typeof console !== "undefined") {
205
+ console.log(`[WebSocketTransport] ${message}`, data || "");
206
+ }
207
+ }
208
+ };
209
+
210
+ // src/transports/WebhookTransport.ts
211
+ var WebhookTransport = class {
212
+ constructor(options) {
213
+ this._state = "disconnected";
214
+ this.messageHandlers = /* @__PURE__ */ new Set();
215
+ this.stateHandlers = /* @__PURE__ */ new Set();
216
+ this.errorHandlers = /* @__PURE__ */ new Set();
217
+ this.url = options.url;
218
+ this.method = options.method ?? "POST";
219
+ this.headers = {
220
+ "Content-Type": "application/json",
221
+ ...options.headers
222
+ };
223
+ this.timeout = options.timeout ?? 1e4;
224
+ this.retry = options.retry ?? false;
225
+ this.maxRetries = options.maxRetries ?? 3;
226
+ this.retryDelay = options.retryDelay ?? 1e3;
227
+ this.debug = options.debug ?? false;
228
+ }
229
+ get state() {
230
+ return this._state;
231
+ }
232
+ async connect() {
233
+ this.log("Webhook transport ready");
234
+ this.setState("connected");
235
+ }
236
+ disconnect() {
237
+ this.log("Disconnecting webhook transport");
238
+ this.setState("disconnected");
239
+ }
240
+ async send(message) {
241
+ if (this._state !== "connected") {
242
+ throw new Error("Transport not connected");
243
+ }
244
+ let lastError = null;
245
+ const attempts = this.retry ? this.maxRetries + 1 : 1;
246
+ for (let attempt = 0; attempt < attempts; attempt++) {
247
+ try {
248
+ await this.sendRequest(message);
249
+ this.log(`Sent message (attempt ${attempt + 1})`);
250
+ return;
251
+ } catch (error) {
252
+ lastError = error instanceof Error ? error : new Error("Send failed");
253
+ this.log(`Send failed (attempt ${attempt + 1}):`, lastError);
254
+ if (attempt < attempts - 1) {
255
+ const delay = this.retryDelay * Math.pow(2, attempt);
256
+ await new Promise((resolve) => setTimeout(resolve, delay));
257
+ }
258
+ }
259
+ }
260
+ if (lastError) {
261
+ this.emitError(lastError);
262
+ throw lastError;
263
+ }
264
+ }
265
+ onMessage(handler) {
266
+ this.messageHandlers.add(handler);
267
+ return () => this.messageHandlers.delete(handler);
268
+ }
269
+ onStateChange(handler) {
270
+ this.stateHandlers.add(handler);
271
+ return () => this.stateHandlers.delete(handler);
272
+ }
273
+ onError(handler) {
274
+ this.errorHandlers.add(handler);
275
+ return () => this.errorHandlers.delete(handler);
276
+ }
277
+ /**
278
+ * Send HTTP request
279
+ */
280
+ async sendRequest(message) {
281
+ const controller = new AbortController();
282
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
283
+ try {
284
+ const response = await fetch(this.url, {
285
+ method: this.method,
286
+ headers: this.headers,
287
+ body: JSON.stringify(message),
288
+ signal: controller.signal
289
+ });
290
+ clearTimeout(timeoutId);
291
+ if (!response.ok) {
292
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
293
+ }
294
+ this.log(`Request successful: ${response.status}`);
295
+ } catch (error) {
296
+ clearTimeout(timeoutId);
297
+ if (error instanceof Error) {
298
+ if (error.name === "AbortError") {
299
+ throw new Error("Request timeout");
300
+ }
301
+ throw error;
302
+ }
303
+ throw new Error("Request failed");
304
+ }
305
+ }
306
+ /**
307
+ * Set connection state
308
+ */
309
+ setState(state) {
310
+ if (this._state === state) {
311
+ return;
312
+ }
313
+ this._state = state;
314
+ this.stateHandlers.forEach((handler) => {
315
+ try {
316
+ handler(state);
317
+ } catch (error) {
318
+ this.log("State handler error:", error);
319
+ }
320
+ });
321
+ }
322
+ /**
323
+ * Emit error to handlers
324
+ */
325
+ emitError(error) {
326
+ this.errorHandlers.forEach((handler) => {
327
+ try {
328
+ handler(error);
329
+ } catch (err) {
330
+ this.log("Error handler error:", err);
331
+ }
332
+ });
333
+ }
334
+ /**
335
+ * Debug logging
336
+ */
337
+ log(message, data) {
338
+ if (this.debug && typeof console !== "undefined") {
339
+ console.log(`[WebhookTransport] ${message}`, data || "");
340
+ }
341
+ }
342
+ };
343
+
344
+ exports.WebSocketTransport = WebSocketTransport;
345
+ exports.WebhookTransport = WebhookTransport;
346
+ //# sourceMappingURL=index.cjs.map
347
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/TalonClient.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type {\n TalonTransport,\n TalonClientOptions,\n OutboundMessage,\n InboundMessage,\n EventHandler,\n TransportState,\n} from './types';\n\n/**\n * TalonClient - Bidirectional event delivery client\n * \n * Features:\n * - Send events TO the edge\n * - Receive updates FROM the edge\n * - Transport-agnostic (WebSocket, Webhook, etc.)\n * - Event emitter pattern for edge responses\n * \n * @example\n * ```typescript\n * import { TalonClient } from '@sygnl/talon';\n * import { WebSocketTransport } from '@sygnl/talon/transports';\n * \n * const talon = new TalonClient({\n * transport: new WebSocketTransport('wss://edge.sygnl.io')\n * });\n * \n * // Send event TO edge\n * await talon.send({ event: 'add_to_cart', product_id: 'abc' });\n * \n * // Receive updates FROM edge\n * talon.on('attribution_updated', (data) => {\n * console.log('Attribution:', data);\n * });\n * ```\n */\nexport class TalonClient {\n private transport: TalonTransport;\n private eventHandlers: Map<string, Set<EventHandler>>;\n private messageUnsubscribe?: () => void;\n private stateUnsubscribe?: () => void;\n private errorUnsubscribe?: () => void;\n private debug: boolean;\n\n constructor(options: TalonClientOptions) {\n this.transport = options.transport;\n this.eventHandlers = new Map();\n this.debug = options.debug ?? false;\n\n // Subscribe to transport messages\n this.messageUnsubscribe = this.transport.onMessage(this.handleMessage.bind(this));\n this.stateUnsubscribe = this.transport.onStateChange(this.handleStateChange.bind(this));\n this.errorUnsubscribe = this.transport.onError(this.handleError.bind(this));\n\n // Auto-connect if requested\n if (options.autoConnect !== false) {\n this.connect().catch((error) => {\n this.log('Auto-connect failed:', error);\n });\n }\n }\n\n /**\n * Connect to the edge\n */\n public async connect(): Promise<void> {\n return this.transport.connect();\n }\n\n /**\n * Disconnect from the edge\n */\n public disconnect(): void {\n this.transport.disconnect();\n this.cleanup();\n }\n\n /**\n * Send event TO the edge\n * \n * @param event - Event data to send\n * @returns Promise that resolves when sent\n * \n * @example\n * ```typescript\n * await talon.send({\n * event: 'add_to_cart',\n * product_id: 'prod_123',\n * price: 99.99\n * });\n * ```\n */\n public async send(event: Record<string, unknown>): Promise<void> {\n const message: OutboundMessage = {\n type: 'event',\n data: event,\n id: this.generateId(),\n timestamp: Date.now(),\n };\n\n this.log('Sending message:', message);\n return this.transport.send(message);\n }\n\n /**\n * Subscribe to events FROM the edge\n * \n * @param eventType - Event type to listen for\n * @param handler - Function to call when event received\n * @returns Unsubscribe function\n * \n * @example\n * ```typescript\n * const unsubscribe = talon.on('attribution_updated', (data) => {\n * console.log('Attribution:', data);\n * });\n * \n * // Later: unsubscribe()\n * ```\n */\n public on<T = any>(eventType: string, handler: EventHandler<T>): () => void {\n if (!this.eventHandlers.has(eventType)) {\n this.eventHandlers.set(eventType, new Set());\n }\n\n this.eventHandlers.get(eventType)!.add(handler);\n this.log(`Subscribed to event: ${eventType}`);\n\n // Return unsubscribe function\n return () => {\n const handlers = this.eventHandlers.get(eventType);\n if (handlers) {\n handlers.delete(handler);\n if (handlers.size === 0) {\n this.eventHandlers.delete(eventType);\n }\n }\n this.log(`Unsubscribed from event: ${eventType}`);\n };\n }\n\n /**\n * Subscribe to event once (auto-unsubscribe after first call)\n */\n public once<T = any>(eventType: string, handler: EventHandler<T>): () => void {\n const wrappedHandler = (data: T) => {\n handler(data);\n unsubscribe();\n };\n\n const unsubscribe = this.on(eventType, wrappedHandler);\n return unsubscribe;\n }\n\n /**\n * Remove all handlers for an event type\n */\n public off(eventType: string): void {\n this.eventHandlers.delete(eventType);\n this.log(`Removed all handlers for: ${eventType}`);\n }\n\n /**\n * Get current connection state\n */\n public get state(): TransportState {\n return this.transport.state;\n }\n\n /**\n * Check if connected\n */\n public get connected(): boolean {\n return this.transport.state === 'connected';\n }\n\n /**\n * Handle incoming message from transport\n */\n private handleMessage(message: InboundMessage): void {\n this.log('Received message:', message);\n\n const handlers = this.eventHandlers.get(message.type);\n if (handlers && handlers.size > 0) {\n handlers.forEach((handler) => {\n try {\n handler(message.data);\n } catch (error) {\n this.log(`Error in handler for ${message.type}:`, error);\n }\n });\n } else {\n this.log(`No handlers for message type: ${message.type}`);\n }\n }\n\n /**\n * Handle transport state change\n */\n private handleStateChange(state: TransportState): void {\n this.log(`State changed: ${state}`);\n \n // Emit state change event\n const handlers = this.eventHandlers.get('_state_change');\n if (handlers) {\n handlers.forEach((handler) => handler({ state }));\n }\n }\n\n /**\n * Handle transport error\n */\n private handleError(error: Error): void {\n this.log('Transport error:', error);\n \n // Emit error event\n const handlers = this.eventHandlers.get('_error');\n if (handlers) {\n handlers.forEach((handler) => handler({ error }));\n }\n }\n\n /**\n * Cleanup subscriptions\n */\n private cleanup(): void {\n this.messageUnsubscribe?.();\n this.stateUnsubscribe?.();\n this.errorUnsubscribe?.();\n this.eventHandlers.clear();\n }\n\n /**\n * Generate unique message ID\n */\n private generateId(): string {\n return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n }\n\n /**\n * Debug logging\n */\n private log(message: string, data?: unknown): void {\n if (this.debug && typeof console !== 'undefined') {\n console.log(`[TalonClient] ${message}`, data || '');\n }\n }\n}\n"]} \n /** Auto-reconnect on disconnect */\n autoReconnect?: boolean;\n \n /** Reconnect delay in ms */\n reconnectDelay?: number;\n \n /** Max reconnect attempts (0 = infinite) */\n maxReconnectAttempts?: number;\n \n /** Connection timeout in ms */\n connectionTimeout?: number;\n \n /** Debug mode */\n debug?: boolean;\n}\n\n/**\n * WebSocket transport for bidirectional communication\n * \n * Features:\n * - Real-time bidirectional communication\n * - Auto-reconnect with exponential backoff\n * - Connection state management\n * - Message queuing during disconnection\n * \n * @example\n * ```typescript\n * const transport = new WebSocketTransport({\n * url: 'wss://edge.sygnl.io',\n * autoReconnect: true\n * });\n * ```\n */\nexport class WebSocketTransport implements TalonTransport {\n private url: string;\n private ws: WebSocket | null = null;\n private _state: TransportState = 'disconnected';\n private autoReconnect: boolean;\n private reconnectDelay: number;\n private maxReconnectAttempts: number;\n private reconnectAttempts = 0;\n private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;\n private connectionTimeout: number;\n private debug: boolean;\n \n private messageHandlers = new Set<(message: InboundMessage) => void>();\n private stateHandlers = new Set<(state: TransportState) => void>();\n private errorHandlers = new Set<(error: Error) => void>();\n private messageQueue: OutboundMessage[] = [];\n\n constructor(options: WebSocketTransportOptions) {\n this.url = options.url;\n this.autoReconnect = options.autoReconnect ?? true;\n this.reconnectDelay = options.reconnectDelay ?? 1000;\n this.maxReconnectAttempts = options.maxReconnectAttempts ?? 0; // 0 = infinite\n this.connectionTimeout = options.connectionTimeout ?? 10000;\n this.debug = options.debug ?? false;\n }\n\n public get state(): TransportState {\n return this._state;\n }\n\n public async connect(): Promise<void> {\n if (this._state === 'connected' || this._state === 'connecting') {\n this.log('Already connected or connecting');\n return;\n }\n\n this.setState('connecting');\n this.log(`Connecting to ${this.url}`);\n\n return new Promise((resolve, reject) => {\n try {\n this.ws = new WebSocket(this.url);\n\n const timeout = setTimeout(() => {\n reject(new Error('Connection timeout'));\n this.ws?.close();\n }, this.connectionTimeout);\n\n this.ws.onopen = () => {\n clearTimeout(timeout);\n this.setState('connected');\n this.reconnectAttempts = 0;\n this.log('Connected');\n \n // Flush queued messages\n this.flushMessageQueue();\n \n resolve();\n };\n\n this.ws.onerror = () => {\n clearTimeout(timeout);\n const error = new Error('WebSocket connection error');\n this.log('Connection error:', error);\n this.emitError(error);\n reject(error);\n };\n\n this.ws.onclose = () => {\n clearTimeout(timeout);\n this.handleDisconnect();\n };\n\n this.ws.onmessage = (event) => {\n this.handleMessage(event.data);\n };\n } catch (error) {\n this.setState('error');\n const err = error instanceof Error ? error : new Error('Unknown error');\n this.emitError(err);\n reject(err);\n }\n });\n }\n\n public disconnect(): void {\n this.log('Disconnecting');\n this.autoReconnect = false; // Disable auto-reconnect on manual disconnect\n \n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n\n this.setState('disconnected');\n }\n\n public async send(message: OutboundMessage): Promise<void> {\n if (this._state !== 'connected' || !this.ws) {\n // Queue message if not connected\n this.log('Not connected, queueing message');\n this.messageQueue.push(message);\n return;\n }\n\n try {\n const payload = JSON.stringify(message);\n this.ws.send(payload);\n this.log('Sent message:', message);\n } catch (error) {\n const err = error instanceof Error ? error : new Error('Send failed');\n this.log('Send error:', err);\n this.emitError(err);\n throw err;\n }\n }\n\n public onMessage(handler: (message: InboundMessage) => void): () => void {\n this.messageHandlers.add(handler);\n return () => this.messageHandlers.delete(handler);\n }\n\n public onStateChange(handler: (state: TransportState) => void): () => void {\n this.stateHandlers.add(handler);\n return () => this.stateHandlers.delete(handler);\n }\n\n public onError(handler: (error: Error) => void): () => void {\n this.errorHandlers.add(handler);\n return () => this.errorHandlers.delete(handler);\n }\n\n /**\n * Handle incoming WebSocket message\n */\n private handleMessage(data: string): void {\n try {\n const message: InboundMessage = JSON.parse(data);\n this.log('Received message:', message);\n \n this.messageHandlers.forEach((handler) => {\n try {\n handler(message);\n } catch (error) {\n this.log('Handler error:', error);\n }\n });\n } catch (error) {\n this.log('Failed to parse message:', error);\n this.emitError(new Error('Invalid message format'));\n }\n }\n\n /**\n * Handle WebSocket disconnect\n */\n private handleDisconnect(): void {\n this.log('Disconnected');\n this.ws = null;\n\n if (this.autoReconnect && \n (this.maxReconnectAttempts === 0 || this.reconnectAttempts < this.maxReconnectAttempts)) {\n this.setState('reconnecting');\n this.scheduleReconnect();\n } else {\n this.setState('disconnected');\n }\n }\n\n /**\n * Schedule reconnection attempt with exponential backoff\n */\n private scheduleReconnect(): void {\n const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);\n this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);\n\n this.reconnectTimeout = setTimeout(() => {\n this.reconnectAttempts++;\n this.connect().catch((error) => {\n this.log('Reconnect failed:', error);\n this.handleDisconnect();\n });\n }, delay);\n }\n\n /**\n * Flush queued messages\n */\n private flushMessageQueue(): void {\n if (this.messageQueue.length === 0) {\n return;\n }\n\n this.log(`Flushing ${this.messageQueue.length} queued messages`);\n const queue = [...this.messageQueue];\n this.messageQueue = [];\n\n queue.forEach((message) => {\n this.send(message).catch((error) => {\n this.log('Failed to flush message:', error);\n });\n });\n }\n\n /**\n * Set connection state\n */\n private setState(state: TransportState): void {\n if (this._state === state) {\n return;\n }\n\n this._state = state;\n this.stateHandlers.forEach((handler) => {\n try {\n handler(state);\n } catch (error) {\n this.log('State handler error:', error);\n }\n });\n }\n\n /**\n * Emit error to handlers\n */\n private emitError(error: Error): void {\n this.errorHandlers.forEach((handler) => {\n try {\n handler(error);\n } catch (err) {\n this.log('Error handler error:', err);\n }\n });\n }\n\n /**\n * Debug logging\n */\n private log(message: string, data?: unknown): void {\n if (this.debug && typeof console !== 'undefined') {\n console.log(`[WebSocketTransport] ${message}`, data || '');\n }\n }\n}\n","import type {\n TalonTransport,\n OutboundMessage,\n InboundMessage,\n TransportState,\n} from '../types';\n\n/**\n * Webhook transport options\n */\nexport interface WebhookTransportOptions {\n /** Webhook URL */\n url: string;\n \n /** HTTP method */\n method?: 'POST' | 'PUT';\n \n /** Custom headers */\n headers?: Record<string, string>;\n \n /** Request timeout in ms */\n timeout?: number;\n \n /** Retry failed requests */\n retry?: boolean;\n \n /** Max retry attempts */\n maxRetries?: number;\n \n /** Retry delay in ms */\n retryDelay?: number;\n \n /** Debug mode */\n debug?: boolean;\n}\n\n/**\n * Webhook transport for fire-and-forget HTTP requests\n * \n * Features:\n * - One-way communication (no responses)\n * - HTTP POST/PUT requests\n * - Optional retry logic\n * - Custom headers support\n * \n * Use this for:\n * - Server-side event delivery\n * - Fire-and-forget scenarios\n * - When WebSocket not available/needed\n * \n * @example\n * ```typescript\n * const transport = new WebhookTransport({\n * url: 'https://edge.sygnl.io/webhook',\n * method: 'POST',\n * retry: true\n * });\n * ```\n */\nexport class WebhookTransport implements TalonTransport {\n private url: string;\n private method: 'POST' | 'PUT';\n private headers: Record<string, string>;\n private timeout: number;\n private retry: boolean;\n private maxRetries: number;\n private retryDelay: number;\n private debug: boolean;\n private _state: TransportState = 'disconnected';\n \n private messageHandlers = new Set<(message: InboundMessage) => void>();\n private stateHandlers = new Set<(state: TransportState) => void>();\n private errorHandlers = new Set<(error: Error) => void>();\n\n constructor(options: WebhookTransportOptions) {\n this.url = options.url;\n this.method = options.method ?? 'POST';\n this.headers = {\n 'Content-Type': 'application/json',\n ...options.headers,\n };\n this.timeout = options.timeout ?? 10000;\n this.retry = options.retry ?? false;\n this.maxRetries = options.maxRetries ?? 3;\n this.retryDelay = options.retryDelay ?? 1000;\n this.debug = options.debug ?? false;\n }\n\n public get state(): TransportState {\n return this._state;\n }\n\n public async connect(): Promise<void> {\n // Webhook is \"connected\" immediately (stateless)\n this.log('Webhook transport ready');\n this.setState('connected');\n }\n\n public disconnect(): void {\n this.log('Disconnecting webhook transport');\n this.setState('disconnected');\n }\n\n public async send(message: OutboundMessage): Promise<void> {\n if (this._state !== 'connected') {\n throw new Error('Transport not connected');\n }\n\n let lastError: Error | null = null;\n const attempts = this.retry ? this.maxRetries + 1 : 1;\n\n for (let attempt = 0; attempt < attempts; attempt++) {\n try {\n await this.sendRequest(message);\n this.log(`Sent message (attempt ${attempt + 1})`);\n return;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error('Send failed');\n this.log(`Send failed (attempt ${attempt + 1}):`, lastError);\n\n if (attempt < attempts - 1) {\n // Wait before retry (exponential backoff)\n const delay = this.retryDelay * Math.pow(2, attempt);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n // All attempts failed\n if (lastError) {\n this.emitError(lastError);\n throw lastError;\n }\n }\n\n public onMessage(handler: (message: InboundMessage) => void): () => void {\n // Webhook is one-way, but keep interface consistent\n this.messageHandlers.add(handler);\n return () => this.messageHandlers.delete(handler);\n }\n\n public onStateChange(handler: (state: TransportState) => void): () => void {\n this.stateHandlers.add(handler);\n return () => this.stateHandlers.delete(handler);\n }\n\n public onError(handler: (error: Error) => void): () => void {\n this.errorHandlers.add(handler);\n return () => this.errorHandlers.delete(handler);\n }\n\n /**\n * Send HTTP request\n */\n private async sendRequest(message: OutboundMessage): Promise<void> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(this.url, {\n method: this.method,\n headers: this.headers,\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n this.log(`Request successful: ${response.status}`);\n } catch (error) {\n clearTimeout(timeoutId);\n \n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new Error('Request timeout');\n }\n throw error;\n }\n \n throw new Error('Request failed');\n }\n }\n\n /**\n * Set connection state\n */\n private setState(state: TransportState): void {\n if (this._state === state) {\n return;\n }\n\n this._state = state;\n this.stateHandlers.forEach((handler) => {\n try {\n handler(state);\n } catch (error) {\n this.log('State handler error:', error);\n }\n });\n }\n\n /**\n * Emit error to handlers\n */\n private emitError(error: Error): void {\n this.errorHandlers.forEach((handler) => {\n try {\n handler(error);\n } catch (err) {\n this.log('Error handler error:', err);\n }\n });\n }\n\n /**\n * Debug logging\n */\n private log(message: string, data?: unknown): void {\n if (this.debug && typeof console !== 'undefined') {\n console.log(`[WebhookTransport] ${message}`, data || '');\n }\n }\n}\n"]}