recker 1.0.83 → 1.0.84-next.ca0ad49
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 +20 -2
- package/dist/mcp/resources/index.js +130 -0
- package/dist/raffel/client.d.ts +12 -4
- package/dist/raffel/client.js +164 -45
- package/dist/raffel/index.d.ts +7 -0
- package/dist/raffel/index.js +7 -0
- package/dist/raffel/transport-factory.d.ts +4 -0
- package/dist/raffel/transport-factory.js +52 -0
- package/dist/raffel/transport-http.d.ts +25 -0
- package/dist/raffel/transport-http.js +211 -0
- package/dist/raffel/transport-jsonrpc.d.ts +24 -0
- package/dist/raffel/transport-jsonrpc.js +139 -0
- package/dist/raffel/transport-tcp.d.ts +30 -0
- package/dist/raffel/transport-tcp.js +183 -0
- package/dist/raffel/transport-udp.d.ts +22 -0
- package/dist/raffel/transport-udp.js +104 -0
- package/dist/raffel/transport-ws.d.ts +19 -0
- package/dist/raffel/transport-ws.js +75 -0
- package/dist/raffel/transport.d.ts +29 -0
- package/dist/raffel/transport.js +14 -0
- package/dist/raffel/types.d.ts +30 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
### Multi-Protocol SDK for the AI Era
|
|
6
6
|
|
|
7
|
-
|
|
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:
|
|
@@ -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
|
+
`;
|
package/dist/raffel/client.d.ts
CHANGED
|
@@ -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,37 @@ declare class SimpleEmitter {
|
|
|
9
10
|
emit(event: string, ...args: any[]): boolean;
|
|
10
11
|
}
|
|
11
12
|
export declare class RaffelClient extends SimpleEmitter {
|
|
12
|
-
private
|
|
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 url;
|
|
20
|
+
private options;
|
|
18
21
|
constructor(url: string, options?: RaffelClientOptions);
|
|
22
|
+
private ensureTransport;
|
|
23
|
+
_setTransport(transport: RaffelTransport): void;
|
|
24
|
+
private wireTransportEvents;
|
|
19
25
|
connect(): Promise<void>;
|
|
20
26
|
close(code?: number, reason?: string): void;
|
|
21
27
|
get isConnected(): boolean;
|
|
22
|
-
get raw():
|
|
28
|
+
get raw(): RaffelTransport;
|
|
29
|
+
get rawWs(): ReckerWebSocket | null;
|
|
23
30
|
call<T = unknown>(procedure: string, payload?: unknown, options?: RaffelCallOptions): Promise<T>;
|
|
31
|
+
callStream<T = unknown>(procedure: string, payload?: unknown): AsyncIterable<T>;
|
|
24
32
|
notify(procedure: string, payload?: unknown): void;
|
|
25
33
|
subscribe(channel: string, handler?: ChannelEventHandler): void;
|
|
26
34
|
unsubscribe(channel: string): void;
|
|
27
35
|
publish(channel: string, event: string, data?: unknown): void;
|
|
28
36
|
cancel(id: string): void;
|
|
29
|
-
private
|
|
37
|
+
private executeWithTimeout;
|
|
38
|
+
private handleIncoming;
|
|
30
39
|
private handleEnvelope;
|
|
31
40
|
private handleChannelMessage;
|
|
32
41
|
private nextId;
|
|
33
42
|
private extractBaseId;
|
|
34
43
|
private sendChannelSubscribe;
|
|
35
|
-
private sendRaw;
|
|
36
44
|
}
|
|
37
45
|
export declare function createRaffelClient(url: string, options?: RaffelClientOptions): RaffelClient;
|
|
38
46
|
export {};
|
package/dist/raffel/client.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
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,52 @@ class SimpleEmitter {
|
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
export class RaffelClient extends SimpleEmitter {
|
|
38
|
-
|
|
41
|
+
transport;
|
|
39
42
|
pendingCalls = new Map();
|
|
40
43
|
subscribedChannels = new Map();
|
|
41
44
|
idCounter = 0;
|
|
42
45
|
defaultTimeout;
|
|
43
46
|
onEvent;
|
|
47
|
+
url;
|
|
48
|
+
options;
|
|
44
49
|
constructor(url, options = {}) {
|
|
45
50
|
super();
|
|
46
|
-
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
if (channels) {
|
|
51
|
-
for (const ch of channels) {
|
|
52
|
-
this.subscribedChannels.set(ch, channelHandlers?.[ch] ?? null);
|
|
51
|
+
this.url = url;
|
|
52
|
+
this.options = options;
|
|
53
|
+
this.defaultTimeout = options.defaultTimeout ?? 30_000;
|
|
54
|
+
this.onEvent = options.onEvent;
|
|
55
|
+
if (options.channels) {
|
|
56
|
+
for (const ch of options.channels) {
|
|
57
|
+
this.subscribedChannels.set(ch, options.channelHandlers?.[ch] ?? null);
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
|
-
if (channelHandlers) {
|
|
56
|
-
for (const [ch, handler] of Object.entries(channelHandlers)) {
|
|
60
|
+
if (options.channelHandlers) {
|
|
61
|
+
for (const [ch, handler] of Object.entries(options.channelHandlers)) {
|
|
57
62
|
if (!this.subscribedChannels.has(ch)) {
|
|
58
63
|
this.subscribedChannels.set(ch, handler);
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
}
|
|
68
|
+
async ensureTransport() {
|
|
69
|
+
if (this.transport)
|
|
70
|
+
return;
|
|
71
|
+
this.transport = await createTransport(this.url, this.options);
|
|
72
|
+
this.wireTransportEvents();
|
|
73
|
+
}
|
|
74
|
+
_setTransport(transport) {
|
|
75
|
+
this.transport = transport;
|
|
76
|
+
this.wireTransportEvents();
|
|
77
|
+
}
|
|
78
|
+
wireTransportEvents() {
|
|
79
|
+
this.transport.on('message', (data) => this.handleIncoming(data));
|
|
80
|
+
this.transport.on('connected', () => {
|
|
64
81
|
this.emit('raffel:connected');
|
|
65
82
|
for (const [channel] of this.subscribedChannels) {
|
|
66
83
|
this.sendChannelSubscribe(channel);
|
|
67
84
|
}
|
|
68
85
|
});
|
|
69
|
-
this.
|
|
86
|
+
this.transport.on('disconnected', () => {
|
|
70
87
|
this.emit('raffel:disconnected');
|
|
71
88
|
for (const [id, pending] of this.pendingCalls) {
|
|
72
89
|
if (pending.timer)
|
|
@@ -75,15 +92,16 @@ export class RaffelClient extends SimpleEmitter {
|
|
|
75
92
|
}
|
|
76
93
|
this.pendingCalls.clear();
|
|
77
94
|
});
|
|
78
|
-
this.
|
|
95
|
+
this.transport.on('reconnecting', (attempt, delay) => {
|
|
79
96
|
this.emit('ws:reconnecting', attempt, delay);
|
|
80
97
|
});
|
|
81
|
-
this.
|
|
98
|
+
this.transport.on('error', (err) => {
|
|
82
99
|
this.emit('ws:error', err);
|
|
83
100
|
});
|
|
84
101
|
}
|
|
85
102
|
async connect() {
|
|
86
|
-
|
|
103
|
+
await this.ensureTransport();
|
|
104
|
+
return this.transport.connect();
|
|
87
105
|
}
|
|
88
106
|
close(code, reason) {
|
|
89
107
|
for (const [id, pending] of this.pendingCalls) {
|
|
@@ -92,13 +110,18 @@ export class RaffelClient extends SimpleEmitter {
|
|
|
92
110
|
pending.reject(new Error(`Connection closed while waiting for response to ${id}`));
|
|
93
111
|
}
|
|
94
112
|
this.pendingCalls.clear();
|
|
95
|
-
this.
|
|
113
|
+
if (this.transport) {
|
|
114
|
+
this.transport.close(code, reason);
|
|
115
|
+
}
|
|
96
116
|
}
|
|
97
117
|
get isConnected() {
|
|
98
|
-
return this.
|
|
118
|
+
return this.transport?.isConnected ?? false;
|
|
99
119
|
}
|
|
100
120
|
get raw() {
|
|
101
|
-
return this.
|
|
121
|
+
return this.transport;
|
|
122
|
+
}
|
|
123
|
+
get rawWs() {
|
|
124
|
+
return this.transport instanceof WsTransport ? this.transport.socket : null;
|
|
102
125
|
}
|
|
103
126
|
async call(procedure, payload, options) {
|
|
104
127
|
const id = this.nextId();
|
|
@@ -110,6 +133,9 @@ export class RaffelClient extends SimpleEmitter {
|
|
|
110
133
|
payload: payload ?? {},
|
|
111
134
|
metadata: {},
|
|
112
135
|
};
|
|
136
|
+
if (hasExecute(this.transport)) {
|
|
137
|
+
return this.executeWithTimeout(this.transport, envelope, timeout, procedure, options?.signal);
|
|
138
|
+
}
|
|
113
139
|
return new Promise((resolve, reject) => {
|
|
114
140
|
let timer = null;
|
|
115
141
|
const cleanup = () => {
|
|
@@ -122,7 +148,7 @@ export class RaffelClient extends SimpleEmitter {
|
|
|
122
148
|
};
|
|
123
149
|
const onAbort = () => {
|
|
124
150
|
cleanup();
|
|
125
|
-
this.
|
|
151
|
+
this.transport.send({ id, type: 'cancel' });
|
|
126
152
|
reject(new Error(`Call to ${procedure} was aborted`));
|
|
127
153
|
};
|
|
128
154
|
if (timeout > 0) {
|
|
@@ -139,9 +165,75 @@ export class RaffelClient extends SimpleEmitter {
|
|
|
139
165
|
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
140
166
|
}
|
|
141
167
|
this.pendingCalls.set(id, { resolve, reject, timer });
|
|
142
|
-
this.
|
|
168
|
+
this.transport.send(envelope);
|
|
143
169
|
});
|
|
144
170
|
}
|
|
171
|
+
async *callStream(procedure, payload) {
|
|
172
|
+
if (!hasCapability(this.transport, TransportCapability.STREAM)) {
|
|
173
|
+
throw new UnsupportedError(`callStream() not supported over ${detectTransportType(this.url, this.options)}. Use WebSocket, HTTP, or TCP.`);
|
|
174
|
+
}
|
|
175
|
+
if (hasExecute(this.transport) && this.transport.executeStream) {
|
|
176
|
+
const envelope = {
|
|
177
|
+
id: this.nextId(),
|
|
178
|
+
procedure,
|
|
179
|
+
type: 'stream:start',
|
|
180
|
+
payload: payload ?? {},
|
|
181
|
+
metadata: {},
|
|
182
|
+
};
|
|
183
|
+
yield* this.transport.executeStream(envelope);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const id = this.nextId();
|
|
187
|
+
const envelope = {
|
|
188
|
+
id,
|
|
189
|
+
procedure,
|
|
190
|
+
type: 'stream:start',
|
|
191
|
+
payload: payload ?? {},
|
|
192
|
+
metadata: {},
|
|
193
|
+
};
|
|
194
|
+
const queue = [];
|
|
195
|
+
let resolve = null;
|
|
196
|
+
const push = (item) => {
|
|
197
|
+
queue.push(item);
|
|
198
|
+
if (resolve) {
|
|
199
|
+
resolve();
|
|
200
|
+
resolve = null;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const streamHandler = (data) => {
|
|
204
|
+
if (!data.id || !data.id.startsWith(id))
|
|
205
|
+
return;
|
|
206
|
+
if (data.type === 'stream:data') {
|
|
207
|
+
push({ value: data.payload });
|
|
208
|
+
}
|
|
209
|
+
else if (data.type === 'stream:end') {
|
|
210
|
+
push({ done: true });
|
|
211
|
+
this.transport.off('message', streamHandler);
|
|
212
|
+
}
|
|
213
|
+
else if (data.type === 'error') {
|
|
214
|
+
push({ error: new RaffelError(data.payload, procedure) });
|
|
215
|
+
this.transport.off('message', streamHandler);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
this.transport.on('message', streamHandler);
|
|
219
|
+
this.transport.send(envelope);
|
|
220
|
+
try {
|
|
221
|
+
while (true) {
|
|
222
|
+
if (queue.length === 0) {
|
|
223
|
+
await new Promise((r) => { resolve = r; });
|
|
224
|
+
}
|
|
225
|
+
const item = queue.shift();
|
|
226
|
+
if (item.done)
|
|
227
|
+
return;
|
|
228
|
+
if (item.error)
|
|
229
|
+
throw item.error;
|
|
230
|
+
yield item.value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
this.transport.off('message', streamHandler);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
145
237
|
notify(procedure, payload) {
|
|
146
238
|
const envelope = {
|
|
147
239
|
id: this.nextId(),
|
|
@@ -150,26 +242,43 @@ export class RaffelClient extends SimpleEmitter {
|
|
|
150
242
|
payload: payload ?? {},
|
|
151
243
|
metadata: {},
|
|
152
244
|
};
|
|
153
|
-
this.
|
|
245
|
+
if (hasExecute(this.transport)) {
|
|
246
|
+
this.transport.execute({
|
|
247
|
+
...envelope,
|
|
248
|
+
type: 'notify',
|
|
249
|
+
}).catch(() => { });
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
this.transport.send(envelope);
|
|
253
|
+
}
|
|
154
254
|
}
|
|
155
255
|
subscribe(channel, handler) {
|
|
256
|
+
if (!hasCapability(this.transport, TransportCapability.SUBSCRIBE)) {
|
|
257
|
+
throw new UnsupportedError(`subscribe() not supported over ${detectTransportType(this.url, this.options)}. Use WebSocket or TCP.`);
|
|
258
|
+
}
|
|
156
259
|
this.subscribedChannels.set(channel, handler ?? null);
|
|
157
|
-
if (this.
|
|
260
|
+
if (this.transport.isConnected) {
|
|
158
261
|
this.sendChannelSubscribe(channel);
|
|
159
262
|
}
|
|
160
263
|
}
|
|
161
264
|
unsubscribe(channel) {
|
|
265
|
+
if (!hasCapability(this.transport, TransportCapability.SUBSCRIBE)) {
|
|
266
|
+
throw new UnsupportedError(`unsubscribe() not supported over ${detectTransportType(this.url, this.options)}. Use WebSocket or TCP.`);
|
|
267
|
+
}
|
|
162
268
|
this.subscribedChannels.delete(channel);
|
|
163
|
-
if (this.
|
|
269
|
+
if (this.transport.isConnected) {
|
|
164
270
|
const msg = {
|
|
165
271
|
id: this.nextId(),
|
|
166
272
|
type: 'unsubscribe',
|
|
167
273
|
channel,
|
|
168
274
|
};
|
|
169
|
-
this.
|
|
275
|
+
this.transport.send(msg);
|
|
170
276
|
}
|
|
171
277
|
}
|
|
172
278
|
publish(channel, event, data) {
|
|
279
|
+
if (!hasCapability(this.transport, TransportCapability.PUBLISH)) {
|
|
280
|
+
throw new UnsupportedError(`publish() not supported over ${detectTransportType(this.url, this.options)}. Use WebSocket or TCP.`);
|
|
281
|
+
}
|
|
173
282
|
const msg = {
|
|
174
283
|
id: this.nextId(),
|
|
175
284
|
type: 'publish',
|
|
@@ -177,9 +286,12 @@ export class RaffelClient extends SimpleEmitter {
|
|
|
177
286
|
event,
|
|
178
287
|
data,
|
|
179
288
|
};
|
|
180
|
-
this.
|
|
289
|
+
this.transport.send(msg);
|
|
181
290
|
}
|
|
182
291
|
cancel(id) {
|
|
292
|
+
if (!hasCapability(this.transport, TransportCapability.CANCEL)) {
|
|
293
|
+
throw new UnsupportedError(`cancel() not supported over ${detectTransportType(this.url, this.options)}. Use WebSocket or TCP.`);
|
|
294
|
+
}
|
|
183
295
|
const pending = this.pendingCalls.get(id);
|
|
184
296
|
if (pending) {
|
|
185
297
|
if (pending.timer)
|
|
@@ -187,26 +299,36 @@ export class RaffelClient extends SimpleEmitter {
|
|
|
187
299
|
pending.reject(new Error(`Call ${id} was cancelled`));
|
|
188
300
|
this.pendingCalls.delete(id);
|
|
189
301
|
}
|
|
190
|
-
this.
|
|
302
|
+
this.transport.send({ id, type: 'cancel' });
|
|
191
303
|
}
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
let parsed;
|
|
196
|
-
try {
|
|
197
|
-
parsed = JSON.parse(msg.data);
|
|
304
|
+
async executeWithTimeout(transport, envelope, timeout, procedure, signal) {
|
|
305
|
+
if (signal?.aborted) {
|
|
306
|
+
throw new Error(`Call to ${procedure} was aborted`);
|
|
198
307
|
}
|
|
199
|
-
|
|
200
|
-
|
|
308
|
+
const promises = [
|
|
309
|
+
transport.execute(envelope),
|
|
310
|
+
];
|
|
311
|
+
if (timeout > 0) {
|
|
312
|
+
promises.push(new Promise((_, reject) => {
|
|
313
|
+
setTimeout(() => reject(new Error(`Call to ${procedure} timed out after ${timeout}ms`)), timeout);
|
|
314
|
+
}));
|
|
201
315
|
}
|
|
202
|
-
if (
|
|
203
|
-
|
|
316
|
+
if (signal) {
|
|
317
|
+
promises.push(new Promise((_, reject) => {
|
|
318
|
+
signal.addEventListener('abort', () => reject(new Error(`Call to ${procedure} was aborted`)), { once: true });
|
|
319
|
+
}));
|
|
204
320
|
}
|
|
205
|
-
|
|
206
|
-
|
|
321
|
+
return Promise.race(promises);
|
|
322
|
+
}
|
|
323
|
+
handleIncoming(data) {
|
|
324
|
+
if (data.channel) {
|
|
325
|
+
this.handleChannelMessage(data);
|
|
326
|
+
}
|
|
327
|
+
else if (data.procedure || data.type === 'cancel') {
|
|
328
|
+
this.handleEnvelope(data);
|
|
207
329
|
}
|
|
208
330
|
else {
|
|
209
|
-
this.emit('raffel:unknown',
|
|
331
|
+
this.emit('raffel:unknown', data);
|
|
210
332
|
}
|
|
211
333
|
}
|
|
212
334
|
handleEnvelope(envelope) {
|
|
@@ -271,10 +393,7 @@ export class RaffelClient extends SimpleEmitter {
|
|
|
271
393
|
type: 'subscribe',
|
|
272
394
|
channel,
|
|
273
395
|
};
|
|
274
|
-
this.
|
|
275
|
-
}
|
|
276
|
-
sendRaw(data) {
|
|
277
|
-
this.ws.sendJSON(data);
|
|
396
|
+
this.transport.send(msg);
|
|
278
397
|
}
|
|
279
398
|
}
|
|
280
399
|
export function createRaffelClient(url, options) {
|
package/dist/raffel/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/raffel/index.js
CHANGED
|
@@ -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>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { WsTransport } from './transport-ws.js';
|
|
2
|
+
export function detectTransportType(url, options) {
|
|
3
|
+
if (options?.transport)
|
|
4
|
+
return options.transport;
|
|
5
|
+
const lower = url.toLowerCase();
|
|
6
|
+
if (lower.startsWith('ws://') || lower.startsWith('wss://')) {
|
|
7
|
+
return 'websocket';
|
|
8
|
+
}
|
|
9
|
+
if (lower.startsWith('tcp://')) {
|
|
10
|
+
return 'tcp';
|
|
11
|
+
}
|
|
12
|
+
if (lower.startsWith('udp://')) {
|
|
13
|
+
return 'udp';
|
|
14
|
+
}
|
|
15
|
+
if (lower.startsWith('http://') || lower.startsWith('https://')) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(url);
|
|
18
|
+
if (parsed.pathname === '/rpc' || parsed.pathname.endsWith('/rpc')) {
|
|
19
|
+
return 'jsonrpc';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
}
|
|
24
|
+
return 'http';
|
|
25
|
+
}
|
|
26
|
+
return 'websocket';
|
|
27
|
+
}
|
|
28
|
+
export async function createTransport(url, options = {}) {
|
|
29
|
+
const type = detectTransportType(url, options);
|
|
30
|
+
switch (type) {
|
|
31
|
+
case 'websocket':
|
|
32
|
+
return new WsTransport(url, options.ws);
|
|
33
|
+
case 'http': {
|
|
34
|
+
const { HttpTransport } = await import('./transport-http.js');
|
|
35
|
+
return new HttpTransport(url, options.http);
|
|
36
|
+
}
|
|
37
|
+
case 'tcp': {
|
|
38
|
+
const { TcpTransport } = await import('./transport-tcp.js');
|
|
39
|
+
return new TcpTransport(url, options.tcp);
|
|
40
|
+
}
|
|
41
|
+
case 'udp': {
|
|
42
|
+
const { UdpTransport } = await import('./transport-udp.js');
|
|
43
|
+
return new UdpTransport(url, options.udp);
|
|
44
|
+
}
|
|
45
|
+
case 'jsonrpc': {
|
|
46
|
+
const { JsonRpcTransport } = await import('./transport-jsonrpc.js');
|
|
47
|
+
return new JsonRpcTransport(url, options.jsonrpc);
|
|
48
|
+
}
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`Unknown transport type: ${type}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RaffelExecutableTransport, RaffelTransportEvent } from './transport.js';
|
|
2
|
+
import type { RaffelEnvelope, RaffelHttpOptions } from './types.js';
|
|
3
|
+
type Listener = (...args: any[]) => void;
|
|
4
|
+
export declare class HttpTransport implements RaffelExecutableTransport {
|
|
5
|
+
readonly capabilities: number;
|
|
6
|
+
private client;
|
|
7
|
+
private connected;
|
|
8
|
+
private listeners;
|
|
9
|
+
private baseUrl;
|
|
10
|
+
constructor(url: string, options?: RaffelHttpOptions);
|
|
11
|
+
get isConnected(): boolean;
|
|
12
|
+
connect(): Promise<void>;
|
|
13
|
+
close(): void;
|
|
14
|
+
send(_data: unknown): void;
|
|
15
|
+
execute(envelope: RaffelEnvelope): Promise<unknown>;
|
|
16
|
+
executeStream(envelope: RaffelEnvelope): AsyncIterable<unknown>;
|
|
17
|
+
private parseSSE;
|
|
18
|
+
private getReader;
|
|
19
|
+
private parseBody;
|
|
20
|
+
on(event: RaffelTransportEvent, listener: Listener): void;
|
|
21
|
+
off(event: RaffelTransportEvent, listener: Listener): void;
|
|
22
|
+
once(event: RaffelTransportEvent, listener: Listener): void;
|
|
23
|
+
private emit;
|
|
24
|
+
}
|
|
25
|
+
export {};
|