recker 1.0.83 → 1.0.84-next.a0e9831

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ### Multi-Protocol SDK for the AI Era
6
6
 
7
- Nine protocols unified: HTTP, WebSocket, DNS, WHOIS, RDAP, FTP, SFTP, Telnet, HLS.
7
+ Ten protocols unified: HTTP, WebSocket, DNS, WHOIS, RDAP, FTP, SFTP, Telnet, HLS, Raffel.
8
8
  <br>
9
9
  AI-native: OpenAI, Anthropic, Google, Ollama + MCP server with 70 tools.
10
10
  <br>
@@ -108,13 +108,14 @@ await recker.whois('github.com'); // WHOIS
108
108
  await recker.dns('google.com'); // DNS
109
109
  await recker.ai.chat('Hello!'); // AI
110
110
  recker.ws('wss://example.com/socket'); // WebSocket
111
+ recker.raffel('ws://game:9999'); // Raffel
111
112
  ```
112
113
 
113
114
  ## What's Inside
114
115
 
115
116
  | Category | Features |
116
117
  |:---------|:---------|
117
- | **Protocols** | HTTP/2, WebSocket, DNS, WHOIS, RDAP, FTP, SFTP, Telnet, HLS |
118
+ | **Protocols** | HTTP/2, WebSocket, DNS, WHOIS, RDAP, FTP, SFTP, Telnet, HLS, Raffel |
118
119
  | **AI** | OpenAI, Anthropic, Google, Ollama, Groq, Mistral + streaming |
119
120
  | **Resilience** | Retry, circuit breaker, rate limiting, request deduplication |
120
121
  | **Auth** | Basic, Bearer, OAuth2, AWS SigV4, Digest, API Key + 15 providers |
@@ -197,6 +198,23 @@ await spider.crawl('https://protected-site.com');
197
198
 
198
199
  [📖 Anti-blocking docs](https://forattini-dev.github.io/recker/#/scraping/06-anti-blocking)
199
200
 
201
+ ### Raffel Protocol
202
+
203
+ ```typescript
204
+ import { createRaffelClient } from 'recker';
205
+
206
+ const client = createRaffelClient('ws://api:3000', { reconnect: true });
207
+ await client.connect();
208
+
209
+ const user = await client.call<User>('users.get', { id: 42 });
210
+ client.subscribe('notifications', (event, data) => console.log(event, data));
211
+ client.notify('analytics.track', { event: 'page_view' });
212
+ ```
213
+
214
+ Recker connects. Raffel serves. One protocol, zero glue.
215
+
216
+ [→ Raffel Documentation](https://forattini-dev.github.io/recker/#/protocols/10-raffel)
217
+
200
218
  ### 48 API Presets
201
219
 
202
220
  Pre-configured clients for popular services:
@@ -133,8 +133,8 @@ export const tlsHandler = withHandler({ loading: true }, async (ctx, out, extCtx
133
133
  valid: socket.authorized,
134
134
  protocol,
135
135
  cipher: cipher?.name,
136
- subject: cert.subject?.CN,
137
- issuer: cert.issuer?.O,
136
+ subject: Array.isArray(cert.subject?.CN) ? cert.subject.CN[0] : cert.subject?.CN,
137
+ issuer: Array.isArray(cert.issuer?.O) ? cert.issuer.O[0] : cert.issuer?.O,
138
138
  validFrom: cert.valid_from,
139
139
  validTo: cert.valid_to,
140
140
  fingerprint: cert.fingerprint,
@@ -45,6 +45,7 @@ export declare function createSmartInput(options: {
45
45
  }) => void;
46
46
  setValue: (v: string) => void;
47
47
  clear: () => void;
48
+ updateOptions: (nextOptions?: import("tuiuiu.js").TextInputOptions) => void;
48
49
  focus: () => void;
49
50
  };
50
51
  getSuggestions: () => Suggestion[];
@@ -161,6 +161,17 @@ export class ResourceRegistry {
161
161
  mimeType: 'text/markdown',
162
162
  text: this.getBenchmarkSummary(),
163
163
  }]);
164
+ this.registerResource({
165
+ uri: 'recker://docs/raffel',
166
+ name: 'Raffel Protocol Client',
167
+ description: 'Connect to Raffel servers: RPC calls, channels, events, error handling',
168
+ mimeType: 'text/markdown',
169
+ }, () => [{
170
+ type: 'resource',
171
+ uri: 'recker://docs/raffel',
172
+ mimeType: 'text/markdown',
173
+ text: RAFFEL_DOCS,
174
+ }]);
164
175
  this.registerResource({
165
176
  uri: 'recker://seo/checklist',
166
177
  name: 'SEO Technical Checklist',
@@ -1091,3 +1102,122 @@ function getErrorMessage(error) {
1091
1102
  return String(error);
1092
1103
  }
1093
1104
  }
1105
+ const RAFFEL_DOCS = `# Raffel Protocol Client
1106
+
1107
+ Connect to Raffel servers with type-safe RPC, channels, and events over WebSocket.
1108
+
1109
+ ## Setup
1110
+
1111
+ \`\`\`typescript
1112
+ import { createRaffelClient, RaffelError } from 'recker';
1113
+ // or: import { RaffelClient } from 'recker';
1114
+ // or: recker.raffel(url, options)
1115
+ \`\`\`
1116
+
1117
+ ## Connect & Close
1118
+
1119
+ \`\`\`typescript
1120
+ // Transport auto-detected from URL: ws://, http://, tcp://, udp://, http://host/rpc
1121
+ const client = createRaffelClient('ws://localhost:3000', {
1122
+ defaultTimeout: 10000,
1123
+ channels: ['lobby'],
1124
+ ws: { // WebSocket-specific options
1125
+ reconnect: true,
1126
+ reconnectDelay: 1000,
1127
+ maxReconnectAttempts: 5,
1128
+ heartbeatInterval: 30000,
1129
+ headers: { Authorization: 'Bearer token' },
1130
+ },
1131
+ });
1132
+ // HTTP: createRaffelClient('http://api:3000', { http: { timeout: 10000 } })
1133
+ // TCP: createRaffelClient('tcp://svc:9000', { tcp: { reconnect: true } })
1134
+ // UDP: createRaffelClient('udp://svc:9000')
1135
+ // JSON-RPC: createRaffelClient('http://api:3000/rpc')
1136
+ await client.connect();
1137
+ client.close();
1138
+ \`\`\`
1139
+
1140
+ ## RPC Calls
1141
+
1142
+ \`\`\`typescript
1143
+ const user = await client.call<User>('users.get', { id: 42 });
1144
+ const data = await client.call('slow.op', payload, { timeout: 60000 });
1145
+ const res = await client.call('search', query, { signal: abortController.signal });
1146
+ client.cancel('req-3'); // Cancel in-flight call
1147
+ \`\`\`
1148
+
1149
+ ## Fire-and-Forget
1150
+
1151
+ \`\`\`typescript
1152
+ client.notify('analytics.track', { event: 'click' });
1153
+ \`\`\`
1154
+
1155
+ ## Channels
1156
+
1157
+ \`\`\`typescript
1158
+ client.subscribe('chat', (event, data) => console.log(event, data));
1159
+ client.publish('chat', 'message', { text: 'Hello' });
1160
+ client.unsubscribe('chat');
1161
+ \`\`\`
1162
+
1163
+ ## Events
1164
+
1165
+ | Event | Args | Description |
1166
+ |---|---|---|
1167
+ | raffel:connected | — | Connected |
1168
+ | raffel:disconnected | — | Disconnected |
1169
+ | raffel:event | (procedure, payload) | Server event |
1170
+ | raffel:channel:subscribed | (channel, members?) | Joined channel |
1171
+ | raffel:channel:unsubscribed | (channel) | Left channel |
1172
+ | raffel:channel:event | (channel, event, data) | Channel event |
1173
+ | ws:reconnecting | (attempt, delay) | Reconnecting |
1174
+ | ws:error | (error) | WebSocket error |
1175
+
1176
+ ## Error Handling
1177
+
1178
+ \`\`\`typescript
1179
+ try {
1180
+ await client.call('users.get', { id: 999 });
1181
+ } catch (err) {
1182
+ if (err instanceof RaffelError) {
1183
+ err.code; // "NOT_FOUND"
1184
+ err.status; // 404
1185
+ err.procedure; // "users.get"
1186
+ err.details; // optional server details
1187
+ }
1188
+ }
1189
+ \`\`\`
1190
+
1191
+ ## Properties
1192
+
1193
+ - \`client.isConnected\` — boolean
1194
+ - \`client.raw\` — underlying RaffelTransport
1195
+ - \`client.rawWs\` — underlying ReckerWebSocket (null for non-WS transports)
1196
+
1197
+ ## Capability Matrix
1198
+
1199
+ | Method | WS | HTTP | TCP | UDP | JSON-RPC |
1200
+ |---|:---:|:---:|:---:|:---:|:---:|
1201
+ | call() | ✅ | ✅ | ✅ | ✅ | ✅ |
1202
+ | callStream() | ✅ | ✅ (SSE) | ✅ | ❌ | ❌ |
1203
+ | notify() | ✅ | ✅ | ✅ | ✅ | ✅ |
1204
+ | subscribe() | ✅ | ❌ | ✅ | ❌ | ❌ |
1205
+ | publish() | ✅ | ❌ | ✅ | ❌ | ❌ |
1206
+ | cancel() | ✅ | ❌ | ✅ | ❌ | ❌ |
1207
+
1208
+ ## Example
1209
+
1210
+ \`\`\`typescript
1211
+ const api = createRaffelClient('ws://api:3000', {
1212
+ channels: ['notifications'],
1213
+ channelHandlers: {
1214
+ notifications: (event, data) => showToast(data),
1215
+ },
1216
+ ws: { reconnect: true },
1217
+ });
1218
+ await api.connect();
1219
+
1220
+ const user = await api.call('auth.verify', { token });
1221
+ api.notify('presence.online', { userId: user.id });
1222
+ \`\`\`
1223
+ `;
@@ -1,5 +1,6 @@
1
1
  import { ReckerWebSocket } from '../websocket/client.js';
