@buoy-gg/sync-broker 7.0.17 → 7.0.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/index.d.mts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +120 -48
- package/dist/index.mjs +120 -48
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -113,11 +113,31 @@ interface DevToolActionResultMessage {
|
|
|
113
113
|
/** Present when not ok. */
|
|
114
114
|
error?: string;
|
|
115
115
|
}
|
|
116
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Server-consolidated signal that a tool gained/lost its audience.
|
|
118
|
+
*
|
|
119
|
+
* `watching` arms CAPTURE (the device subscribes, which starts the tool's
|
|
120
|
+
* store recording); `stream` decides whether it also PUSHES a snapshot on
|
|
121
|
+
* every change. A dashboard wants both; a request/response client (MCP) wants
|
|
122
|
+
* only the first, and reads via `request-devtool-state`.
|
|
123
|
+
*
|
|
124
|
+
* The SERVER consolidates these across dashboards — see `handleDevtoolWatch`.
|
|
125
|
+
* A capture-only watcher must never downgrade a streaming one, or opening an
|
|
126
|
+
* agent session would silently freeze the desktop's panel.
|
|
127
|
+
*/
|
|
117
128
|
interface DevToolWatchMessage {
|
|
118
129
|
toolId: string;
|
|
119
130
|
targetDeviceId: string;
|
|
120
131
|
watching: boolean;
|
|
132
|
+
/** Push snapshots on change. Absent means TRUE (pre-`stream` dashboards). */
|
|
133
|
+
stream?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Set by the BROKER on the message it sends the device, meaning "this is the
|
|
136
|
+
* merged mode". Devices ignore `stream` without it, so an old broker (which
|
|
137
|
+
* forwards a dashboard's message verbatim) can't let one client's
|
|
138
|
+
* capture-only request quiet another client's live panel.
|
|
139
|
+
*/
|
|
140
|
+
consolidated?: boolean;
|
|
121
141
|
}
|
|
122
142
|
/** Explicit request for a tool's current state (works even while unwatched). */
|
|
123
143
|
interface DevToolRequestMessage {
|
package/dist/index.d.ts
CHANGED
|
@@ -113,11 +113,31 @@ interface DevToolActionResultMessage {
|
|
|
113
113
|
/** Present when not ok. */
|
|
114
114
|
error?: string;
|
|
115
115
|
}
|
|
116
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Server-consolidated signal that a tool gained/lost its audience.
|
|
118
|
+
*
|
|
119
|
+
* `watching` arms CAPTURE (the device subscribes, which starts the tool's
|
|
120
|
+
* store recording); `stream` decides whether it also PUSHES a snapshot on
|
|
121
|
+
* every change. A dashboard wants both; a request/response client (MCP) wants
|
|
122
|
+
* only the first, and reads via `request-devtool-state`.
|
|
123
|
+
*
|
|
124
|
+
* The SERVER consolidates these across dashboards — see `handleDevtoolWatch`.
|
|
125
|
+
* A capture-only watcher must never downgrade a streaming one, or opening an
|
|
126
|
+
* agent session would silently freeze the desktop's panel.
|
|
127
|
+
*/
|
|
117
128
|
interface DevToolWatchMessage {
|
|
118
129
|
toolId: string;
|
|
119
130
|
targetDeviceId: string;
|
|
120
131
|
watching: boolean;
|
|
132
|
+
/** Push snapshots on change. Absent means TRUE (pre-`stream` dashboards). */
|
|
133
|
+
stream?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Set by the BROKER on the message it sends the device, meaning "this is the
|
|
136
|
+
* merged mode". Devices ignore `stream` without it, so an old broker (which
|
|
137
|
+
* forwards a dashboard's message verbatim) can't let one client's
|
|
138
|
+
* capture-only request quiet another client's live panel.
|
|
139
|
+
*/
|
|
140
|
+
consolidated?: boolean;
|
|
121
141
|
}
|
|
122
142
|
/** Explicit request for a tool's current state (works even while unwatched). */
|
|
123
143
|
interface DevToolRequestMessage {
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,88 @@ module.exports = __toCommonJS(index_exports);
|
|
|
41
41
|
// src/protocol.ts
|
|
42
42
|
var SYNC_PROTOCOL_VERSION = 1;
|
|
43
43
|
|
|
44
|
+
// src/watchRegistry.ts
|
|
45
|
+
var toWatchKey = (targetDeviceId, toolId) => JSON.stringify([targetDeviceId, toolId]);
|
|
46
|
+
var fromWatchKey = (key) => JSON.parse(key);
|
|
47
|
+
var WatchRegistry = class {
|
|
48
|
+
constructor() {
|
|
49
|
+
/** Per dashboard: the key it watches → the mode it asked for. */
|
|
50
|
+
this.byDashboard = /* @__PURE__ */ new Map();
|
|
51
|
+
this.streamCounts = /* @__PURE__ */ new Map();
|
|
52
|
+
this.captureCounts = /* @__PURE__ */ new Map();
|
|
53
|
+
}
|
|
54
|
+
counts(mode) {
|
|
55
|
+
return mode === "stream" ? this.streamCounts : this.captureCounts;
|
|
56
|
+
}
|
|
57
|
+
bump(mode, key, delta) {
|
|
58
|
+
const counts = this.counts(mode);
|
|
59
|
+
const next = (counts.get(key) ?? 0) + delta;
|
|
60
|
+
if (next > 0) counts.set(key, next);
|
|
61
|
+
else counts.delete(key);
|
|
62
|
+
}
|
|
63
|
+
/** The strongest mode any dashboard currently wants; null for nobody. */
|
|
64
|
+
effective(key) {
|
|
65
|
+
if ((this.streamCounts.get(key) ?? 0) > 0) return "stream";
|
|
66
|
+
if ((this.captureCounts.get(key) ?? 0) > 0) return "capture";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** Every key with at least one watcher (for replay to a reconnecting device). */
|
|
70
|
+
activeKeys() {
|
|
71
|
+
return [
|
|
72
|
+
.../* @__PURE__ */ new Set([...this.streamCounts.keys(), ...this.captureCounts.keys()])
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Record a watch. Returns the mode to send the device — always non-null,
|
|
77
|
+
* because the device answers a watch with a snapshot and that is how a
|
|
78
|
+
* dashboard joining a quiet tool gets its first paint. Re-watching at the
|
|
79
|
+
* same mode is therefore NOT a no-op at the socket layer.
|
|
80
|
+
*/
|
|
81
|
+
watch(dashboardId, key, mode) {
|
|
82
|
+
let held = this.byDashboard.get(dashboardId);
|
|
83
|
+
if (!held) {
|
|
84
|
+
held = /* @__PURE__ */ new Map();
|
|
85
|
+
this.byDashboard.set(dashboardId, held);
|
|
86
|
+
}
|
|
87
|
+
const previous = held.get(key);
|
|
88
|
+
if (previous !== mode) {
|
|
89
|
+
if (previous) this.bump(previous, key, -1);
|
|
90
|
+
this.bump(mode, key, 1);
|
|
91
|
+
held.set(key, mode);
|
|
92
|
+
}
|
|
93
|
+
return this.effective(key);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Drop one dashboard's claim. Returns the new effective mode ONLY when it
|
|
97
|
+
* changed — the device is left alone while someone else still wants the
|
|
98
|
+
* same thing.
|
|
99
|
+
*/
|
|
100
|
+
unwatch(dashboardId, key) {
|
|
101
|
+
const held = this.byDashboard.get(dashboardId);
|
|
102
|
+
const mode = held?.get(key);
|
|
103
|
+
if (!held || !mode) return { changed: false, mode: this.effective(key) };
|
|
104
|
+
const before = this.effective(key);
|
|
105
|
+
held.delete(key);
|
|
106
|
+
this.bump(mode, key, -1);
|
|
107
|
+
const after = this.effective(key);
|
|
108
|
+
return { changed: after !== before, mode: after };
|
|
109
|
+
}
|
|
110
|
+
/** Release everything a disconnecting dashboard held. */
|
|
111
|
+
releaseDashboard(dashboardId) {
|
|
112
|
+
const held = this.byDashboard.get(dashboardId);
|
|
113
|
+
if (!held) return [];
|
|
114
|
+
this.byDashboard.delete(dashboardId);
|
|
115
|
+
const changes = [];
|
|
116
|
+
for (const [key, mode] of held) {
|
|
117
|
+
const before = this.effective(key);
|
|
118
|
+
this.bump(mode, key, -1);
|
|
119
|
+
const after = this.effective(key);
|
|
120
|
+
if (after !== before) changes.push({ key, mode: after });
|
|
121
|
+
}
|
|
122
|
+
return changes;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
44
126
|
// src/socketHandle.ts
|
|
45
127
|
var users = [];
|
|
46
128
|
var allDevices = [];
|
|
@@ -48,10 +130,7 @@ var dashboardClients = [];
|
|
|
48
130
|
var deviceIdMap = /* @__PURE__ */ new Map();
|
|
49
131
|
var activeConnectionsMap = /* @__PURE__ */ new Map();
|
|
50
132
|
var deviceCapabilities = /* @__PURE__ */ new Map();
|
|
51
|
-
var
|
|
52
|
-
var watchCounts = /* @__PURE__ */ new Map();
|
|
53
|
-
var toWatchKey = (targetDeviceId, toolId) => JSON.stringify([targetDeviceId, toolId]);
|
|
54
|
-
var fromWatchKey = (key) => JSON.parse(key);
|
|
133
|
+
var watches = new WatchRegistry();
|
|
55
134
|
var brokerLogBuffer = [];
|
|
56
135
|
var BROKER_LOG_BUFFER_MAX = 200;
|
|
57
136
|
var MAX_OFFLINE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -299,54 +378,42 @@ function socketHandle({ io }) {
|
|
|
299
378
|
}
|
|
300
379
|
}
|
|
301
380
|
}
|
|
381
|
+
function emitWatchState(key, mode) {
|
|
382
|
+
const [targetDeviceId, toolId] = fromWatchKey(key);
|
|
383
|
+
const message = mode === null ? { toolId, targetDeviceId, watching: false, consolidated: true } : {
|
|
384
|
+
toolId,
|
|
385
|
+
targetDeviceId,
|
|
386
|
+
watching: true,
|
|
387
|
+
stream: mode === "stream",
|
|
388
|
+
consolidated: true
|
|
389
|
+
};
|
|
390
|
+
withTargetUsers(
|
|
391
|
+
targetDeviceId,
|
|
392
|
+
(user) => io.to(user.id).emit("devtool-watch", message),
|
|
393
|
+
`devtool ${mode === null ? "unwatch" : `watch(${mode})`} ${toolId}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
302
396
|
function handleDevtoolWatch(dashboardId, message) {
|
|
303
397
|
const key = toWatchKey(message.targetDeviceId, message.toolId);
|
|
304
|
-
let watches = dashboardWatches.get(dashboardId);
|
|
305
|
-
if (!watches) {
|
|
306
|
-
watches = /* @__PURE__ */ new Set();
|
|
307
|
-
dashboardWatches.set(dashboardId, watches);
|
|
308
|
-
}
|
|
309
398
|
if (message.watching) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
watchCounts.set(key, (watchCounts.get(key) ?? 0) + 1);
|
|
313
|
-
}
|
|
399
|
+
const mode2 = message.stream === false ? "capture" : "stream";
|
|
400
|
+
const effective = watches.watch(dashboardId, key, mode2);
|
|
314
401
|
console.log(
|
|
315
|
-
`${LOG_PREFIX} Dashboard ${dashboardId} watching "${message.toolId}" on ${message.targetDeviceId}`
|
|
316
|
-
);
|
|
317
|
-
withTargetUsers(
|
|
318
|
-
message.targetDeviceId,
|
|
319
|
-
(user) => io.to(user.id).emit("devtool-watch", message),
|
|
320
|
-
`devtool watch ${message.toolId}`
|
|
402
|
+
`${LOG_PREFIX} Dashboard ${dashboardId} watching "${message.toolId}" on ${message.targetDeviceId} (${mode2}, effective ${effective})`
|
|
321
403
|
);
|
|
322
|
-
|
|
323
|
-
watches.delete(key);
|
|
324
|
-
decrementWatch(key);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
function decrementWatch(key) {
|
|
328
|
-
const next = (watchCounts.get(key) ?? 1) - 1;
|
|
329
|
-
if (next > 0) {
|
|
330
|
-
watchCounts.set(key, next);
|
|
404
|
+
emitWatchState(key, effective);
|
|
331
405
|
return;
|
|
332
406
|
}
|
|
333
|
-
|
|
334
|
-
|
|
407
|
+
const { changed, mode } = watches.unwatch(dashboardId, key);
|
|
408
|
+
if (!changed) return;
|
|
335
409
|
console.log(
|
|
336
|
-
`${LOG_PREFIX} Last dashboard stopped watching "${toolId}" on ${targetDeviceId}`
|
|
337
|
-
);
|
|
338
|
-
withTargetUsers(
|
|
339
|
-
targetDeviceId,
|
|
340
|
-
(user) => io.to(user.id).emit("devtool-watch", { toolId, targetDeviceId, watching: false }),
|
|
341
|
-
`devtool unwatch ${toolId}`
|
|
410
|
+
mode === null ? `${LOG_PREFIX} Last dashboard stopped watching "${message.toolId}" on ${message.targetDeviceId}` : `${LOG_PREFIX} "${message.toolId}" on ${message.targetDeviceId} downgraded to ${mode}`
|
|
342
411
|
);
|
|
412
|
+
emitWatchState(key, mode);
|
|
343
413
|
}
|
|
344
414
|
function releaseDashboardWatches(dashboardId) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
dashboardWatches.delete(dashboardId);
|
|
348
|
-
for (const key of watches) {
|
|
349
|
-
decrementWatch(key);
|
|
415
|
+
for (const { key, mode } of watches.releaseDashboard(dashboardId)) {
|
|
416
|
+
emitWatchState(key, mode);
|
|
350
417
|
}
|
|
351
418
|
}
|
|
352
419
|
io.on("connection", (socket) => {
|
|
@@ -424,15 +491,20 @@ function socketHandle({ io }) {
|
|
|
424
491
|
}
|
|
425
492
|
deviceCapabilities.set(message.persistentDeviceId, message);
|
|
426
493
|
forwardToDashboards("devtool-capabilities", message);
|
|
427
|
-
for (const key of
|
|
494
|
+
for (const key of watches.activeKeys()) {
|
|
428
495
|
const [targetDeviceId, toolId] = fromWatchKey(key);
|
|
429
|
-
if (targetDeviceId
|
|
430
|
-
|
|
431
|
-
toolId,
|
|
432
|
-
targetDeviceId,
|
|
433
|
-
watching: true
|
|
434
|
-
});
|
|
496
|
+
if (targetDeviceId !== "All" && targetDeviceId !== message.persistentDeviceId) {
|
|
497
|
+
continue;
|
|
435
498
|
}
|
|
499
|
+
const mode = watches.effective(key);
|
|
500
|
+
if (!mode) continue;
|
|
501
|
+
socket.emit("devtool-watch", {
|
|
502
|
+
toolId,
|
|
503
|
+
targetDeviceId,
|
|
504
|
+
watching: true,
|
|
505
|
+
stream: mode === "stream",
|
|
506
|
+
consolidated: true
|
|
507
|
+
});
|
|
436
508
|
}
|
|
437
509
|
});
|
|
438
510
|
socket.on("devtool-sync", (message) => {
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,88 @@
|
|
|
1
1
|
// src/protocol.ts
|
|
2
2
|
var SYNC_PROTOCOL_VERSION = 1;
|
|
3
3
|
|
|
4
|
+
// src/watchRegistry.ts
|
|
5
|
+
var toWatchKey = (targetDeviceId, toolId) => JSON.stringify([targetDeviceId, toolId]);
|
|
6
|
+
var fromWatchKey = (key) => JSON.parse(key);
|
|
7
|
+
var WatchRegistry = class {
|
|
8
|
+
constructor() {
|
|
9
|
+
/** Per dashboard: the key it watches → the mode it asked for. */
|
|
10
|
+
this.byDashboard = /* @__PURE__ */ new Map();
|
|
11
|
+
this.streamCounts = /* @__PURE__ */ new Map();
|
|
12
|
+
this.captureCounts = /* @__PURE__ */ new Map();
|
|
13
|
+
}
|
|
14
|
+
counts(mode) {
|
|
15
|
+
return mode === "stream" ? this.streamCounts : this.captureCounts;
|
|
16
|
+
}
|
|
17
|
+
bump(mode, key, delta) {
|
|
18
|
+
const counts = this.counts(mode);
|
|
19
|
+
const next = (counts.get(key) ?? 0) + delta;
|
|
20
|
+
if (next > 0) counts.set(key, next);
|
|
21
|
+
else counts.delete(key);
|
|
22
|
+
}
|
|
23
|
+
/** The strongest mode any dashboard currently wants; null for nobody. */
|
|
24
|
+
effective(key) {
|
|
25
|
+
if ((this.streamCounts.get(key) ?? 0) > 0) return "stream";
|
|
26
|
+
if ((this.captureCounts.get(key) ?? 0) > 0) return "capture";
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/** Every key with at least one watcher (for replay to a reconnecting device). */
|
|
30
|
+
activeKeys() {
|
|
31
|
+
return [
|
|
32
|
+
.../* @__PURE__ */ new Set([...this.streamCounts.keys(), ...this.captureCounts.keys()])
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Record a watch. Returns the mode to send the device — always non-null,
|
|
37
|
+
* because the device answers a watch with a snapshot and that is how a
|
|
38
|
+
* dashboard joining a quiet tool gets its first paint. Re-watching at the
|
|
39
|
+
* same mode is therefore NOT a no-op at the socket layer.
|
|
40
|
+
*/
|
|
41
|
+
watch(dashboardId, key, mode) {
|
|
42
|
+
let held = this.byDashboard.get(dashboardId);
|
|
43
|
+
if (!held) {
|
|
44
|
+
held = /* @__PURE__ */ new Map();
|
|
45
|
+
this.byDashboard.set(dashboardId, held);
|
|
46
|
+
}
|
|
47
|
+
const previous = held.get(key);
|
|
48
|
+
if (previous !== mode) {
|
|
49
|
+
if (previous) this.bump(previous, key, -1);
|
|
50
|
+
this.bump(mode, key, 1);
|
|
51
|
+
held.set(key, mode);
|
|
52
|
+
}
|
|
53
|
+
return this.effective(key);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Drop one dashboard's claim. Returns the new effective mode ONLY when it
|
|
57
|
+
* changed — the device is left alone while someone else still wants the
|
|
58
|
+
* same thing.
|
|
59
|
+
*/
|
|
60
|
+
unwatch(dashboardId, key) {
|
|
61
|
+
const held = this.byDashboard.get(dashboardId);
|
|
62
|
+
const mode = held?.get(key);
|
|
63
|
+
if (!held || !mode) return { changed: false, mode: this.effective(key) };
|
|
64
|
+
const before = this.effective(key);
|
|
65
|
+
held.delete(key);
|
|
66
|
+
this.bump(mode, key, -1);
|
|
67
|
+
const after = this.effective(key);
|
|
68
|
+
return { changed: after !== before, mode: after };
|
|
69
|
+
}
|
|
70
|
+
/** Release everything a disconnecting dashboard held. */
|
|
71
|
+
releaseDashboard(dashboardId) {
|
|
72
|
+
const held = this.byDashboard.get(dashboardId);
|
|
73
|
+
if (!held) return [];
|
|
74
|
+
this.byDashboard.delete(dashboardId);
|
|
75
|
+
const changes = [];
|
|
76
|
+
for (const [key, mode] of held) {
|
|
77
|
+
const before = this.effective(key);
|
|
78
|
+
this.bump(mode, key, -1);
|
|
79
|
+
const after = this.effective(key);
|
|
80
|
+
if (after !== before) changes.push({ key, mode: after });
|
|
81
|
+
}
|
|
82
|
+
return changes;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
4
86
|
// src/socketHandle.ts
|
|
5
87
|
var users = [];
|
|
6
88
|
var allDevices = [];
|
|
@@ -8,10 +90,7 @@ var dashboardClients = [];
|
|
|
8
90
|
var deviceIdMap = /* @__PURE__ */ new Map();
|
|
9
91
|
var activeConnectionsMap = /* @__PURE__ */ new Map();
|
|
10
92
|
var deviceCapabilities = /* @__PURE__ */ new Map();
|
|
11
|
-
var
|
|
12
|
-
var watchCounts = /* @__PURE__ */ new Map();
|
|
13
|
-
var toWatchKey = (targetDeviceId, toolId) => JSON.stringify([targetDeviceId, toolId]);
|
|
14
|
-
var fromWatchKey = (key) => JSON.parse(key);
|
|
93
|
+
var watches = new WatchRegistry();
|
|
15
94
|
var brokerLogBuffer = [];
|
|
16
95
|
var BROKER_LOG_BUFFER_MAX = 200;
|
|
17
96
|
var MAX_OFFLINE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -259,54 +338,42 @@ function socketHandle({ io }) {
|
|
|
259
338
|
}
|
|
260
339
|
}
|
|
261
340
|
}
|
|
341
|
+
function emitWatchState(key, mode) {
|
|
342
|
+
const [targetDeviceId, toolId] = fromWatchKey(key);
|
|
343
|
+
const message = mode === null ? { toolId, targetDeviceId, watching: false, consolidated: true } : {
|
|
344
|
+
toolId,
|
|
345
|
+
targetDeviceId,
|
|
346
|
+
watching: true,
|
|
347
|
+
stream: mode === "stream",
|
|
348
|
+
consolidated: true
|
|
349
|
+
};
|
|
350
|
+
withTargetUsers(
|
|
351
|
+
targetDeviceId,
|
|
352
|
+
(user) => io.to(user.id).emit("devtool-watch", message),
|
|
353
|
+
`devtool ${mode === null ? "unwatch" : `watch(${mode})`} ${toolId}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
262
356
|
function handleDevtoolWatch(dashboardId, message) {
|
|
263
357
|
const key = toWatchKey(message.targetDeviceId, message.toolId);
|
|
264
|
-
let watches = dashboardWatches.get(dashboardId);
|
|
265
|
-
if (!watches) {
|
|
266
|
-
watches = /* @__PURE__ */ new Set();
|
|
267
|
-
dashboardWatches.set(dashboardId, watches);
|
|
268
|
-
}
|
|
269
358
|
if (message.watching) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
watchCounts.set(key, (watchCounts.get(key) ?? 0) + 1);
|
|
273
|
-
}
|
|
359
|
+
const mode2 = message.stream === false ? "capture" : "stream";
|
|
360
|
+
const effective = watches.watch(dashboardId, key, mode2);
|
|
274
361
|
console.log(
|
|
275
|
-
`${LOG_PREFIX} Dashboard ${dashboardId} watching "${message.toolId}" on ${message.targetDeviceId}`
|
|
276
|
-
);
|
|
277
|
-
withTargetUsers(
|
|
278
|
-
message.targetDeviceId,
|
|
279
|
-
(user) => io.to(user.id).emit("devtool-watch", message),
|
|
280
|
-
`devtool watch ${message.toolId}`
|
|
362
|
+
`${LOG_PREFIX} Dashboard ${dashboardId} watching "${message.toolId}" on ${message.targetDeviceId} (${mode2}, effective ${effective})`
|
|
281
363
|
);
|
|
282
|
-
|
|
283
|
-
watches.delete(key);
|
|
284
|
-
decrementWatch(key);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
function decrementWatch(key) {
|
|
288
|
-
const next = (watchCounts.get(key) ?? 1) - 1;
|
|
289
|
-
if (next > 0) {
|
|
290
|
-
watchCounts.set(key, next);
|
|
364
|
+
emitWatchState(key, effective);
|
|
291
365
|
return;
|
|
292
366
|
}
|
|
293
|
-
|
|
294
|
-
|
|
367
|
+
const { changed, mode } = watches.unwatch(dashboardId, key);
|
|
368
|
+
if (!changed) return;
|
|
295
369
|
console.log(
|
|
296
|
-
`${LOG_PREFIX} Last dashboard stopped watching "${toolId}" on ${targetDeviceId}`
|
|
297
|
-
);
|
|
298
|
-
withTargetUsers(
|
|
299
|
-
targetDeviceId,
|
|
300
|
-
(user) => io.to(user.id).emit("devtool-watch", { toolId, targetDeviceId, watching: false }),
|
|
301
|
-
`devtool unwatch ${toolId}`
|
|
370
|
+
mode === null ? `${LOG_PREFIX} Last dashboard stopped watching "${message.toolId}" on ${message.targetDeviceId}` : `${LOG_PREFIX} "${message.toolId}" on ${message.targetDeviceId} downgraded to ${mode}`
|
|
302
371
|
);
|
|
372
|
+
emitWatchState(key, mode);
|
|
303
373
|
}
|
|
304
374
|
function releaseDashboardWatches(dashboardId) {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
dashboardWatches.delete(dashboardId);
|
|
308
|
-
for (const key of watches) {
|
|
309
|
-
decrementWatch(key);
|
|
375
|
+
for (const { key, mode } of watches.releaseDashboard(dashboardId)) {
|
|
376
|
+
emitWatchState(key, mode);
|
|
310
377
|
}
|
|
311
378
|
}
|
|
312
379
|
io.on("connection", (socket) => {
|
|
@@ -384,15 +451,20 @@ function socketHandle({ io }) {
|
|
|
384
451
|
}
|
|
385
452
|
deviceCapabilities.set(message.persistentDeviceId, message);
|
|
386
453
|
forwardToDashboards("devtool-capabilities", message);
|
|
387
|
-
for (const key of
|
|
454
|
+
for (const key of watches.activeKeys()) {
|
|
388
455
|
const [targetDeviceId, toolId] = fromWatchKey(key);
|
|
389
|
-
if (targetDeviceId
|
|
390
|
-
|
|
391
|
-
toolId,
|
|
392
|
-
targetDeviceId,
|
|
393
|
-
watching: true
|
|
394
|
-
});
|
|
456
|
+
if (targetDeviceId !== "All" && targetDeviceId !== message.persistentDeviceId) {
|
|
457
|
+
continue;
|
|
395
458
|
}
|
|
459
|
+
const mode = watches.effective(key);
|
|
460
|
+
if (!mode) continue;
|
|
461
|
+
socket.emit("devtool-watch", {
|
|
462
|
+
toolId,
|
|
463
|
+
targetDeviceId,
|
|
464
|
+
watching: true,
|
|
465
|
+
stream: mode === "stream",
|
|
466
|
+
consolidated: true
|
|
467
|
+
});
|
|
396
468
|
}
|
|
397
469
|
});
|
|
398
470
|
socket.on("devtool-sync", (message) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/sync-broker",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.19",
|
|
4
4
|
"description": "Headless Socket.IO broker for the Buoy devtool sync protocol (v1) — routes messages between devices and dashboard/MCP clients",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|