@gowelle/stint-agent 1.2.17 → 1.2.19
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/dist/{StatusDashboard-ATB5BFZR.js → StatusDashboard-I5DG5GEB.js} +2 -3
- package/dist/api-76OMVWNR.js +7 -0
- package/dist/{chunk-D6BP2Z5S.js → chunk-6LQKDEQR.js} +3 -5
- package/dist/{chunk-XHXSWLUC.js → chunk-HZ4K7EPF.js} +120 -2
- package/dist/{chunk-QQP6IASS.js → chunk-NVQIKLOA.js} +1 -1
- package/dist/chunk-ZUQRDCS2.js +2618 -0
- package/dist/daemon/runner.js +45 -149
- package/dist/index.js +199 -133
- package/package.json +5 -1
- package/dist/api-ORVLKYAQ.js +0 -8
- package/dist/chunk-DCY3EXDX.js +0 -40
- package/dist/chunk-ES33YYVA.js +0 -95
- package/dist/chunk-VHCNMUHN.js +0 -477
- package/dist/notify-NXUEEO7M.js +0 -7
package/dist/chunk-VHCNMUHN.js
DELETED
|
@@ -1,477 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
apiService
|
|
3
|
-
} from "./chunk-D6BP2Z5S.js";
|
|
4
|
-
import {
|
|
5
|
-
gitService,
|
|
6
|
-
projectService
|
|
7
|
-
} from "./chunk-QQP6IASS.js";
|
|
8
|
-
import {
|
|
9
|
-
authService
|
|
10
|
-
} from "./chunk-ES33YYVA.js";
|
|
11
|
-
import {
|
|
12
|
-
notify
|
|
13
|
-
} from "./chunk-DCY3EXDX.js";
|
|
14
|
-
import {
|
|
15
|
-
config,
|
|
16
|
-
logger
|
|
17
|
-
} from "./chunk-XHXSWLUC.js";
|
|
18
|
-
|
|
19
|
-
// src/daemon/queue.ts
|
|
20
|
-
var CommitQueueProcessor = class {
|
|
21
|
-
queue = [];
|
|
22
|
-
isProcessing = false;
|
|
23
|
-
/**
|
|
24
|
-
* Add commit to processing queue
|
|
25
|
-
*/
|
|
26
|
-
addToQueue(commit, project) {
|
|
27
|
-
this.queue.push({ commit, project });
|
|
28
|
-
logger.info("queue", `Added commit ${commit.id} to queue (position: ${this.queue.length})`);
|
|
29
|
-
if (!this.isProcessing) {
|
|
30
|
-
this.processQueue();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Process commits sequentially
|
|
35
|
-
*/
|
|
36
|
-
async processQueue() {
|
|
37
|
-
if (this.isProcessing) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
this.isProcessing = true;
|
|
41
|
-
while (this.queue.length > 0) {
|
|
42
|
-
const item = this.queue.shift();
|
|
43
|
-
if (!item) break;
|
|
44
|
-
try {
|
|
45
|
-
await this.executeCommit(item.commit, item.project);
|
|
46
|
-
} catch (error) {
|
|
47
|
-
logger.error("queue", `Failed to execute commit ${item.commit.id}`, error);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
this.isProcessing = false;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Execute a single commit
|
|
54
|
-
*/
|
|
55
|
-
async executeCommit(commit, project, onProgress, options) {
|
|
56
|
-
logger.info("queue", `Processing commit: ${commit.id} - ${commit.message}`);
|
|
57
|
-
try {
|
|
58
|
-
onProgress?.("Finding project directory...");
|
|
59
|
-
const projectPath = this.findProjectPath(project.id);
|
|
60
|
-
if (!projectPath) {
|
|
61
|
-
throw new Error(`Project ${project.id} is not linked to any local directory`);
|
|
62
|
-
}
|
|
63
|
-
logger.info("queue", `Executing in directory: ${projectPath}`);
|
|
64
|
-
onProgress?.("Validating repository...");
|
|
65
|
-
const isRepo = await gitService.isRepo(projectPath);
|
|
66
|
-
if (!isRepo) {
|
|
67
|
-
throw new Error(`Directory ${projectPath} is not a git repository`);
|
|
68
|
-
}
|
|
69
|
-
onProgress?.("Checking repository status...");
|
|
70
|
-
let status = await gitService.getStatus(projectPath);
|
|
71
|
-
if (commit.files && commit.files.length > 0) {
|
|
72
|
-
onProgress?.(`Staging ${commit.files.length} specified files...`);
|
|
73
|
-
await gitService.stageFiles(projectPath, commit.files);
|
|
74
|
-
status = await gitService.getStatus(projectPath);
|
|
75
|
-
logger.info("queue", `Auto-staged files: ${commit.files.join(", ")}`);
|
|
76
|
-
} else if (status.staged.length === 0) {
|
|
77
|
-
const hasChanges = status.unstaged.length > 0 || status.untracked.length > 0;
|
|
78
|
-
if (hasChanges) {
|
|
79
|
-
onProgress?.("Staging all changes...");
|
|
80
|
-
await gitService.stageAll(projectPath);
|
|
81
|
-
status = await gitService.getStatus(projectPath);
|
|
82
|
-
logger.info("queue", `Auto-staged all changes`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (status.staged.length === 0) {
|
|
86
|
-
throw new Error("No changes to commit. The working directory is clean.");
|
|
87
|
-
}
|
|
88
|
-
logger.info("queue", `Committing ${status.staged.length} staged files.`);
|
|
89
|
-
onProgress?.("Creating commit...");
|
|
90
|
-
logger.info("queue", `Creating commit with message: "${commit.message}"`);
|
|
91
|
-
const sha = await gitService.commit(projectPath, commit.message);
|
|
92
|
-
logger.success("queue", `Commit created successfully: ${sha}`);
|
|
93
|
-
let pushed = false;
|
|
94
|
-
let pushError;
|
|
95
|
-
const shouldPush = options?.push !== false;
|
|
96
|
-
if (shouldPush) {
|
|
97
|
-
try {
|
|
98
|
-
onProgress?.("Pushing to remote...");
|
|
99
|
-
await gitService.push(projectPath);
|
|
100
|
-
pushed = true;
|
|
101
|
-
logger.success("queue", `Pushed commit ${sha} to remote`);
|
|
102
|
-
notify({
|
|
103
|
-
title: `Commit Pushed - ${project.name}`,
|
|
104
|
-
message: `Commit "${commit.message}" successfully pushed.`
|
|
105
|
-
});
|
|
106
|
-
} catch (error) {
|
|
107
|
-
pushError = error.message;
|
|
108
|
-
const isConflict = pushError.includes("rejected") || pushError.includes("non-fast-forward") || pushError.includes("failed to push") || pushError.includes("Updates were rejected");
|
|
109
|
-
if (isConflict) {
|
|
110
|
-
logger.warn("queue", `Push failed due to remote conflict: ${pushError}`);
|
|
111
|
-
notify({
|
|
112
|
-
title: `Push Conflict - ${project.name}`,
|
|
113
|
-
message: `Commit "${commit.message}" created but push failed.
|
|
114
|
-
Run "git pull --rebase" to resolve.`
|
|
115
|
-
});
|
|
116
|
-
} else {
|
|
117
|
-
logger.error("queue", `Push failed: ${pushError}`);
|
|
118
|
-
notify({
|
|
119
|
-
title: `Push Failed - ${project.name}`,
|
|
120
|
-
message: `Commit created but push failed: ${pushError}`
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
onProgress?.("Reporting to server...");
|
|
126
|
-
await this.reportSuccess(commit.id, sha, pushed, pushError);
|
|
127
|
-
if (!pushed && !pushError) {
|
|
128
|
-
notify({
|
|
129
|
-
title: `Commit Created - ${project.name}`,
|
|
130
|
-
message: `Commit "${commit.message}" created locally.`
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return sha;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
msg = error.message;
|
|
136
|
-
logger.error("queue", `Commit execution failed: ${msg}`);
|
|
137
|
-
notify({
|
|
138
|
-
title: `Commit Failed - ${project.name}`,
|
|
139
|
-
message: `Failed to execute commit "${commit.message}": ${msg}`
|
|
140
|
-
});
|
|
141
|
-
await this.reportFailure(commit.id, msg);
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Report successful execution to API
|
|
147
|
-
* @param commitId - Commit ID
|
|
148
|
-
* @param sha - Git commit SHA
|
|
149
|
-
* @param pushed - Whether the commit was pushed to remote
|
|
150
|
-
* @param pushError - Error message if push failed
|
|
151
|
-
*/
|
|
152
|
-
async reportSuccess(commitId, sha, pushed = true, pushError) {
|
|
153
|
-
try {
|
|
154
|
-
await apiService.markCommitExecuted(commitId, sha, pushed, pushError);
|
|
155
|
-
const status = pushed ? "executed" : "committed (push failed)";
|
|
156
|
-
logger.success("queue", `Reported commit ${status} to API: ${commitId} -> ${sha}`);
|
|
157
|
-
} catch (error) {
|
|
158
|
-
logger.error("queue", "Failed to report commit success to API", error);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Report failed execution to API
|
|
163
|
-
*/
|
|
164
|
-
async reportFailure(commitId, error) {
|
|
165
|
-
try {
|
|
166
|
-
await apiService.markCommitFailed(commitId, error);
|
|
167
|
-
logger.info("queue", `Reported commit failure to API: ${commitId}`);
|
|
168
|
-
} catch (apiError) {
|
|
169
|
-
logger.error("queue", "Failed to report commit failure to API", apiError);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Find local path for a project ID
|
|
174
|
-
*/
|
|
175
|
-
findProjectPath(projectId) {
|
|
176
|
-
const allProjects = projectService.getAllLinkedProjects();
|
|
177
|
-
for (const [path, linkedProject] of Object.entries(allProjects)) {
|
|
178
|
-
if (linkedProject.projectId === projectId) {
|
|
179
|
-
return path;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Check if queue is currently processing
|
|
186
|
-
*/
|
|
187
|
-
isCurrentlyProcessing() {
|
|
188
|
-
return this.isProcessing;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Get queue length
|
|
192
|
-
*/
|
|
193
|
-
getQueueLength() {
|
|
194
|
-
return this.queue.length;
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
var commitQueue = new CommitQueueProcessor();
|
|
198
|
-
|
|
199
|
-
// src/services/websocket.ts
|
|
200
|
-
import WebSocket from "ws";
|
|
201
|
-
var WebSocketServiceImpl = class {
|
|
202
|
-
ws = null;
|
|
203
|
-
userId = null;
|
|
204
|
-
socketId = null;
|
|
205
|
-
reconnectAttempts = 0;
|
|
206
|
-
maxReconnectAttempts = 10;
|
|
207
|
-
reconnectTimer = null;
|
|
208
|
-
isManualDisconnect = false;
|
|
209
|
-
// Event handlers
|
|
210
|
-
commitApprovedHandlers = [];
|
|
211
|
-
commitPendingHandlers = [];
|
|
212
|
-
suggestionCreatedHandlers = [];
|
|
213
|
-
projectUpdatedHandlers = [];
|
|
214
|
-
disconnectHandlers = [];
|
|
215
|
-
agentDisconnectedHandlers = [];
|
|
216
|
-
syncRequestedHandlers = [];
|
|
217
|
-
/**
|
|
218
|
-
* Connect to the WebSocket server
|
|
219
|
-
* @throws Error if connection fails or no auth token available
|
|
220
|
-
*/
|
|
221
|
-
async connect() {
|
|
222
|
-
try {
|
|
223
|
-
const token = await authService.getToken();
|
|
224
|
-
if (!token) {
|
|
225
|
-
throw new Error("No authentication token available");
|
|
226
|
-
}
|
|
227
|
-
const wsUrl = config.getWsUrl();
|
|
228
|
-
const url = `${wsUrl}?token=${encodeURIComponent(token)}`;
|
|
229
|
-
logger.info("websocket", `Connecting to ${wsUrl}...`);
|
|
230
|
-
this.ws = new WebSocket(url);
|
|
231
|
-
return new Promise((resolve, reject) => {
|
|
232
|
-
if (!this.ws) {
|
|
233
|
-
reject(new Error("WebSocket not initialized"));
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
this.ws.on("open", () => {
|
|
237
|
-
logger.success("websocket", "WebSocket connected");
|
|
238
|
-
this.reconnectAttempts = 0;
|
|
239
|
-
this.isManualDisconnect = false;
|
|
240
|
-
resolve();
|
|
241
|
-
});
|
|
242
|
-
this.ws.on("message", (data) => {
|
|
243
|
-
this.handleMessage(data);
|
|
244
|
-
});
|
|
245
|
-
this.ws.on("close", () => {
|
|
246
|
-
logger.warn("websocket", "WebSocket disconnected");
|
|
247
|
-
this.handleDisconnect();
|
|
248
|
-
});
|
|
249
|
-
this.ws.on("error", (error) => {
|
|
250
|
-
logger.error("websocket", "WebSocket error", error);
|
|
251
|
-
reject(error);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
} catch (error) {
|
|
255
|
-
logger.error("websocket", "Failed to connect", error);
|
|
256
|
-
throw error;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Disconnect from the WebSocket server
|
|
261
|
-
* Prevents automatic reconnection
|
|
262
|
-
*/
|
|
263
|
-
disconnect() {
|
|
264
|
-
this.isManualDisconnect = true;
|
|
265
|
-
if (this.reconnectTimer) {
|
|
266
|
-
clearTimeout(this.reconnectTimer);
|
|
267
|
-
this.reconnectTimer = null;
|
|
268
|
-
}
|
|
269
|
-
if (this.ws) {
|
|
270
|
-
if (this.userId) {
|
|
271
|
-
this.sendMessage({
|
|
272
|
-
event: "pusher:unsubscribe",
|
|
273
|
-
data: {
|
|
274
|
-
channel: `private-user.${this.userId}`
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
this.ws.close();
|
|
279
|
-
this.ws = null;
|
|
280
|
-
logger.info("websocket", "WebSocket disconnected");
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Check if WebSocket is currently connected
|
|
285
|
-
* @returns True if connected and ready
|
|
286
|
-
*/
|
|
287
|
-
isConnected() {
|
|
288
|
-
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Subscribe to user-specific channel for real-time updates
|
|
292
|
-
* @param userId - User ID to subscribe to
|
|
293
|
-
*/
|
|
294
|
-
async subscribeToUserChannel(userId) {
|
|
295
|
-
this.userId = userId;
|
|
296
|
-
if (this.userId) {
|
|
297
|
-
const channel = `user.${this.userId}`;
|
|
298
|
-
logger.info("websocket", `Subscribing to channel: ${channel}`);
|
|
299
|
-
this.sendMessage({
|
|
300
|
-
event: "pusher:subscribe",
|
|
301
|
-
data: {
|
|
302
|
-
channel
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Get authentication signature for private channel from Laravel backend
|
|
309
|
-
*/
|
|
310
|
-
async getChannelAuth(channel, socketId) {
|
|
311
|
-
const { apiService: apiService2 } = await import("./api-ORVLKYAQ.js");
|
|
312
|
-
const response = await apiService2.request("/api/broadcasting/auth", {
|
|
313
|
-
method: "POST",
|
|
314
|
-
body: JSON.stringify({
|
|
315
|
-
socket_id: socketId,
|
|
316
|
-
channel_name: channel
|
|
317
|
-
})
|
|
318
|
-
});
|
|
319
|
-
return response.auth;
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Register handler for commit approved events
|
|
323
|
-
* @param handler - Callback function
|
|
324
|
-
*/
|
|
325
|
-
onCommitApproved(handler) {
|
|
326
|
-
this.commitApprovedHandlers.push(handler);
|
|
327
|
-
}
|
|
328
|
-
onCommitPending(handler) {
|
|
329
|
-
this.commitPendingHandlers.push(handler);
|
|
330
|
-
}
|
|
331
|
-
onSuggestionCreated(handler) {
|
|
332
|
-
this.suggestionCreatedHandlers.push(handler);
|
|
333
|
-
}
|
|
334
|
-
onProjectUpdated(handler) {
|
|
335
|
-
this.projectUpdatedHandlers.push(handler);
|
|
336
|
-
}
|
|
337
|
-
onDisconnect(handler) {
|
|
338
|
-
this.disconnectHandlers.push(handler);
|
|
339
|
-
}
|
|
340
|
-
onAgentDisconnected(handler) {
|
|
341
|
-
this.agentDisconnectedHandlers.push(handler);
|
|
342
|
-
}
|
|
343
|
-
onSyncRequested(handler) {
|
|
344
|
-
this.syncRequestedHandlers.push(handler);
|
|
345
|
-
}
|
|
346
|
-
sendMessage(message) {
|
|
347
|
-
if (!this.isConnected()) {
|
|
348
|
-
logger.warn("websocket", "Cannot send message: not connected");
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
this.ws.send(JSON.stringify(message));
|
|
352
|
-
}
|
|
353
|
-
async handleMessage(data) {
|
|
354
|
-
try {
|
|
355
|
-
const message = JSON.parse(data.toString());
|
|
356
|
-
logger.info("websocket", `Received message: ${message.event}`);
|
|
357
|
-
if (message.event === "pusher:connection_established") {
|
|
358
|
-
try {
|
|
359
|
-
const connectionData = typeof message.data === "string" ? JSON.parse(message.data) : message.data;
|
|
360
|
-
this.socketId = connectionData.socket_id;
|
|
361
|
-
logger.success("websocket", `Connection established (socket_id: ${this.socketId})`);
|
|
362
|
-
if (this.userId) {
|
|
363
|
-
this.subscribeToUserChannel(this.userId);
|
|
364
|
-
}
|
|
365
|
-
} catch (error) {
|
|
366
|
-
logger.success("websocket", "Connection established");
|
|
367
|
-
}
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
if (message.event === "pusher_internal:subscription_succeeded") {
|
|
371
|
-
logger.success("websocket", `Subscribed to channel: ${message.channel}`);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
if (message.event === "pusher:error") {
|
|
375
|
-
try {
|
|
376
|
-
const errorData = typeof message.data === "string" ? JSON.parse(message.data) : message.data;
|
|
377
|
-
const errorCode = errorData.code;
|
|
378
|
-
const errorMessage = errorData.message;
|
|
379
|
-
logger.error("websocket", `WebSocket error (${errorCode}): ${errorMessage}`);
|
|
380
|
-
if (errorCode === 4001) {
|
|
381
|
-
logger.error("websocket", "Application does not exist - check Reverb app key configuration");
|
|
382
|
-
} else if (errorCode === 4009) {
|
|
383
|
-
logger.error("websocket", "Connection is unauthorized - authentication token may be invalid or expired");
|
|
384
|
-
const { notify: notify2 } = await import("./notify-NXUEEO7M.js");
|
|
385
|
-
notify2({
|
|
386
|
-
title: "Stint Agent - Connection Issue",
|
|
387
|
-
message: "WebSocket authentication failed. Notifications may be delayed (falling back to polling)."
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
} catch (parseError) {
|
|
391
|
-
logger.error("websocket", `WebSocket error: ${JSON.stringify(message.data)}`);
|
|
392
|
-
}
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
if (message.event === "commit.approved") {
|
|
396
|
-
const { pendingCommit } = message.data;
|
|
397
|
-
logger.info("websocket", `Commit approved: ${pendingCommit.id}`);
|
|
398
|
-
this.commitApprovedHandlers.forEach((handler) => handler(pendingCommit, pendingCommit.project));
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
if (message.event === "commit.pending") {
|
|
402
|
-
const { pendingCommit } = message.data;
|
|
403
|
-
logger.info("websocket", `Commit pending: ${pendingCommit.id}`);
|
|
404
|
-
this.commitPendingHandlers.forEach((handler) => handler(pendingCommit));
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
if (message.event === "suggestion.created") {
|
|
408
|
-
const { suggestion } = message.data;
|
|
409
|
-
logger.info("websocket", `Suggestion created: ${suggestion.id}`);
|
|
410
|
-
this.suggestionCreatedHandlers.forEach((handler) => handler(suggestion));
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
if (message.event === "project.updated") {
|
|
414
|
-
const { project } = message.data;
|
|
415
|
-
logger.info("websocket", `Project updated: ${project.id}`);
|
|
416
|
-
this.projectUpdatedHandlers.forEach((handler) => handler(project));
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
if (message.event === "sync.requested") {
|
|
420
|
-
const { project } = message.data;
|
|
421
|
-
logger.info("websocket", `Sync requested for project: ${project.id}`);
|
|
422
|
-
this.syncRequestedHandlers.forEach((handler) => handler(project.id));
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
if (message.event === "agent.disconnected") {
|
|
426
|
-
const { reason } = message.data;
|
|
427
|
-
logger.warn("websocket", `Agent disconnected by server: ${reason ?? "Server requested disconnect"}`);
|
|
428
|
-
this.agentDisconnectedHandlers.forEach((handler) => handler(reason ?? "Server requested disconnect"));
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
logger.info("websocket", `Unhandled event: ${message.event}, payload: ${JSON.stringify(message)}`);
|
|
432
|
-
} catch (error) {
|
|
433
|
-
logger.error("websocket", "Failed to parse message", error);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
handleDisconnect() {
|
|
437
|
-
this.ws = null;
|
|
438
|
-
this.disconnectHandlers.forEach((handler) => handler());
|
|
439
|
-
if (this.isManualDisconnect) {
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
443
|
-
const delay = this.getReconnectDelay();
|
|
444
|
-
this.reconnectAttempts++;
|
|
445
|
-
logger.info("websocket", `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
446
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
447
|
-
try {
|
|
448
|
-
await this.connect();
|
|
449
|
-
if (this.userId) {
|
|
450
|
-
await this.subscribeToUserChannel(this.userId);
|
|
451
|
-
}
|
|
452
|
-
} catch (error) {
|
|
453
|
-
logger.error("websocket", "Reconnection failed", error);
|
|
454
|
-
}
|
|
455
|
-
}, delay);
|
|
456
|
-
} else {
|
|
457
|
-
logger.error("websocket", "Max reconnection attempts reached");
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* Get reconnect delay with exponential backoff and jitter
|
|
462
|
-
* Jitter prevents thundering herd problem when many clients reconnect simultaneously
|
|
463
|
-
*/
|
|
464
|
-
getReconnectDelay() {
|
|
465
|
-
const delays = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
466
|
-
const index = Math.min(this.reconnectAttempts, delays.length - 1);
|
|
467
|
-
const baseDelay = delays[index];
|
|
468
|
-
const jitter = baseDelay * (Math.random() * 0.3);
|
|
469
|
-
return Math.floor(baseDelay + jitter);
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
var websocketService = new WebSocketServiceImpl();
|
|
473
|
-
|
|
474
|
-
export {
|
|
475
|
-
commitQueue,
|
|
476
|
-
websocketService
|
|
477
|
-
};
|