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.
@@ -0,0 +1,211 @@
1
+ import { Client } from '../core/client.js';
2
+ import { TransportCapability } from './transport.js';
3
+ import { RaffelError } from './types.js';
4
+ const HTTP_CAPABILITIES = TransportCapability.CALL |
5
+ TransportCapability.NOTIFY |
6
+ TransportCapability.STREAM;
7
+ export class HttpTransport {
8
+ capabilities = HTTP_CAPABILITIES;
9
+ client;
10
+ connected = false;
11
+ listeners = new Map();
12
+ baseUrl;
13
+ constructor(url, options = {}) {
14
+ this.baseUrl = url;
15
+ this.client = new Client({
16
+ baseUrl: url,
17
+ timeout: options.timeout ?? 30_000,
18
+ headers: {
19
+ 'content-type': 'application/json',
20
+ 'accept': 'application/json',
21
+ ...options.headers,
22
+ },
23
+ });
24
+ }
25
+ get isConnected() {
26
+ return this.connected;
27
+ }
28
+ async connect() {
29
+ this.connected = true;
30
+ this.emit('connected');
31
+ }
32
+ close() {
33
+ this.connected = false;
34
+ this.emit('disconnected');
35
+ }
36
+ send(_data) {
37
+ }
38
+ async execute(envelope) {
39
+ const { procedure, type, payload, metadata } = envelope;
40
+ if (type === 'notify' || type === 'event') {
41
+ await this.client.post(`/events/${procedure}`, {
42
+ body: JSON.stringify(payload ?? {}),
43
+ throwHttpErrors: false,
44
+ });
45
+ return undefined;
46
+ }
47
+ const response = await this.client.post(`/${procedure}`, {
48
+ body: JSON.stringify(payload ?? {}),
49
+ headers: metadata,
50
+ throwHttpErrors: false,
51
+ });
52
+ const status = response.status;
53
+ if (status >= 400) {
54
+ const body = await this.parseBody(response);
55
+ if (body?.error) {
56
+ throw new RaffelError(body.error, procedure);
57
+ }
58
+ throw new RaffelError({
59
+ code: 'INTERNAL_ERROR',
60
+ status: status,
61
+ message: `HTTP ${status}`,
62
+ }, procedure);
63
+ }
64
+ return this.parseBody(response);
65
+ }
66
+ async *executeStream(envelope) {
67
+ const { procedure, payload } = envelope;
68
+ const params = new URLSearchParams();
69
+ if (payload && typeof payload === 'object') {
70
+ for (const [key, value] of Object.entries(payload)) {
71
+ params.set(key, typeof value === 'string' ? value : JSON.stringify(value));
72
+ }
73
+ }
74
+ const query = params.toString();
75
+ const url = `/streams/${procedure}${query ? `?${query}` : ''}`;
76
+ const response = await this.client.get(url, {
77
+ headers: { accept: 'text/event-stream' },
78
+ throwHttpErrors: false,
79
+ });
80
+ const body = response.read();
81
+ if (!body)
82
+ return;
83
+ yield* this.parseSSE(body, procedure);
84
+ }
85
+ async *parseSSE(body, procedure) {
86
+ const reader = this.getReader(body);
87
+ if (!reader)
88
+ return;
89
+ const decoder = new TextDecoder();
90
+ let buffer = '';
91
+ let currentEvent = '';
92
+ let currentData = '';
93
+ const processLine = function* (line) {
94
+ if (line === '') {
95
+ if (currentData) {
96
+ yield { event: currentEvent || 'data', data: currentData };
97
+ currentEvent = '';
98
+ currentData = '';
99
+ }
100
+ }
101
+ else if (line.startsWith('event: ')) {
102
+ currentEvent = line.slice(7);
103
+ }
104
+ else if (line.startsWith('data: ')) {
105
+ currentData += (currentData ? '\n' : '') + line.slice(6);
106
+ }
107
+ };
108
+ try {
109
+ while (true) {
110
+ const { done, value } = await reader.read();
111
+ if (done)
112
+ break;
113
+ buffer += typeof value === 'string' ? value : decoder.decode(value, { stream: true });
114
+ const lines = buffer.split('\n');
115
+ buffer = lines.pop() ?? '';
116
+ for (const line of lines) {
117
+ for (const event of processLine(line)) {
118
+ if (event.event === 'end')
119
+ return;
120
+ if (event.event === 'error') {
121
+ let parsed;
122
+ try {
123
+ parsed = JSON.parse(event.data);
124
+ }
125
+ catch {
126
+ parsed = { code: 'INTERNAL_ERROR', message: event.data, status: 500 };
127
+ }
128
+ throw new RaffelError(parsed, procedure);
129
+ }
130
+ if (event.event === 'data') {
131
+ try {
132
+ yield JSON.parse(event.data);
133
+ }
134
+ catch {
135
+ yield event.data;
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ finally {
143
+ if ('cancel' in reader) {
144
+ await reader.cancel();
145
+ }
146
+ }
147
+ }
148
+ getReader(body) {
149
+ if (body && typeof body.getReader === 'function') {
150
+ return body.getReader();
151
+ }
152
+ if (body && Symbol.asyncIterator in body) {
153
+ return asyncIterableToReader(body);
154
+ }
155
+ return null;
156
+ }
157
+ async parseBody(response) {
158
+ if (typeof response.json === 'function') {
159
+ try {
160
+ return await response.json();
161
+ }
162
+ catch {
163
+ return undefined;
164
+ }
165
+ }
166
+ const text = typeof response.text === 'function' ? await response.text() : String(response.body ?? '');
167
+ try {
168
+ return JSON.parse(text);
169
+ }
170
+ catch {
171
+ return undefined;
172
+ }
173
+ }
174
+ on(event, listener) {
175
+ const set = this.listeners.get(event) ?? new Set();
176
+ set.add(listener);
177
+ this.listeners.set(event, set);
178
+ }
179
+ off(event, listener) {
180
+ const set = this.listeners.get(event);
181
+ if (set) {
182
+ set.delete(listener);
183
+ if (set.size === 0)
184
+ this.listeners.delete(event);
185
+ }
186
+ }
187
+ once(event, listener) {
188
+ const wrapped = (...args) => {
189
+ this.off(event, wrapped);
190
+ listener(...args);
191
+ };
192
+ this.on(event, wrapped);
193
+ }
194
+ emit(event, ...args) {
195
+ const set = this.listeners.get(event);
196
+ if (!set)
197
+ return;
198
+ for (const listener of [...set]) {
199
+ listener(...args);
200
+ }
201
+ }
202
+ }
203
+ function asyncIterableToReader(iterable) {
204
+ const iterator = iterable[Symbol.asyncIterator]();
205
+ return {
206
+ async read() {
207
+ const { done, value } = await iterator.next();
208
+ return { done: done ?? false, value };
209
+ },
210
+ };
211
+ }
@@ -0,0 +1,24 @@
1
+ import type { RaffelExecutableTransport, RaffelTransportEvent } from './transport.js';
2
+ import type { RaffelEnvelope, RaffelJsonRpcOptions } from './types.js';
3
+ type Listener = (...args: any[]) => void;
4
+ export declare class JsonRpcTransport implements RaffelExecutableTransport {
5
+ readonly capabilities: number;
6
+ private client;
7
+ private connected;
8
+ private listeners;
9
+ private rpcPath;
10
+ private idCounter;
11
+ constructor(url: string, options?: RaffelJsonRpcOptions);
12
+ get isConnected(): boolean;
13
+ connect(): Promise<void>;
14
+ close(): void;
15
+ send(_data: unknown): void;
16
+ execute(envelope: RaffelEnvelope): Promise<unknown>;
17
+ private rpcCodeToStatus;
18
+ private parseBody;
19
+ on(event: RaffelTransportEvent, listener: Listener): void;
20
+ off(event: RaffelTransportEvent, listener: Listener): void;
21
+ once(event: RaffelTransportEvent, listener: Listener): void;
22
+ private emit;
23
+ }
24
+ export {};
@@ -0,0 +1,139 @@
1
+ import { Client } from '../core/client.js';
2
+ import { TransportCapability } from './transport.js';
3
+ import { RaffelError } from './types.js';
4
+ const JSONRPC_CAPABILITIES = TransportCapability.CALL |
5
+ TransportCapability.NOTIFY;
6
+ const JSONRPC_ERROR_MAP = {
7
+ [-32700]: 'PARSE_ERROR',
8
+ [-32600]: 'INVALID_ARGUMENT',
9
+ [-32601]: 'NOT_FOUND',
10
+ [-32602]: 'VALIDATION_ERROR',
11
+ [-32603]: 'INTERNAL_ERROR',
12
+ [-32001]: 'UNAUTHENTICATED',
13
+ [-32002]: 'RATE_LIMITED',
14
+ };
15
+ export class JsonRpcTransport {
16
+ capabilities = JSONRPC_CAPABILITIES;
17
+ client;
18
+ connected = false;
19
+ listeners = new Map();
20
+ rpcPath;
21
+ idCounter = 0;
22
+ constructor(url, options = {}) {
23
+ this.rpcPath = options.path ?? '/rpc';
24
+ let baseUrl = url;
25
+ if (baseUrl.endsWith('/rpc')) {
26
+ baseUrl = baseUrl.slice(0, -4);
27
+ }
28
+ else if (baseUrl.endsWith('/rpc/')) {
29
+ baseUrl = baseUrl.slice(0, -5);
30
+ }
31
+ this.client = new Client({
32
+ baseUrl,
33
+ headers: {
34
+ 'content-type': 'application/json',
35
+ 'accept': 'application/json',
36
+ ...options.headers,
37
+ },
38
+ });
39
+ }
40
+ get isConnected() {
41
+ return this.connected;
42
+ }
43
+ async connect() {
44
+ this.connected = true;
45
+ this.emit('connected');
46
+ }
47
+ close() {
48
+ this.connected = false;
49
+ this.emit('disconnected');
50
+ }
51
+ send(_data) {
52
+ }
53
+ async execute(envelope) {
54
+ const { procedure, type, payload } = envelope;
55
+ const isNotification = type === 'notify' || type === 'event';
56
+ const body = {
57
+ jsonrpc: '2.0',
58
+ method: procedure,
59
+ params: payload ?? {},
60
+ };
61
+ if (!isNotification) {
62
+ body.id = ++this.idCounter;
63
+ }
64
+ const response = await this.client.post(this.rpcPath, {
65
+ body: JSON.stringify(body),
66
+ });
67
+ if (isNotification) {
68
+ return undefined;
69
+ }
70
+ const result = await this.parseBody(response);
71
+ if (result?.error) {
72
+ const rpcError = result.error;
73
+ const code = JSONRPC_ERROR_MAP[rpcError.code] ?? 'INTERNAL_ERROR';
74
+ throw new RaffelError({
75
+ code,
76
+ status: this.rpcCodeToStatus(rpcError.code),
77
+ message: rpcError.message ?? 'JSON-RPC error',
78
+ details: rpcError.data,
79
+ }, procedure);
80
+ }
81
+ return result?.result;
82
+ }
83
+ rpcCodeToStatus(code) {
84
+ switch (code) {
85
+ case -32700: return 400;
86
+ case -32600: return 400;
87
+ case -32601: return 404;
88
+ case -32602: return 400;
89
+ case -32001: return 401;
90
+ case -32002: return 429;
91
+ default: return 500;
92
+ }
93
+ }
94
+ async parseBody(response) {
95
+ if (typeof response.json === 'function') {
96
+ try {
97
+ return await response.json();
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ }
103
+ const text = typeof response.text === 'function' ? await response.text() : String(response.body ?? '');
104
+ try {
105
+ return JSON.parse(text);
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ }
111
+ on(event, listener) {
112
+ const set = this.listeners.get(event) ?? new Set();
113
+ set.add(listener);
114
+ this.listeners.set(event, set);
115
+ }
116
+ off(event, listener) {
117
+ const set = this.listeners.get(event);
118
+ if (set) {
119
+ set.delete(listener);
120
+ if (set.size === 0)
121
+ this.listeners.delete(event);
122
+ }
123
+ }
124
+ once(event, listener) {
125
+ const wrapped = (...args) => {
126
+ this.off(event, wrapped);
127
+ listener(...args);
128
+ };
129
+ this.on(event, wrapped);
130
+ }
131
+ emit(event, ...args) {
132
+ const set = this.listeners.get(event);
133
+ if (!set)
134
+ return;
135
+ for (const listener of [...set]) {
136
+ listener(...args);
137
+ }
138
+ }
139
+ }
@@ -0,0 +1,30 @@
1
+ import type { RaffelTransport, RaffelTransportEvent } from './transport.js';
2
+ import type { RaffelTcpOptions } from './types.js';
3
+ type Listener = (...args: any[]) => void;
4
+ export declare class TcpTransport implements RaffelTransport {
5
+ readonly capabilities: number;
6
+ private socket;
7
+ private connected;
8
+ private listeners;
9
+ private host;
10
+ private port;
11
+ private options;
12
+ private buffer;
13
+ private reconnectAttempts;
14
+ private reconnectTimer;
15
+ private closed;
16
+ constructor(url: string, options?: RaffelTcpOptions);
17
+ get isConnected(): boolean;
18
+ connect(): Promise<void>;
19
+ private _connect;
20
+ close(): void;
21
+ send(data: unknown): void;
22
+ on(event: RaffelTransportEvent, listener: Listener): void;
23
+ off(event: RaffelTransportEvent, listener: Listener): void;
24
+ once(event: RaffelTransportEvent, listener: Listener): void;
25
+ private emit;
26
+ private onData;
27
+ private onClose;
28
+ private attemptReconnect;
29
+ }
30
+ export {};
@@ -0,0 +1,183 @@
1
+ import { createConnection } from 'node:net';
2
+ import { TransportCapability } from './transport.js';
3
+ const MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
4
+ const ALL_CAPABILITIES = TransportCapability.CALL |
5
+ TransportCapability.NOTIFY |
6
+ TransportCapability.SUBSCRIBE |
7
+ TransportCapability.PUBLISH |
8
+ TransportCapability.CANCEL |
9
+ TransportCapability.STREAM;
10
+ export class TcpTransport {
11
+ capabilities = ALL_CAPABILITIES;
12
+ socket = null;
13
+ connected = false;
14
+ listeners = new Map();
15
+ host;
16
+ port;
17
+ options;
18
+ buffer = Buffer.alloc(0);
19
+ reconnectAttempts = 0;
20
+ reconnectTimer = null;
21
+ closed = false;
22
+ constructor(url, options = {}) {
23
+ const parsed = new URL(url.replace('tcp://', 'http://'));
24
+ this.host = parsed.hostname;
25
+ this.port = parseInt(parsed.port, 10) || 9000;
26
+ this.options = options;
27
+ }
28
+ get isConnected() {
29
+ return this.connected;
30
+ }
31
+ async connect() {
32
+ this.closed = false;
33
+ return this._connect();
34
+ }
35
+ _connect() {
36
+ return new Promise((resolve, reject) => {
37
+ this.socket = createConnection({
38
+ host: this.host,
39
+ port: this.port,
40
+ });
41
+ const noDelay = this.options.noDelay ?? true;
42
+ this.socket.setNoDelay(noDelay);
43
+ const keepAlive = this.options.keepAlive ?? true;
44
+ if (keepAlive) {
45
+ this.socket.setKeepAlive(true, this.options.keepAliveInterval ?? 30_000);
46
+ }
47
+ const onConnect = () => {
48
+ this.connected = true;
49
+ this.reconnectAttempts = 0;
50
+ this.buffer = Buffer.alloc(0);
51
+ cleanup();
52
+ this.emit('connected');
53
+ resolve();
54
+ };
55
+ const onError = (err) => {
56
+ cleanup();
57
+ if (!this.connected) {
58
+ reject(err);
59
+ }
60
+ else {
61
+ this.emit('error', err);
62
+ }
63
+ };
64
+ const cleanup = () => {
65
+ this.socket.removeListener('connect', onConnect);
66
+ this.socket.removeListener('error', onError);
67
+ };
68
+ this.socket.once('connect', onConnect);
69
+ this.socket.once('error', onError);
70
+ this.socket.on('data', (chunk) => this.onData(chunk));
71
+ this.socket.on('close', () => this.onClose());
72
+ this.socket.on('error', (err) => {
73
+ if (this.connected)
74
+ this.emit('error', err);
75
+ });
76
+ });
77
+ }
78
+ close() {
79
+ this.closed = true;
80
+ if (this.reconnectTimer) {
81
+ clearTimeout(this.reconnectTimer);
82
+ this.reconnectTimer = null;
83
+ }
84
+ if (this.socket) {
85
+ this.socket.destroy();
86
+ this.socket = null;
87
+ }
88
+ if (this.connected) {
89
+ this.connected = false;
90
+ this.emit('disconnected');
91
+ }
92
+ }
93
+ send(data) {
94
+ if (!this.socket || !this.connected) {
95
+ throw new Error('TCP transport not connected');
96
+ }
97
+ const json = JSON.stringify(data);
98
+ const payload = Buffer.from(json, 'utf8');
99
+ if (payload.length > MAX_MESSAGE_SIZE) {
100
+ throw new Error(`Message size ${payload.length} exceeds maximum ${MAX_MESSAGE_SIZE}`);
101
+ }
102
+ const header = Buffer.alloc(4);
103
+ header.writeUInt32BE(payload.length, 0);
104
+ this.socket.write(Buffer.concat([header, payload]));
105
+ }
106
+ on(event, listener) {
107
+ const set = this.listeners.get(event) ?? new Set();
108
+ set.add(listener);
109
+ this.listeners.set(event, set);
110
+ }
111
+ off(event, listener) {
112
+ const set = this.listeners.get(event);
113
+ if (set) {
114
+ set.delete(listener);
115
+ if (set.size === 0)
116
+ this.listeners.delete(event);
117
+ }
118
+ }
119
+ once(event, listener) {
120
+ const wrapped = (...args) => {
121
+ this.off(event, wrapped);
122
+ listener(...args);
123
+ };
124
+ this.on(event, wrapped);
125
+ }
126
+ emit(event, ...args) {
127
+ const set = this.listeners.get(event);
128
+ if (!set)
129
+ return;
130
+ for (const listener of [...set]) {
131
+ listener(...args);
132
+ }
133
+ }
134
+ onData(chunk) {
135
+ this.buffer = Buffer.concat([this.buffer, chunk]);
136
+ while (this.buffer.length >= 4) {
137
+ const messageLength = this.buffer.readUInt32BE(0);
138
+ if (messageLength > MAX_MESSAGE_SIZE) {
139
+ this.emit('error', new Error(`Message size ${messageLength} exceeds maximum ${MAX_MESSAGE_SIZE}`));
140
+ this.socket?.destroy();
141
+ return;
142
+ }
143
+ if (this.buffer.length < 4 + messageLength) {
144
+ break;
145
+ }
146
+ const jsonBuf = this.buffer.subarray(4, 4 + messageLength);
147
+ this.buffer = this.buffer.subarray(4 + messageLength);
148
+ let parsed;
149
+ try {
150
+ parsed = JSON.parse(jsonBuf.toString('utf8'));
151
+ }
152
+ catch {
153
+ this.emit('error', new Error('Invalid JSON in TCP frame'));
154
+ continue;
155
+ }
156
+ this.emit('message', parsed);
157
+ }
158
+ }
159
+ onClose() {
160
+ this.connected = false;
161
+ this.emit('disconnected');
162
+ if (!this.closed && this.options.reconnect !== false) {
163
+ this.attemptReconnect();
164
+ }
165
+ }
166
+ attemptReconnect() {
167
+ const maxAttempts = this.options.maxReconnectAttempts ?? 10;
168
+ if (this.reconnectAttempts >= maxAttempts)
169
+ return;
170
+ this.reconnectAttempts++;
171
+ const baseDelay = this.options.reconnectDelay ?? 1000;
172
+ const delay = Math.min(baseDelay * Math.pow(2, this.reconnectAttempts - 1), 30_000);
173
+ const jitter = delay * (0.5 + Math.random() * 0.5);
174
+ this.emit('reconnecting', this.reconnectAttempts, jitter);
175
+ this.reconnectTimer = setTimeout(async () => {
176
+ try {
177
+ await this._connect();
178
+ }
179
+ catch {
180
+ }
181
+ }, jitter);
182
+ }
183
+ }
@@ -0,0 +1,22 @@
1
+ import type { RaffelTransport, RaffelTransportEvent } from './transport.js';
2
+ import type { RaffelUdpOptions } from './types.js';
3
+ type Listener = (...args: any[]) => void;
4
+ export declare class UdpTransport implements RaffelTransport {
5
+ readonly capabilities: number;
6
+ private socket;
7
+ private connected;
8
+ private listeners;
9
+ private host;
10
+ private port;
11
+ private options;
12
+ constructor(url: string, options?: RaffelUdpOptions);
13
+ get isConnected(): boolean;
14
+ connect(): Promise<void>;
15
+ close(): void;
16
+ send(data: unknown): void;
17
+ on(event: RaffelTransportEvent, listener: Listener): void;
18
+ off(event: RaffelTransportEvent, listener: Listener): void;
19
+ once(event: RaffelTransportEvent, listener: Listener): void;
20
+ private emit;
21
+ }
22
+ export {};