recker 1.0.14 → 1.0.15-next.3794a15

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,311 @@
1
+ import { EventEmitter } from 'node:events';
2
+ export class SignalingChannel extends EventEmitter {
3
+ options;
4
+ constructor(options) {
5
+ super();
6
+ this.options = options;
7
+ this.options.onMessage((message) => {
8
+ this.emit('message', message);
9
+ this.emit(message.type, message);
10
+ });
11
+ if (this.options.onClose) {
12
+ this.options.onClose(() => {
13
+ this.emit('close');
14
+ });
15
+ }
16
+ }
17
+ async send(message) {
18
+ await this.options.send(message);
19
+ }
20
+ async sendOffer(to, from, offer) {
21
+ await this.send({ type: 'offer', from, to, payload: offer });
22
+ }
23
+ async sendAnswer(to, from, answer) {
24
+ await this.send({ type: 'answer', from, to, payload: answer });
25
+ }
26
+ async sendIceCandidate(to, from, candidate) {
27
+ await this.send({ type: 'ice-candidate', from, to, payload: candidate });
28
+ }
29
+ async sendBye(to, from) {
30
+ await this.send({ type: 'bye', from, to, payload: null });
31
+ }
32
+ }
33
+ export class WebRTCClient extends EventEmitter {
34
+ peerId;
35
+ signaling;
36
+ iceServers;
37
+ dataChannelOptions;
38
+ connectionTimeout;
39
+ debug;
40
+ connections = new Map();
41
+ dataChannels = new Map();
42
+ pendingCandidates = new Map();
43
+ constructor(options) {
44
+ super();
45
+ this.peerId = options.peerId || this.generatePeerId();
46
+ this.signaling = new SignalingChannel(options.signaling);
47
+ this.iceServers = options.iceServers || [
48
+ { urls: 'stun:stun.l.google.com:19302' },
49
+ { urls: 'stun:stun1.l.google.com:19302' },
50
+ ];
51
+ this.dataChannelOptions = options.dataChannelOptions || {
52
+ ordered: true,
53
+ };
54
+ this.connectionTimeout = options.connectionTimeout || 30000;
55
+ this.debug = options.debug || false;
56
+ this.setupSignalingHandlers();
57
+ }
58
+ getPeerId() {
59
+ return this.peerId;
60
+ }
61
+ getPeerInfo(peerId) {
62
+ const pc = this.connections.get(peerId);
63
+ const dc = this.dataChannels.get(peerId);
64
+ if (!pc)
65
+ return null;
66
+ return {
67
+ peerId,
68
+ state: pc.connectionState,
69
+ dataChannelState: dc?.readyState || 'none',
70
+ localCandidates: 0,
71
+ remoteCandidates: 0,
72
+ };
73
+ }
74
+ getConnectedPeers() {
75
+ return Array.from(this.connections.keys()).filter((peerId) => {
76
+ const pc = this.connections.get(peerId);
77
+ return pc?.connectionState === 'connected';
78
+ });
79
+ }
80
+ async connect(remotePeerId) {
81
+ if (this.connections.has(remotePeerId)) {
82
+ throw new Error(`Already connected to peer: ${remotePeerId}`);
83
+ }
84
+ this.log(`Connecting to peer: ${remotePeerId}`);
85
+ const pc = this.createPeerConnection(remotePeerId);
86
+ const dc = pc.createDataChannel('data', this.dataChannelOptions);
87
+ this.setupDataChannel(remotePeerId, dc);
88
+ const offer = await pc.createOffer();
89
+ await pc.setLocalDescription(offer);
90
+ await this.signaling.sendOffer(remotePeerId, this.peerId, offer);
91
+ await this.waitForConnection(remotePeerId);
92
+ }
93
+ disconnect(remotePeerId) {
94
+ this.log(`Disconnecting from peer: ${remotePeerId}`);
95
+ const dc = this.dataChannels.get(remotePeerId);
96
+ if (dc) {
97
+ dc.close();
98
+ this.dataChannels.delete(remotePeerId);
99
+ }
100
+ const pc = this.connections.get(remotePeerId);
101
+ if (pc) {
102
+ pc.close();
103
+ this.connections.delete(remotePeerId);
104
+ }
105
+ this.pendingCandidates.delete(remotePeerId);
106
+ this.signaling.sendBye(remotePeerId, this.peerId).catch(() => { });
107
+ this.emit('disconnected', remotePeerId);
108
+ }
109
+ disconnectAll() {
110
+ for (const peerId of this.connections.keys()) {
111
+ this.disconnect(peerId);
112
+ }
113
+ }
114
+ send(data, remotePeerId) {
115
+ const message = typeof data === 'string' ? data : JSON.stringify(data);
116
+ if (remotePeerId) {
117
+ const dc = this.dataChannels.get(remotePeerId);
118
+ if (!dc || dc.readyState !== 'open') {
119
+ throw new Error(`No open data channel to peer: ${remotePeerId}`);
120
+ }
121
+ dc.send(message);
122
+ }
123
+ else {
124
+ for (const [peerId, dc] of this.dataChannels) {
125
+ if (dc.readyState === 'open') {
126
+ dc.send(message);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ sendBinary(data, remotePeerId) {
132
+ if (remotePeerId) {
133
+ const dc = this.dataChannels.get(remotePeerId);
134
+ if (!dc || dc.readyState !== 'open') {
135
+ throw new Error(`No open data channel to peer: ${remotePeerId}`);
136
+ }
137
+ dc.send(data);
138
+ }
139
+ else {
140
+ for (const [, dc] of this.dataChannels) {
141
+ if (dc.readyState === 'open') {
142
+ dc.send(data);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ generatePeerId() {
148
+ return `peer-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
149
+ }
150
+ log(message, ...args) {
151
+ if (this.debug) {
152
+ console.log(`[WebRTC:${this.peerId}] ${message}`, ...args);
153
+ }
154
+ }
155
+ createPeerConnection(remotePeerId) {
156
+ if (typeof RTCPeerConnection === 'undefined') {
157
+ throw new Error('RTCPeerConnection is not available. ' +
158
+ 'In Node.js, install the "wrtc" package and ensure it\'s loaded before using WebRTC.');
159
+ }
160
+ const pc = new RTCPeerConnection({
161
+ iceServers: this.iceServers,
162
+ });
163
+ this.connections.set(remotePeerId, pc);
164
+ this.pendingCandidates.set(remotePeerId, []);
165
+ pc.onicecandidate = (event) => {
166
+ if (event.candidate) {
167
+ this.signaling.sendIceCandidate(remotePeerId, this.peerId, event.candidate.toJSON());
168
+ }
169
+ };
170
+ pc.onconnectionstatechange = () => {
171
+ this.log(`Connection state with ${remotePeerId}: ${pc.connectionState}`);
172
+ this.emit('connectionStateChange', remotePeerId, pc.connectionState);
173
+ if (pc.connectionState === 'connected') {
174
+ this.emit('connected', remotePeerId);
175
+ }
176
+ else if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
177
+ this.emit('disconnected', remotePeerId);
178
+ }
179
+ };
180
+ pc.ondatachannel = (event) => {
181
+ this.log(`Received data channel from ${remotePeerId}`);
182
+ this.setupDataChannel(remotePeerId, event.channel);
183
+ };
184
+ return pc;
185
+ }
186
+ setupDataChannel(remotePeerId, dc) {
187
+ this.dataChannels.set(remotePeerId, dc);
188
+ dc.onopen = () => {
189
+ this.log(`Data channel opened with ${remotePeerId}`);
190
+ this.emit('dataChannelOpen', remotePeerId);
191
+ };
192
+ dc.onclose = () => {
193
+ this.log(`Data channel closed with ${remotePeerId}`);
194
+ this.emit('dataChannelClose', remotePeerId);
195
+ };
196
+ dc.onerror = (error) => {
197
+ this.log(`Data channel error with ${remotePeerId}:`, error);
198
+ this.emit('error', error, remotePeerId);
199
+ };
200
+ dc.onmessage = (event) => {
201
+ let data = event.data;
202
+ if (typeof data === 'string') {
203
+ try {
204
+ data = JSON.parse(data);
205
+ }
206
+ catch {
207
+ }
208
+ }
209
+ this.emit('data', data, remotePeerId);
210
+ };
211
+ }
212
+ setupSignalingHandlers() {
213
+ this.signaling.on('offer', async (message) => {
214
+ if (message.to !== this.peerId)
215
+ return;
216
+ this.log(`Received offer from ${message.from}`);
217
+ const pc = this.createPeerConnection(message.from);
218
+ await pc.setRemoteDescription(message.payload);
219
+ await this.addPendingCandidates(message.from);
220
+ const answer = await pc.createAnswer();
221
+ await pc.setLocalDescription(answer);
222
+ await this.signaling.sendAnswer(message.from, this.peerId, answer);
223
+ });
224
+ this.signaling.on('answer', async (message) => {
225
+ if (message.to !== this.peerId)
226
+ return;
227
+ this.log(`Received answer from ${message.from}`);
228
+ const pc = this.connections.get(message.from);
229
+ if (!pc) {
230
+ this.log(`No connection found for ${message.from}`);
231
+ return;
232
+ }
233
+ await pc.setRemoteDescription(message.payload);
234
+ await this.addPendingCandidates(message.from);
235
+ });
236
+ this.signaling.on('ice-candidate', async (message) => {
237
+ if (message.to !== this.peerId)
238
+ return;
239
+ const pc = this.connections.get(message.from);
240
+ if (pc && pc.remoteDescription) {
241
+ await pc.addIceCandidate(message.payload);
242
+ }
243
+ else {
244
+ const pending = this.pendingCandidates.get(message.from) || [];
245
+ pending.push(message.payload);
246
+ this.pendingCandidates.set(message.from, pending);
247
+ }
248
+ });
249
+ this.signaling.on('bye', (message) => {
250
+ if (message.to !== this.peerId)
251
+ return;
252
+ this.log(`Received bye from ${message.from}`);
253
+ this.disconnect(message.from);
254
+ });
255
+ }
256
+ async addPendingCandidates(remotePeerId) {
257
+ const pc = this.connections.get(remotePeerId);
258
+ const candidates = this.pendingCandidates.get(remotePeerId) || [];
259
+ for (const candidate of candidates) {
260
+ await pc?.addIceCandidate(candidate);
261
+ }
262
+ this.pendingCandidates.set(remotePeerId, []);
263
+ }
264
+ waitForConnection(remotePeerId) {
265
+ return new Promise((resolve, reject) => {
266
+ const timeout = setTimeout(() => {
267
+ reject(new Error(`Connection timeout to peer: ${remotePeerId}`));
268
+ }, this.connectionTimeout);
269
+ const checkConnection = () => {
270
+ const dc = this.dataChannels.get(remotePeerId);
271
+ if (dc?.readyState === 'open') {
272
+ clearTimeout(timeout);
273
+ resolve();
274
+ }
275
+ };
276
+ this.on('dataChannelOpen', (peerId) => {
277
+ if (peerId === remotePeerId) {
278
+ clearTimeout(timeout);
279
+ resolve();
280
+ }
281
+ });
282
+ this.on('disconnected', (peerId) => {
283
+ if (peerId === remotePeerId) {
284
+ clearTimeout(timeout);
285
+ reject(new Error(`Connection failed to peer: ${remotePeerId}`));
286
+ }
287
+ });
288
+ checkConnection();
289
+ });
290
+ }
291
+ }
292
+ export const DEFAULT_ICE_SERVERS = [
293
+ { urls: 'stun:stun.l.google.com:19302' },
294
+ { urls: 'stun:stun1.l.google.com:19302' },
295
+ { urls: 'stun:stun2.l.google.com:19302' },
296
+ { urls: 'stun:stun3.l.google.com:19302' },
297
+ { urls: 'stun:stun4.l.google.com:19302' },
298
+ ];
299
+ export function isWebRTCAvailable() {
300
+ return typeof RTCPeerConnection !== 'undefined';
301
+ }
302
+ export function getWebRTCSupport() {
303
+ const isBrowser = typeof window !== 'undefined';
304
+ const isNode = typeof process !== 'undefined' && process.versions?.node;
305
+ return {
306
+ available: typeof RTCPeerConnection !== 'undefined',
307
+ dataChannels: typeof RTCPeerConnection !== 'undefined',
308
+ media: typeof navigator !== 'undefined' && !!navigator.mediaDevices,
309
+ environment: isBrowser ? 'browser' : isNode ? 'node' : 'unknown',
310
+ };
311
+ }
@@ -62,5 +62,5 @@ export declare class ReckerWebSocket extends EventEmitter {
62
62
  private getBufferedAmount;
63
63
  private waitForDrain;
64
64
  }
65
- export declare function websocket(url: string, options?: WebSocketOptions): ReckerWebSocket;
65
+ export declare function createWebSocket(url: string, options?: WebSocketOptions): ReckerWebSocket;
66
66
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/websocket/client.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAKvC,MAAM,WAAW,gBAAgB;IAI/B,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAK9B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAKjC,UAAU,CAAC,EAAE,UAAU,CAAC;IAKxB,KAAK,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;IAK9B,GAAG,CAAC,EAAE,UAAU,CAAC;IAMjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAM5B,SAAS,CAAC,EAAE,OAAO,CAAC;IAMpB,cAAc,CAAC,EAAE,MAAM,CAAC;IAMxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAO9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAM3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAYD,qBAAa,eAAgB,SAAQ,YAAY;IAC/C,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,OAAO,CAAgP;IAC/P,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,YAAY,CAAC,CAAiB;IACtC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,YAAY,CAAS;gBAEjB,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB;IA4BjD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA+ExB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,eAAe,EAAE,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BtI,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAS7G,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI;IAOzB,KAAK,CAAC,IAAI,SAAO,EAAE,MAAM,SAAK,GAAG,IAAI;IAgBrC,IAAI,IAAI,IAAI;IAwBZ,IAAI,UAAU,IAAI,MAAM,CAEvB;IAKD,IAAI,WAAW,IAAI,OAAO,CAEzB;IAMD,UAAU,IAAI,QAAQ,GAAG,IAAI;IAavB,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrG,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBxD,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,cAAc,CAAC,gBAAgB,CAAC;IAoDjE,OAAO,CAAC,gBAAgB;IAuBxB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,iBAAiB;YAKX,YAAY;CAgB3B;AAKD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe,CAMlF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/websocket/client.ts"],"names":[],"mappings":"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"}
@@ -304,7 +304,7 @@ export class ReckerWebSocket extends EventEmitter {
304
304
  });
305
305
  }
306
306
  }
307
- export function websocket(url, options) {
307
+ export function createWebSocket(url, options) {
308
308
  const ws = new ReckerWebSocket(url, options);
309
309
  ws.connect().catch(() => {
310
310
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recker",
3
- "version": "1.0.14",
3
+ "version": "1.0.15-next.3794a15",
4
4
  "description": "AI & DevX focused HTTP client for Node.js 18+",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",