@buoy-gg/sync-broker 4.0.1

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/index.js ADDED
@@ -0,0 +1,482 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ DEFAULT_BROKER_PORT: () => DEFAULT_BROKER_PORT,
34
+ SOCKET_CONFIG: () => SOCKET_CONFIG,
35
+ SYNC_PROTOCOL_VERSION: () => SYNC_PROTOCOL_VERSION,
36
+ createBroker: () => createBroker,
37
+ socketHandle: () => socketHandle
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/socketHandle.ts
42
+ var users = [];
43
+ var allDevices = [];
44
+ var dashboardClients = [];
45
+ var deviceIdMap = /* @__PURE__ */ new Map();
46
+ var activeConnectionsMap = /* @__PURE__ */ new Map();
47
+ var deviceCapabilities = /* @__PURE__ */ new Map();
48
+ var dashboardWatches = /* @__PURE__ */ new Map();
49
+ var watchCounts = /* @__PURE__ */ new Map();
50
+ var toWatchKey = (targetDeviceId, toolId) => JSON.stringify([targetDeviceId, toolId]);
51
+ var fromWatchKey = (key) => JSON.parse(key);
52
+ function socketHandle({ io }) {
53
+ const LOG_PREFIX = "[SERVER]";
54
+ function handleClose(id, reason) {
55
+ const user = users.find((user2) => user2.id === id);
56
+ const deviceId = deviceIdMap.get(id);
57
+ if (!deviceId || !users.some((u) => u.id !== id && u.deviceId === deviceId)) {
58
+ users = users.filter((user2) => user2.id !== id);
59
+ console.log(
60
+ `${LOG_PREFIX} Client disconnected - ID: ${id}, Name: ${user?.deviceName || "Unknown"} - Removing user record`
61
+ );
62
+ if (deviceId) {
63
+ const deviceIndex = allDevices.findIndex(
64
+ (u) => u.deviceId === deviceId
65
+ );
66
+ if (deviceIndex !== -1) {
67
+ allDevices[deviceIndex].isConnected = false;
68
+ allDevices[deviceIndex].lastDisconnectReason = reason;
69
+ activeConnectionsMap.delete(deviceId);
70
+ }
71
+ }
72
+ } else {
73
+ console.log(
74
+ `${LOG_PREFIX} Client disconnected - ID: ${id}, Name: ${user?.deviceName || "Unknown"} - Keeping user record for same device with ID: ${deviceId}`
75
+ );
76
+ }
77
+ deviceIdMap.delete(id);
78
+ const wasDashboard = dashboardClients.includes(id);
79
+ dashboardClients = dashboardClients.filter((clientId) => clientId !== id);
80
+ releaseDashboardWatches(id);
81
+ console.log(
82
+ `${LOG_PREFIX} Client disconnected - ID: ${id}, Name: ${user?.deviceName || "Unknown"}, Was Dashboard: ${wasDashboard}, Reason: ${reason ?? "unknown"}`
83
+ );
84
+ io.emit(
85
+ "all-devices-update",
86
+ allDevices.filter((device) => device.deviceName !== "Dashboard")
87
+ );
88
+ console.log(
89
+ `${LOG_PREFIX} Updated users list: ${users.map((u) => u.deviceName).join(", ")}`
90
+ );
91
+ }
92
+ function addNewUser({
93
+ id,
94
+ deviceName,
95
+ deviceId,
96
+ platform,
97
+ extraDeviceInfo,
98
+ envVariables,
99
+ connectionNonce
100
+ }) {
101
+ const isDashboard = deviceName === "Dashboard";
102
+ if (deviceId) {
103
+ deviceIdMap.set(id, deviceId);
104
+ activeConnectionsMap.set(deviceId, id);
105
+ const existingUserIndex = users.findIndex(
106
+ (user) => user.deviceId === deviceId
107
+ );
108
+ const existingDeviceIndex = allDevices.findIndex(
109
+ (device) => device.deviceId === deviceId
110
+ );
111
+ if (existingUserIndex !== -1) {
112
+ console.log(
113
+ `${LOG_PREFIX} Reconnecting existing device - ID: ${id}, Name: ${deviceName}, DeviceId: ${deviceId}, Platform: ${platform || "Unknown"}, ExtraDeviceInfo: ${extraDeviceInfo}`
114
+ );
115
+ users[existingUserIndex].id = id;
116
+ if (platform) {
117
+ users[existingUserIndex].platform = platform;
118
+ }
119
+ if (extraDeviceInfo) {
120
+ users[existingUserIndex].extraDeviceInfo = extraDeviceInfo;
121
+ }
122
+ if (envVariables) {
123
+ users[existingUserIndex].envVariables = envVariables;
124
+ }
125
+ users[existingUserIndex].isConnected = true;
126
+ users[existingUserIndex].connectionNonce = connectionNonce;
127
+ if (existingDeviceIndex !== -1) {
128
+ allDevices[existingDeviceIndex].id = id;
129
+ allDevices[existingDeviceIndex].isConnected = true;
130
+ allDevices[existingDeviceIndex].connectionNonce = connectionNonce;
131
+ if (platform) {
132
+ allDevices[existingDeviceIndex].platform = platform;
133
+ }
134
+ if (extraDeviceInfo) {
135
+ allDevices[existingDeviceIndex].extraDeviceInfo = extraDeviceInfo;
136
+ }
137
+ if (envVariables) {
138
+ allDevices[existingDeviceIndex].envVariables = envVariables;
139
+ }
140
+ } else if (deviceName !== "Dashboard") {
141
+ allDevices.push({
142
+ id,
143
+ deviceName,
144
+ deviceId,
145
+ platform,
146
+ isConnected: true,
147
+ extraDeviceInfo,
148
+ envVariables
149
+ });
150
+ }
151
+ io.emit(
152
+ "all-devices-update",
153
+ allDevices.filter((device) => device.deviceName !== "Dashboard")
154
+ );
155
+ console.log(
156
+ `${LOG_PREFIX} Updated users list: ${users.map((u) => u.deviceName).join(", ")}`
157
+ );
158
+ return;
159
+ }
160
+ }
161
+ const extractNumber = (name) => {
162
+ const match = name.match(/(\d+)$/);
163
+ return match ? parseInt(match[1], 10) : 1;
164
+ };
165
+ let highestNumber = 0;
166
+ users.forEach((user) => {
167
+ if (user.deviceName.startsWith(deviceName)) {
168
+ const number = extractNumber(user.deviceName);
169
+ if (number >= highestNumber) {
170
+ highestNumber = number;
171
+ }
172
+ }
173
+ });
174
+ if (highestNumber > 0 && !isDashboard) {
175
+ deviceName = `${deviceName} #${highestNumber + 1}`;
176
+ }
177
+ if (id) {
178
+ const newUser = {
179
+ id,
180
+ deviceName,
181
+ deviceId,
182
+ platform,
183
+ isConnected: true,
184
+ extraDeviceInfo,
185
+ envVariables,
186
+ connectionNonce
187
+ };
188
+ users.push(newUser);
189
+ if (deviceName !== "Dashboard" && deviceId) {
190
+ const existingDeviceIndex = allDevices.findIndex(
191
+ (device) => device.deviceId === deviceId
192
+ );
193
+ if (existingDeviceIndex !== -1) {
194
+ allDevices[existingDeviceIndex] = { ...newUser };
195
+ } else {
196
+ allDevices.push({ ...newUser });
197
+ }
198
+ }
199
+ console.log(
200
+ `${LOG_PREFIX} New user connected - ID: ${id}, Name: ${deviceName}${deviceId ? `, DeviceId: ${deviceId}` : ""}${platform ? `, Platform: ${platform}` : ""}`
201
+ );
202
+ if (isDashboard && id) {
203
+ dashboardClients.push(id);
204
+ console.log(`${LOG_PREFIX} Registered dashboard client with ID: ${id}`);
205
+ }
206
+ io.emit(
207
+ "all-devices-update",
208
+ allDevices.filter((device) => device.deviceName !== "Dashboard")
209
+ );
210
+ console.log(
211
+ `${LOG_PREFIX} Updated device list: ${users.map((u) => u.deviceName).join(", ")}`
212
+ );
213
+ }
214
+ }
215
+ function getDeviceFromDeviceId(deviceId) {
216
+ return allDevices.find((device) => device.deviceId === deviceId);
217
+ }
218
+ function handleUserMessage(message, deviceName) {
219
+ console.log(`${LOG_PREFIX} Message from ${deviceName}: ${message}`);
220
+ io.emit("message", `Device ${deviceName} sent message: ${message}`);
221
+ }
222
+ function forwardToDashboards(event, message) {
223
+ dashboardClients = dashboardClients.filter(
224
+ (id) => io.sockets.sockets.has(id)
225
+ );
226
+ if (dashboardClients.length === 0) {
227
+ return;
228
+ }
229
+ for (const dashboardId of dashboardClients) {
230
+ io.sockets.sockets.get(dashboardId)?.emit(event, message);
231
+ }
232
+ }
233
+ function withTargetUsers(targetDeviceId, action, actionName) {
234
+ const deviceName = getDeviceFromDeviceId(targetDeviceId)?.deviceName;
235
+ if (targetDeviceId === "No devices available") {
236
+ console.log(
237
+ `${LOG_PREFIX} Skipping ${actionName} - No devices available`
238
+ );
239
+ return;
240
+ }
241
+ if (targetDeviceId === "All") {
242
+ console.log(`${LOG_PREFIX} Broadcasting ${actionName} to all devices`);
243
+ const deviceUsers = users.filter(
244
+ (user) => user.deviceName !== "Dashboard"
245
+ );
246
+ console.log(
247
+ `${LOG_PREFIX} Sending to ${deviceUsers.length} devices: ${deviceUsers.map((u) => u.deviceName).join(", ")}`
248
+ );
249
+ deviceUsers.forEach(action);
250
+ } else {
251
+ const targetUser = users.find((user) => user.deviceId === targetDeviceId);
252
+ if (targetUser) {
253
+ console.log(
254
+ `${LOG_PREFIX} Sending to target device ${targetUser.deviceName} (${targetUser.deviceId}) targetDeviceId: ${targetDeviceId}`
255
+ );
256
+ action(targetUser);
257
+ } else {
258
+ console.log(
259
+ `${LOG_PREFIX} Target device not found - DeviceId: ${targetDeviceId}, DeviceName: ${deviceName}`
260
+ );
261
+ }
262
+ }
263
+ }
264
+ function handleDevtoolWatch(dashboardId, message) {
265
+ const key = toWatchKey(message.targetDeviceId, message.toolId);
266
+ let watches = dashboardWatches.get(dashboardId);
267
+ if (!watches) {
268
+ watches = /* @__PURE__ */ new Set();
269
+ dashboardWatches.set(dashboardId, watches);
270
+ }
271
+ if (message.watching) {
272
+ if (!watches.has(key)) {
273
+ watches.add(key);
274
+ watchCounts.set(key, (watchCounts.get(key) ?? 0) + 1);
275
+ }
276
+ console.log(
277
+ `${LOG_PREFIX} Dashboard ${dashboardId} watching "${message.toolId}" on ${message.targetDeviceId}`
278
+ );
279
+ withTargetUsers(
280
+ message.targetDeviceId,
281
+ (user) => io.to(user.id).emit("devtool-watch", message),
282
+ `devtool watch ${message.toolId}`
283
+ );
284
+ } else if (watches.has(key)) {
285
+ watches.delete(key);
286
+ decrementWatch(key);
287
+ }
288
+ }
289
+ function decrementWatch(key) {
290
+ const next = (watchCounts.get(key) ?? 1) - 1;
291
+ if (next > 0) {
292
+ watchCounts.set(key, next);
293
+ return;
294
+ }
295
+ watchCounts.delete(key);
296
+ const [targetDeviceId, toolId] = fromWatchKey(key);
297
+ console.log(
298
+ `${LOG_PREFIX} Last dashboard stopped watching "${toolId}" on ${targetDeviceId}`
299
+ );
300
+ withTargetUsers(
301
+ targetDeviceId,
302
+ (user) => io.to(user.id).emit("devtool-watch", { toolId, targetDeviceId, watching: false }),
303
+ `devtool unwatch ${toolId}`
304
+ );
305
+ }
306
+ function releaseDashboardWatches(dashboardId) {
307
+ const watches = dashboardWatches.get(dashboardId);
308
+ if (!watches) return;
309
+ dashboardWatches.delete(dashboardId);
310
+ for (const key of watches) {
311
+ decrementWatch(key);
312
+ }
313
+ }
314
+ io.on("connection", (socket) => {
315
+ const {
316
+ deviceName,
317
+ deviceId,
318
+ platform,
319
+ extraDeviceInfo,
320
+ envVariables,
321
+ connectionNonce
322
+ } = socket.handshake.query;
323
+ console.log(
324
+ `${LOG_PREFIX} New connection - ID: ${socket.id}, Name: ${deviceName || "Unknown Device"}${deviceId ? `, DeviceId: ${deviceId}` : ""}${platform ? `, Platform: ${platform}` : ""}${extraDeviceInfo ? `, ExtraDeviceInfo: ${extraDeviceInfo}` : ""}${envVariables ? `, EnvVariables: ${envVariables}` : ""}`
325
+ );
326
+ addNewUser({
327
+ id: socket.id,
328
+ deviceName: deviceName || "Unknown Device Name",
329
+ deviceId,
330
+ platform,
331
+ extraDeviceInfo,
332
+ envVariables,
333
+ connectionNonce
334
+ });
335
+ if (deviceName === "Dashboard") {
336
+ for (const capabilities of deviceCapabilities.values()) {
337
+ socket.emit("devtool-capabilities", capabilities);
338
+ }
339
+ }
340
+ socket.on(
341
+ "device-license",
342
+ (payload) => {
343
+ const nextIsPro = payload?.isPro === "1" || payload?.isPro === true;
344
+ const nextKey = payload?.licenseKey || void 0;
345
+ const user = users.find((u) => u.id === socket.id);
346
+ if (user) {
347
+ user.isPro = nextIsPro;
348
+ user.licenseKey = nextIsPro ? nextKey : void 0;
349
+ }
350
+ const did = user?.deviceId || deviceIdMap.get(socket.id);
351
+ if (did) {
352
+ const device = allDevices.find((d) => d.deviceId === did);
353
+ if (device) {
354
+ device.isPro = nextIsPro;
355
+ device.licenseKey = nextIsPro ? nextKey : void 0;
356
+ }
357
+ }
358
+ console.log(
359
+ `${LOG_PREFIX} device-license from ${user?.deviceName || socket.id}: isPro=${nextIsPro}${nextKey ? ", key set" : ""}`
360
+ );
361
+ io.emit(
362
+ "all-devices-update",
363
+ allDevices.filter((device) => device.deviceName !== "Dashboard")
364
+ );
365
+ }
366
+ );
367
+ socket.on(
368
+ "disconnect",
369
+ (reason) => handleClose(socket.id, reason)
370
+ );
371
+ socket.on("message", (msg) => {
372
+ handleUserMessage(msg, deviceName || "Unknown");
373
+ });
374
+ socket.on("devtool-capabilities", (message) => {
375
+ console.log(
376
+ `${LOG_PREFIX} Capabilities from ${message.persistentDeviceId}: ${message.tools.map((tool) => `${tool.toolId}@v${tool.version}`).join(", ")}`
377
+ );
378
+ deviceCapabilities.set(message.persistentDeviceId, message);
379
+ forwardToDashboards("devtool-capabilities", message);
380
+ for (const key of watchCounts.keys()) {
381
+ const [targetDeviceId, toolId] = fromWatchKey(key);
382
+ if (targetDeviceId === "All" || targetDeviceId === message.persistentDeviceId) {
383
+ socket.emit("devtool-watch", {
384
+ toolId,
385
+ targetDeviceId,
386
+ watching: true
387
+ });
388
+ }
389
+ }
390
+ });
391
+ socket.on("devtool-sync", (message) => {
392
+ forwardToDashboards("devtool-sync", message);
393
+ });
394
+ socket.on(
395
+ "devtool-action-result",
396
+ (message) => {
397
+ console.log(
398
+ `${LOG_PREFIX} Action result from ${message.persistentDeviceId}: ${message.toolId}.${message.action} -> ${message.ok ? "ok" : `error: ${message.error}`}`
399
+ );
400
+ forwardToDashboards("devtool-action-result", message);
401
+ }
402
+ );
403
+ socket.on("devtool-watch", (message) => {
404
+ handleDevtoolWatch(socket.id, message);
405
+ });
406
+ socket.on("request-devtool-state", (message) => {
407
+ withTargetUsers(
408
+ message.targetDeviceId,
409
+ (user) => io.to(user.id).emit("request-devtool-state", message),
410
+ `devtool state request ${message.toolId}`
411
+ );
412
+ });
413
+ socket.on("devtool-action", (message) => {
414
+ console.log(
415
+ `${LOG_PREFIX} Devtool action from dashboard: ${message.toolId}.${message.action} -> ${message.targetDeviceId}`
416
+ );
417
+ withTargetUsers(
418
+ message.targetDeviceId,
419
+ (user) => io.to(user.id).emit("devtool-action", message),
420
+ `devtool action ${message.toolId}.${message.action}`
421
+ );
422
+ });
423
+ });
424
+ }
425
+
426
+ // src/createBroker.ts
427
+ var import_http = __toESM(require("http"));
428
+ var import_socket = require("socket.io");
429
+ var DEFAULT_BROKER_PORT = 42831;
430
+ var SOCKET_CONFIG = {
431
+ cors: {
432
+ origin: "*",
433
+ methods: ["GET", "POST"]
434
+ },
435
+ // Dev-tool snapshots are whole app-state dumps (a dehydrated React Query
436
+ // cache, a Redux/Zustand store, an aggregated event log, etc.) and a device
437
+ // pushes one per watched tool on connect. socket.io v3+ caps inbound messages
438
+ // at 1 MB by default and TERMINATES the connection ("transport error") when a
439
+ // single message exceeds it — which, with a dashboard watching all ~11 tools
440
+ // at once, makes a device's connection flap. These are local, trusted dev
441
+ // connections, so lift the cap well clear of any realistic state dump.
442
+ maxHttpBufferSize: 1e8,
443
+ // 100 MB
444
+ transports: ["websocket", "polling"],
445
+ pingTimeout: 3e4,
446
+ pingInterval: 25e3
447
+ };
448
+ function createBroker(options = {}) {
449
+ const port = options.port ?? DEFAULT_BROKER_PORT;
450
+ const httpServer = import_http.default.createServer((_req, res) => {
451
+ res.writeHead(200, { "content-type": "text/plain" });
452
+ res.end(`Buoy sync broker running on port ${port}`);
453
+ });
454
+ const io = new import_socket.Server(httpServer, {
455
+ ...SOCKET_CONFIG,
456
+ ...options.socketOptions
457
+ });
458
+ socketHandle({ io });
459
+ httpServer.on("error", (error) => {
460
+ options.onError?.(error);
461
+ });
462
+ httpServer.listen(port, () => {
463
+ options.onListening?.(port);
464
+ });
465
+ const close = () => new Promise((resolve) => {
466
+ io.close(() => {
467
+ httpServer.close(() => resolve());
468
+ });
469
+ });
470
+ return { io, httpServer, port, close };
471
+ }
472
+
473
+ // src/protocol.ts
474
+ var SYNC_PROTOCOL_VERSION = 1;
475
+ // Annotate the CommonJS export names for ESM import in node:
476
+ 0 && (module.exports = {
477
+ DEFAULT_BROKER_PORT,
478
+ SOCKET_CONFIG,
479
+ SYNC_PROTOCOL_VERSION,
480
+ createBroker,
481
+ socketHandle
482
+ });