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 +20 -2
- package/dist/cli/handlers/network.js +2 -2
- package/dist/cli/tui/components/smart-input.d.ts +1 -0
- package/dist/mcp/resources/index.js +130 -0
- package/dist/raffel/client.d.ts +20 -4
- package/dist/raffel/client.js +215 -46
- 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 +32 -2
- package/dist/version.js +1 -1
- package/package.json +18 -18
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:
|
|
@@ -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
|
+
`;
|
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,45 @@ 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 _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():
|
|
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
|
|
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 {};
|
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,61 @@ 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
|
+
_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
|
-
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
this.
|
|
89
|
+
if (this._mode === 'full') {
|
|
90
|
+
for (const [channel] of this.subscribedChannels) {
|
|
91
|
+
this.sendChannelSubscribe(channel);
|
|
92
|
+
}
|
|
67
93
|
}
|
|
68
94
|
});
|
|
69
|
-
this.
|
|
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.
|
|
104
|
+
this.transport.on('reconnecting', (attempt, delay) => {
|
|
79
105
|
this.emit('ws:reconnecting', attempt, delay);
|
|
80
106
|
});
|
|
81
|
-
this.
|
|
107
|
+
this.transport.on('error', (err) => {
|
|
82
108
|
this.emit('ws:error', err);
|
|
83
109
|
});
|
|
84
110
|
}
|
|
85
111
|
async connect() {
|
|
86
|
-
|
|
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.
|
|
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.
|
|
132
|
+
return this.transport?.isConnected ?? false;
|
|
99
133
|
}
|
|
100
134
|
get raw() {
|
|
101
|
-
return this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
335
|
+
this.transport.send({ id, type: 'cancel' });
|
|
191
336
|
}
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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 (
|
|
203
|
-
this.handleChannelMessage(
|
|
374
|
+
if (data.channel) {
|
|
375
|
+
this.handleChannelMessage(data);
|
|
204
376
|
}
|
|
205
|
-
else if (
|
|
206
|
-
this.handleEnvelope(
|
|
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',
|
|
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.
|
|
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) {
|
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>;
|