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