@recursorsdk/sdk 1.0.0
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/LICENSE +19 -0
- package/README.md +412 -0
- package/dist/index.d.ts +386 -0
- package/dist/index.js +480 -0
- package/dist/plugins/vscodeStub.d.ts +2 -0
- package/dist/plugins/vscodeStub.js +24 -0
- package/dist/plugins/windsurfStub.d.ts +1 -0
- package/dist/plugins/windsurfStub.js +14 -0
- package/dist/websocket.d.ts +73 -0
- package/dist/websocket.js +190 -0
- package/package.json +33 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Client for Recursor SDK
|
|
3
|
+
* Provides real-time updates via WebSocket connection
|
|
4
|
+
*/
|
|
5
|
+
export class RecursorWebSocket {
|
|
6
|
+
constructor(baseUrl, accessToken) {
|
|
7
|
+
this.ws = null;
|
|
8
|
+
this.reconnectAttempts = 0;
|
|
9
|
+
this.maxReconnectAttempts = 5;
|
|
10
|
+
this.reconnectDelay = 1000;
|
|
11
|
+
this.pingInterval = null;
|
|
12
|
+
this.isConnecting = false;
|
|
13
|
+
this.shouldReconnect = true;
|
|
14
|
+
this.eventHandlers = new Map();
|
|
15
|
+
// Convert http/https to ws/wss
|
|
16
|
+
this.baseUrl = baseUrl.replace(/^http/, "ws");
|
|
17
|
+
this.accessToken = accessToken;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Connect to WebSocket server
|
|
21
|
+
*/
|
|
22
|
+
connect() {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
|
25
|
+
resolve();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.isConnecting = true;
|
|
29
|
+
this.shouldReconnect = true;
|
|
30
|
+
const wsUrl = `${this.baseUrl}/client/ws?token=${this.accessToken}`;
|
|
31
|
+
try {
|
|
32
|
+
this.ws = new WebSocket(wsUrl);
|
|
33
|
+
this.ws.onopen = () => {
|
|
34
|
+
this.isConnecting = false;
|
|
35
|
+
this.reconnectAttempts = 0;
|
|
36
|
+
this.startPing();
|
|
37
|
+
resolve();
|
|
38
|
+
};
|
|
39
|
+
this.ws.onmessage = (event) => {
|
|
40
|
+
try {
|
|
41
|
+
const message = JSON.parse(event.data);
|
|
42
|
+
this.handleMessage(message);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error("Failed to parse WebSocket message:", error);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
this.ws.onclose = () => {
|
|
49
|
+
this.isConnecting = false;
|
|
50
|
+
this.stopPing();
|
|
51
|
+
if (this.shouldReconnect) {
|
|
52
|
+
this.attemptReconnect();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
this.ws.onerror = (error) => {
|
|
56
|
+
console.error("WebSocket error:", error);
|
|
57
|
+
this.isConnecting = false;
|
|
58
|
+
reject(error);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
this.isConnecting = false;
|
|
63
|
+
reject(error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Attempt to reconnect with exponential backoff
|
|
69
|
+
*/
|
|
70
|
+
attemptReconnect() {
|
|
71
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
72
|
+
console.error("Max reconnection attempts reached");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.reconnectAttempts++;
|
|
76
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
this.connect().catch(console.error);
|
|
79
|
+
}, delay);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Start ping interval to keep connection alive
|
|
83
|
+
*/
|
|
84
|
+
startPing() {
|
|
85
|
+
this.stopPing();
|
|
86
|
+
this.pingInterval = setInterval(() => {
|
|
87
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
88
|
+
this.send({ type: "ping" });
|
|
89
|
+
}
|
|
90
|
+
}, 30000); // Ping every 30 seconds
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Stop ping interval
|
|
94
|
+
*/
|
|
95
|
+
stopPing() {
|
|
96
|
+
if (this.pingInterval) {
|
|
97
|
+
clearInterval(this.pingInterval);
|
|
98
|
+
this.pingInterval = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Send message to server
|
|
103
|
+
*/
|
|
104
|
+
send(message) {
|
|
105
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
106
|
+
this.ws.send(JSON.stringify(message));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Subscribe to event type
|
|
111
|
+
*/
|
|
112
|
+
on(event, handler) {
|
|
113
|
+
if (!this.eventHandlers.has(event)) {
|
|
114
|
+
this.eventHandlers.set(event, new Set());
|
|
115
|
+
}
|
|
116
|
+
this.eventHandlers.get(event).add(handler);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Unsubscribe from event type
|
|
120
|
+
*/
|
|
121
|
+
off(event, handler) {
|
|
122
|
+
this.eventHandlers.get(event)?.delete(handler);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Handle incoming message
|
|
126
|
+
*/
|
|
127
|
+
handleMessage(message) {
|
|
128
|
+
// Handle connection confirmation
|
|
129
|
+
if (message.type === "connected") {
|
|
130
|
+
// Connection confirmed, emit connected event
|
|
131
|
+
this.emit("connected", message);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Handle pong response
|
|
135
|
+
if (message.type === "pong") {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Emit event based on message type
|
|
139
|
+
if (message.type) {
|
|
140
|
+
this.emit(message.type, message.data || message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Emit event to handlers
|
|
145
|
+
*/
|
|
146
|
+
emit(event, data) {
|
|
147
|
+
const handlers = this.eventHandlers.get(event);
|
|
148
|
+
if (!handlers || handlers.size === 0)
|
|
149
|
+
return;
|
|
150
|
+
handlers.forEach((handler) => {
|
|
151
|
+
try {
|
|
152
|
+
handler(data);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error(`Error in WebSocket handler for ${event}:`, error);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Disconnect WebSocket
|
|
161
|
+
*/
|
|
162
|
+
disconnect() {
|
|
163
|
+
this.shouldReconnect = false;
|
|
164
|
+
this.stopPing();
|
|
165
|
+
if (this.ws) {
|
|
166
|
+
this.ws.close();
|
|
167
|
+
this.ws = null;
|
|
168
|
+
}
|
|
169
|
+
this.isConnecting = false;
|
|
170
|
+
this.reconnectAttempts = 0;
|
|
171
|
+
this.eventHandlers.clear();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if WebSocket is connected
|
|
175
|
+
*/
|
|
176
|
+
isConnected() {
|
|
177
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Update access token (useful after token refresh)
|
|
181
|
+
*/
|
|
182
|
+
updateToken(newToken) {
|
|
183
|
+
this.accessToken = newToken;
|
|
184
|
+
// Reconnect with new token
|
|
185
|
+
if (this.isConnected()) {
|
|
186
|
+
this.disconnect();
|
|
187
|
+
this.connect().catch(console.error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@recursorsdk/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Recursor SDK for Node.js",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json && node scripts/fix-esm-imports.js",
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"test": "npm run build && node --test test/*.test.ts test/*.test.js",
|
|
23
|
+
"prepare": "npm run build",
|
|
24
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.6.3",
|
|
31
|
+
"@types/node": "^20.11.30"
|
|
32
|
+
}
|
|
33
|
+
}
|