2
2
  import type { RaffelClientOptions, RaffelCallOptions, ChannelEventHandler } from './types.js';
3
+ import type { RaffelTransport } from './transport.js';
3
4
  type Listener = (...args: any[]) => void;
4
5
  declare class SimpleEmitter {
5
6
  private listeners;
@@ -9,30 +10,45 @@ declare class SimpleEmitter {
9
10
  emit(event: string, ...args: any[]): boolean;
10
11
  }
11
12
  export declare class RaffelClient extends SimpleEmitter {
12
- private ws;
13
+ private transport;
13
14
  private pendingCalls;
14
15
  private subscribedChannels;
15
16
  private idCounter;
16
17
  private defaultTimeout;
17
18
  private onEvent?;
19
+ private _onMessage?;
20
+ private url;
21
+ private options;
22
+ private _mode;
23
+ private _waiters;
24
+ private _messageBuffer;
25
+ private static readonly MAX_BUFFER_SIZE;
18
26
  constructor(url: string, options?: RaffelClientOptions);
27
+ private ensureTransport;
28
+ _setTransport(transport: RaffelTransport): void;
29
+ private wireTransportEvents;
19
30
  connect(): Promise<void>;
20
31
  close(code?: number, reason?: string): void;
21
32
  get isConnected(): boolean;
22
- get raw(): ReckerWebSocket;
33
+ get raw(): RaffelTransport;
34
+ get rawWs(): ReckerWebSocket | null;
23
35
  call<T = unknown>(procedure: string, payload?: unknown, options?: RaffelCallOptions): Promise<T>;
36
+ callStream<T = unknown>(procedure: string, payload?: unknown): AsyncIterable<T>;
24
37
  notify(procedure: string, payload?: unknown): void;
25
38
  subscribe(channel: string, handler?: ChannelEventHandler): void;
26
39
  unsubscribe(channel: string): void;
27
40
  publish(channel: string, event: string, data?: unknown): void;
41
+ sendRaw(data: unknown): void;
42
+ waitFor<T = any>(predicate: (msg: any) => boolean, timeoutMs?: number): Promise<T>;
43
+ get mode(): 'raw' | 'full';
28
44
  cancel(id: string): void;
29
- private handleMessage;
45
+ private executeWithTimeout;
46
+ private handleIncoming;
30
47
  private handleEnvelope;
31
48
  private handleChannelMessage;
32
49
  private nextId;
33
50
  private extractBaseId;
34
51
  private sendChannelSubscribe;
35
- private sendRaw;
36
52
  }
37
53
  export declare function createRaffelClient(url: string, options?: RaffelClientOptions): RaffelClient;
38
54
  export {};
@@ -1,5 +1,8 @@
1
- import { ReckerWebSocket } from '../websocket/client.js';
1
+ import { UnsupportedError } from '../core/errors.js';
2
2
  import { RaffelError } from './types.js';
3
+ import { TransportCapability, hasExecute, hasCapability } from './transport.js';
4
+ import { WsTransport } from './transport-ws.js';
5
+ import { createTransport, detectTransportType } from './transport-factory.js';
3
6
  class SimpleEmitter {
4
7
  listeners = new Map();
5
8
  on(event, listener) {
@@ -35,38 +38,61 @@ class SimpleEmitter {
35
38
  }
36
39
  }
37
40
  export class RaffelClient extends SimpleEmitter {
38
- ws;
41
+ transport;
39
42
  pendingCalls = new Map();
40
43
  subscribedChannels = new Map();
41
44
  idCounter = 0;
42
45
  defaultTimeout;
43
46
  onEvent;
47
+ _onMessage;
48
+ url;
49
+ options;
50
+ _mode;
51
+ _waiters = [];
52
+ _messageBuffer = [];
53
+ static MAX_BUFFER_SIZE = 100;
44
54
  constructor(url, options = {}) {
45
55
  super();
46
- const { channels, channelHandlers, onEvent, defaultTimeout, ...wsOptions } = options;
47
- this.defaultTimeout = defaultTimeout ?? 30_000;
48
- this.onEvent = onEvent;
49
- this.ws = new ReckerWebSocket(url, wsOptions);
50
- if (channels) {
51
- for (const ch of channels) {
52
- this.subscribedChannels.set(ch, channelHandlers?.[ch] ?? null);
56
+ this.url = url;
57
+ this.options = options;
58
+ this._mode = options.mode ?? 'full';
59
+ this.defaultTimeout = options.defaultTimeout ?? 30_000;
60
+ this.onEvent = options.onEvent;
61
+ this._onMessage = options.onMessage;
62
+ if (options.channels) {
63
+ for (const ch of options.channels) {
64
+ this.subscribedChannels.set(ch, options.channelHandlers?.[ch] ?? null);
53
65
  }
54
66
  }
55
- if (channelHandlers) {
56
- for (const [ch, handler] of Object.entries(channelHandlers)) {
67
+ if (options.channelHandlers) {
68
+ for (const [ch, handler] of Object.entries(options.channelHandlers)) {
57
69
  if (!this.subscribedChannels.has(ch)) {
58
70
  this.subscribedChannels.set(ch, handler);
59
71
  }
60
72
  }
61
73
  }
62
- this.ws.on('message', (msg) => this.handleMessage(msg));
63
- this.ws.on('open', () => {
74
+ }
75
+ async ensureTransport() {
76
+ if (this.transport)
77
+ return;
78
+ this.transport = await createTransport(this.url, this.options);
79
+ this.wireTransportEvents();
80
+ }
81
+ _setTransport(transport) {
82
+ this.transport = transport;
83
+ this.wireTransportEvents();
84
+ }
85
+ wireTransportEvents() {
86
+ this.transport.on('message', (data) => this.handleIncoming(data));
87
+ this.transport.on('connected', () => {
64
88
  this.emit('raffel:connected');
65
- for (const [channel] of this.subscribedChannels) {
66
- this.sendChannelSubscribe(channel);
89
+ if (this._mode === 'full') {
90
+ for (const [channel] of this.subscribedChannels) {
91
+ this.sendChannelSubscribe(channel);
92
+ }
67
93
  }
68
94
  });
69
- this.ws.on('close', (_code, _reason) => {
95
+ this.transport.on('disconnected', () => {
70
96
  this.emit('raffel:disconnected');
71
97
  for (const [id, pending] of this.pendingCalls) {
72
98
  if (pending.timer)
@@ -75,15 +101,16 @@ export class RaffelClient extends SimpleEmitter {
75
101
  }
76
102
  this.pendingCalls.clear();
77
103
  });
78
- this.ws.on('reconnecting', (attempt, delay) => {
104
+ this.transport.on('reconnecting', (attempt, delay) => {
79
105
  this.emit('ws:reconnecting', attempt, delay);
80
106
  });
81
- this.ws.on('error', (err) => {
107
+ this.transport.on('error', (err) => {
82
108
  this.emit('ws:error', err);
83
109
  });
84
110
  }
85
111
  async connect() {
86
- return this.ws.connect();
112
+ await this.ensureTransport();
113
+ return this.transport.connect();
87
114
  }
88
115
  close(code, reason) {
89
116
  for (const [id, pending] of this.pendingCalls) {
@@ -92,13 +119,23 @@ export class RaffelClient extends SimpleEmitter {
92
119
  pending.reject(new Error(`Connection closed while waiting for response to ${id}`));
93
120
  }
94
121
  this.pendingCalls.clear();
95
- this.ws.close(code, reason);
122
+ for (const waiter of this._waiters) {
123
+ clearTimeout(waiter.timer);
124
+ waiter.reject(new Error('Connection closed'));
125
+ }
126
+ this._waiters = [];
127
+ if (this.transport) {
128
+ this.transport.close(code, reason);
129
+ }
96
130
  }
97
131
  get isConnected() {
98
- return this.ws.isConnected;
132
+ return this.transport?.isConnected ?? false;
99
133
  }
100
134
  get raw() {
101
- return this.ws;
135
+ return this.transport;
136
+ }
137
+ get rawWs() {
138
+ return this.transport instanceof WsTransport ? this.transport.socket : null;
102
139
  }
103
140
  async call(procedure, payload, options) {
104
141
  const id = this.nextId();
@@ -110,6 +147,9 @@ export class RaffelClient extends SimpleEmitter {
110
147
  payload: payload ?? {},
111
148
  metadata: {},
112
149
  };
150
+ if (hasExecute(this.transport)) {
151
+ return this.executeWithTimeout(this.transport, envelope, timeout, procedure, options?.signal);
152
+ }
113
153
  return new Promise((resolve, reject) => {
114
154
  let timer = null;
115
155
  const cleanup = () => {
@@ -122,7 +162,7 @@ export class RaffelClient extends SimpleEmitter {
122
162
  };
123
163
  const onAbort = () => {
124
164
  cleanup();
125
- this.sendRaw({ id, type: 'cancel' });
165
+ this.transport.send({ id, type: 'cancel' });
126
166
  reject(new Error(`Call to ${procedure} was aborted`));
127
167
  };
128
168
  if (timeout > 0) {
@@ -139,9 +179,75 @@ export class RaffelClient extends SimpleEmitter {
139
179
  options.signal.addEventListener('abort', onAbort, { once: true });
140
180
  }
141
181
  this.pendingCalls.set(id, { resolve, reject, timer });
142
- this.ws.sendJSON(envelope);
182
+ this.transport.send(envelope);
143
183
  });
144
184
  }
185
+ async *callStream(procedure, payload) {
186
+ if (!hasCapability(this.transport, TransportCapability.STREAM)) {
187
+ throw new UnsupportedError(`callStream() not supported over ${detectTransportType(this.url, this.options)}. Use WebSocket, HTTP, or TCP.`);
188
+ }
189
+ if (hasExecute(this.transport) && this.transport.executeStream) {
190
+ const envelope = {
191
+ id: this.nextId(),
192
+ procedure,
193
+ type: 'stream:start',
194
+ payload: payload ?? {},
195
+ metadata: {},
196
+ };
197
+ yield* this.transport.executeStream(envelope);
198
+ return;
199
+ }
200
+ const id = this.nextId();
201
+ const envelope = {
202
+ id,
203
+ procedure,
204
+ type: 'stream:start',
205
+ payload: payload ?? {},
206
+ metadata: {},
207
+ };
208
+ const queue = [];
209
+ let resolve = null;
210
+ const push = (item) => {
211
+ queue.push(item);
212
+ if (resolve) {
213
+ resolve();
214
+ resolve = null;
215
+ }
216
+ };
217
+ const streamHandler = (data) => {
218
+ if (!data.id || !data.id.startsWith(id))
219
+ return;
220
+ if (data.type === 'stream:data') {
221
+ push({ value: data.payload });
222
+ }
223
+ else if (data.type === 'stream:end') {
224
+ push({ done: true });
225
+ this.transport.off('message', streamHandler);
226
+ }
227
+ else if (data.type === 'error') {
228
+ push({ error: new RaffelError(data.payload, procedure) });
229
+ this.transport.off('message', streamHandler);
230
+ }
231
+ };
232
+ this.transport.on('message', streamHandler);
233
+ this.transport.send(envelope);
234
+ try {
235
+ while (true) {
236
+ if (queue.length === 0) {
237
+ await new Promise((r) => { resolve = r; });
238
+ }
239
+ const item = queue.shift();
240
+ if (item.done)
241
+ return;
242
+ if (item.error)
243
+ throw item.error;
244
+ yield item.value;
245
+ }
246
+ }
247
+ finally {
248
+ this.transport.off('message', streamHandler);
249
+ }
250
+ }
145
251
  notify(procedure, payload) {
146
252
  const envelope = {
147
253
  id: this.nextId(),
@@ -150,26 +256,43 @@ export class RaffelClient extends SimpleEmitter {
150
256
  payload: payload ?? {},
151
257
  metadata: {},
152
258
  };
153
- this.ws.sendJSON(envelope);
259
+ if (hasExecute(this.transport)) {
260
+ this.transport.execute({
261
+ ...envelope,
262
+ type: 'notify',
263
+ }).catch(() => { });
264
+ }
265
+ else {
266
+ this.transport.send(envelope);
267
+ }
154
268
  }
155
269
  subscribe(channel, handler) {
270
+ if (!hasCapability(this.transport, TransportCapability.SUBSCRIBE)) {
271
+ throw new UnsupportedError(`subscribe() not supported over ${detectTransportType(this.url, this.options)}. Use WebSocket or TCP.`);
272
+ }
156
273
  this.subscribedChannels.set(channel, handler ?? null);
157
- if (this.ws.isConnected) {
274
+ if (this.transport.isConnected) {
158
275
  this.sendChannelSubscribe(channel);
159
276
  }
160
277
  }
161
278
  unsubscribe(channel) {
279
+ if (!hasCapability(this.transport, TransportCapability.SUBSCRIBE)) {
280
+ throw new UnsupportedError(`unsubscribe() not supported over ${detectTransportType(this.url, this.options)}. Use WebSocket or TCP.`);
281
+ }
162
282
  this.subscribedChannels.delete(channel);
163
- if (this.ws.isConnected) {
283
+ if (this.transport.isConnected) {
164
284
  const msg = {
165
285
  id: this.nextId(),
166
286
  type: 'unsubscribe',
167
287
  channel,
168
288
  };
169
- this.ws.sendJSON(msg);
289
+ this.transport.send(msg);
170
290
  }
171
291
  }
172
292
  publish(channel, event, data) {
293
+ if (!hasCapability(this.transport, TransportCapability.PUBLISH)) {
294
+ throw new UnsupportedError(`publish() not supported over ${detectTransportType(this.url, this.options)}. Use WebSocket or TCP.`);
295
+ }
173
296
  const msg = {
174
297
  id: this.nextId(),
175
298
  type: 'publish',
@@ -177,9 +300,31 @@ export class RaffelClient extends SimpleEmitter {
177
300
  event,
178
301
  data,
179
302
  };
180
- this.ws.sendJSON(msg);
303
+ this.transport.send(msg);
304
+ }
305
+ sendRaw(data) {
306
+ this.transport.send(data);
307
+ }
308
+ waitFor(predicate, timeoutMs) {
309
+ const existing = this._messageBuffer.find(predicate);
310
+ if (existing)
311
+ return Promise.resolve(existing);
312
+ const timeout = timeoutMs ?? this.defaultTimeout;
313
+ return new Promise((resolve, reject) => {
314
+ const timer = setTimeout(() => {
315
+ this._waiters = this._waiters.filter(w => w.timer !== timer);
316
+ reject(new Error('waitFor() timed out'));
317
+ }, timeout);
318
+ this._waiters.push({ predicate, resolve, reject, timer });
319
+ });
320
+ }
321
+ get mode() {
322
+ return this._mode;
181
323
  }
182
324
  cancel(id) {
325
+ if (!hasCapability(this.transport, TransportCapability.CANCEL)) {
326
+ throw new UnsupportedError(`cancel() not supported over ${detectTransportType(this.url, this.options)}. Use WebSocket or TCP.`);
327
+ }
183
328
  const pending = this.pendingCalls.get(id);
184
329
  if (pending) {
185
330
  if (pending.timer)
@@ -187,26 +332,53 @@ export class RaffelClient extends SimpleEmitter {
187
332
  pending.reject(new Error(`Call ${id} was cancelled`));
188
333
  this.pendingCalls.delete(id);
189
334
  }
190
- this.sendRaw({ id, type: 'cancel' });
335
+ this.transport.send({ id, type: 'cancel' });
191
336
  }
192
- handleMessage(msg) {
193
- if (typeof msg.data !== 'string')
194
- return;
195
- let parsed;
196
- try {
197
- parsed = JSON.parse(msg.data);
337
+ async executeWithTimeout(transport, envelope, timeout, procedure, signal) {
338
+ if (signal?.aborted) {
339
+ throw new Error(`Call to ${procedure} was aborted`);
340
+ }
341
+ const promises = [
342
+ transport.execute(envelope),
343
+ ];
344
+ if (timeout > 0) {
345
+ promises.push(new Promise((_, reject) => {
346
+ setTimeout(() => reject(new Error(`Call to ${procedure} timed out after ${timeout}ms`)), timeout);
347
+ }));
198
348
  }
199
- catch {
349
+ if (signal) {
350
+ promises.push(new Promise((_, reject) => {
351
+ signal.addEventListener('abort', () => reject(new Error(`Call to ${procedure} was aborted`)), { once: true });
352
+ }));
353
+ }
354
+ return Promise.race(promises);
355
+ }
356
+ handleIncoming(data) {
357
+ this._messageBuffer.push(data);
358
+ if (this._messageBuffer.length > RaffelClient.MAX_BUFFER_SIZE) {
359
+ this._messageBuffer.shift();
360
+ }
361
+ for (let i = this._waiters.length - 1; i >= 0; i--) {
362
+ const waiter = this._waiters[i];
363
+ if (waiter.predicate(data)) {
364
+ clearTimeout(waiter.timer);
365
+ this._waiters.splice(i, 1);
366
+ waiter.resolve(data);
367
+ }
368
+ }
369
+ if (this._mode === 'raw') {
370
+ this._onMessage?.(data);
371
+ this.emit('message', data);
200
372
  return;
201
373
  }
202
- if (parsed.channel) {
203
- this.handleChannelMessage(parsed);
374
+ if (data.channel) {
375
+ this.handleChannelMessage(data);
204
376
  }
205
- else if (parsed.procedure || parsed.type === 'cancel') {
206
- this.handleEnvelope(parsed);
377
+ else if (data.procedure || data.type === 'cancel' || data.type === 'response' || data.type === 'error') {
378
+ this.handleEnvelope(data);
207
379
  }
208
380
  else {
209
- this.emit('raffel:unknown', parsed);
381
+ this.emit('raffel:unknown', data);
210
382
  }
211
383
  }
212
384
  handleEnvelope(envelope) {
@@ -271,10 +443,7 @@ export class RaffelClient extends SimpleEmitter {
271
443
  type: 'subscribe',
272
444
  channel,
273
445
  };
274
- this.ws.sendJSON(msg);
275
- }
276
- sendRaw(data) {
277
- this.ws.sendJSON(data);
446
+ this.transport.send(msg);
278
447
  }
279
448
  }
280
449
  export function createRaffelClient(url, options) {
@@ -1,2 +1,9 @@
1
1
  export * from './client.js';
2
2
  export * from './types.js';
3
+ export * from './transport.js';
4
+ export * from './transport-ws.js';
5
+ export * from './transport-http.js';
6
+ export * from './transport-tcp.js';
7
+ export * from './transport-udp.js';
8
+ export * from './transport-jsonrpc.js';
9
+ export { detectTransportType, createTransport } from './transport-factory.js';
@@ -1,2 +1,9 @@
1
1
  export * from './client.js';
2
2
  export * from './types.js';
3
+ export * from './transport.js';
4
+ export * from './transport-ws.js';
5
+ export * from './transport-http.js';
6
+ export * from './transport-tcp.js';
7
+ export * from './transport-udp.js';
8
+ export * from './transport-jsonrpc.js';
9
+ export { detectTransportType, createTransport } from './transport-factory.js';
@@ -0,0 +1,4 @@
1
+ import type { RaffelTransport } from './transport.js';
2
+ import type { RaffelClientOptions, RaffelTransportType } from './types.js';
3
+ export declare function detectTransportType(url: string, options?: RaffelClientOptions): RaffelTransportType;
4
+ export declare function createTransport(url: string, options?: RaffelClientOptions): Promise<RaffelTransport>;