@buoy-gg/external-sync 7.0.36 → 7.0.38
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/README.md +79 -0
- package/lib/commonjs/attachDeviceSync.js +64 -6
- package/lib/commonjs/brokerAccess.js +29 -0
- package/lib/commonjs/useExternalSync.js +8 -2
- package/lib/commonjs/useExternalSyncSocket.js +48 -61
- package/lib/module/attachDeviceSync.js +64 -6
- package/lib/module/brokerAccess.js +25 -0
- package/lib/module/useExternalSync.js +8 -2
- package/lib/module/useExternalSyncSocket.js +48 -61
- package/lib/typescript/attachDeviceSync.d.ts.map +1 -1
- package/lib/typescript/brokerAccess.d.ts +9 -0
- package/lib/typescript/brokerAccess.d.ts.map +1 -0
- package/lib/typescript/useExternalSync.d.ts.map +1 -1
- package/lib/typescript/useExternalSyncSocket.d.ts +1 -1
- package/lib/typescript/useExternalSyncSocket.d.ts.map +1 -1
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @buoy-gg/external-sync
|
|
2
|
+
|
|
3
|
+
Connect a React Native app's Buoy tools to Buoy Desktop or MCP. This package provides the device-side socket connection, adapter synchronization, and remote-action handling. Most apps configure it through `FloatingDevTools` from `@buoy-gg/core`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @buoy-gg/core @buoy-gg/external-sync @buoy-gg/network
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Account setup
|
|
12
|
+
|
|
13
|
+
Start in a development build with a Free or Pro Buoy account key. From the app directory:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx --package=@buoy-gg/core buoy login
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For Expo, initialize Buoy with the key written to `.env.local`:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { Buoy } from "@buoy-gg/core";
|
|
23
|
+
|
|
24
|
+
Buoy.init({ licenseKey: process.env.EXPO_PUBLIC_BUOY_KEY });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For React Native CLI, pass the key through your app's environment configuration; `.env.local` is not loaded automatically. Keep the menu inside the providers used by your app. Restart the development server after installing packages. See the [Quick Start](https://buoy.gg/buoy/latest/docs/quick-start) for the complete root component.
|
|
28
|
+
|
|
29
|
+
## Connect your app
|
|
30
|
+
|
|
31
|
+
Mount the menu in your existing root component after account initialization:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { FloatingDevTools } from "@buoy-gg/core";
|
|
35
|
+
|
|
36
|
+
<FloatingDevTools />;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Open Desktop and sign in. In development, the app normally derives the broker address from Metro. If your network or Expo tunnel setup needs an explicit address, replace the menu above with:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<FloatingDevTools
|
|
43
|
+
externalSync={{ socketURL: "http://YOUR_COMPUTER_LAN_IP:42831" }}
|
|
44
|
+
/>;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`socketURL` has an uppercase URL. The device must be able to reach that address; a Metro tunnel does not automatically expose the broker. For Android over USB, follow the port-forwarding instructions in the [Desktop guide](https://buoy.gg/buoy/latest/docs/desktop).
|
|
48
|
+
|
|
49
|
+
Select the device in Desktop, trigger a fresh HTTP request in the app, and check that it appears in Network. If no device appears, check both account states, the broker address, and network connectivity. If the device appears but the panel is empty, check the tool's installation and capture path.
|
|
50
|
+
|
|
51
|
+
## Headless and release builds
|
|
52
|
+
|
|
53
|
+
`<FloatingDevTools headless />` keeps the connection integration without the floating menu. It still needs account access and configured tools.
|
|
54
|
+
|
|
55
|
+
Release sync is disabled by default. For an authorized internal release build with Pro, explicitly configure it:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<FloatingDevTools
|
|
59
|
+
headless
|
|
60
|
+
externalSync={{
|
|
61
|
+
enableInRelease: true,
|
|
62
|
+
socketURL: "http://YOUR_COMPUTER_LAN_IP:42831",
|
|
63
|
+
}}
|
|
64
|
+
/>;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Apply your app's authorization checks before mounting this integration. Enabling sync does not make development-only tools work in release builds.
|
|
68
|
+
|
|
69
|
+
## Custom integrations
|
|
70
|
+
|
|
71
|
+
The public exports include `useExternalSyncSocket`, `useExternalSync`, `ToolSyncAdapter`, URL helpers, sync-status subscriptions, and crash-flush helpers. An adapter supplies snapshots, subscriptions, and named actions. The bridge subscribes according to dashboard watch state and throttles updates; it also enforces device account admission.
|
|
72
|
+
|
|
73
|
+
Prefer the core integration unless you are building a custom host. A direct hook integration must manage socket and device identity, account initialization, adapter lifecycle, and action authorization. Use the exported option types and [custom-tool guide](https://buoy.gg/buoy/latest/docs/custom-tools).
|
|
74
|
+
|
|
75
|
+
## Desktop and MCP
|
|
76
|
+
|
|
77
|
+
React Native connections require `@buoy-gg/external-sync`. Follow the [Desktop connection guide](https://buoy.gg/buoy/latest/docs/desktop), sign in to Desktop separately, and confirm the selected device. [MCP](https://buoy.gg/buoy/latest/docs/mcp) also needs a process account and Pro access.
|
|
78
|
+
|
|
79
|
+
Device data can travel over your LAN to the configured broker. Account validation makes network requests. See [Telemetry](https://buoy.gg/buoy/latest/docs/telemetry) for data-flow details.
|
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.attachDeviceSync = attachDeviceSync;
|
|
7
|
+
var _license = require("@buoy-gg/license");
|
|
7
8
|
var _logger = require("./utils/logger");
|
|
8
9
|
var _crashFlush = require("./crashFlush");
|
|
9
10
|
var _emitBudget = require("./emitBudget");
|
|
@@ -29,11 +30,16 @@ function attachDeviceSync({
|
|
|
29
30
|
throttleMs = 200,
|
|
30
31
|
enableLogs = false
|
|
31
32
|
}) {
|
|
33
|
+
let disposed = false;
|
|
34
|
+
let brokerReady = false;
|
|
35
|
+
let accessGeneration = 0;
|
|
36
|
+
const allowed = () => !disposed && brokerReady && socket.connected && _license.LicenseManager.hasAccountAccess();
|
|
32
37
|
const unsubs = new Map();
|
|
33
38
|
const lastEmit = {};
|
|
34
39
|
const timers = {};
|
|
35
40
|
const isForThisDevice = targetDeviceId => targetDeviceId === "All" || targetDeviceId === deviceId;
|
|
36
41
|
const safeEmit = (event, message) => {
|
|
42
|
+
if (!allowed()) return;
|
|
37
43
|
try {
|
|
38
44
|
socket.emit(event, message);
|
|
39
45
|
} catch (error) {
|
|
@@ -42,10 +48,16 @@ function attachDeviceSync({
|
|
|
42
48
|
};
|
|
43
49
|
const warnedOversized = new Set();
|
|
44
50
|
const sendSnapshot = toolId => {
|
|
51
|
+
if (!allowed()) return;
|
|
45
52
|
const tool = getTools()[toolId];
|
|
46
53
|
if (!tool) return;
|
|
47
54
|
lastEmit[toolId] = Date.now();
|
|
48
|
-
|
|
55
|
+
let payload;
|
|
56
|
+
try {
|
|
57
|
+
payload = tool.getSnapshot();
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
49
61
|
const {
|
|
50
62
|
over,
|
|
51
63
|
approxBytes
|
|
@@ -108,6 +120,7 @@ function attachDeviceSync({
|
|
|
108
120
|
/** The mode each currently-subscribed tool is wired for. */
|
|
109
121
|
const activeModes = new Map();
|
|
110
122
|
const watchTool = (toolId, mode) => {
|
|
123
|
+
if (!allowed()) return;
|
|
111
124
|
const tool = getTools()[toolId];
|
|
112
125
|
if (!tool) return;
|
|
113
126
|
if (unsubs.has(toolId)) {
|
|
@@ -149,7 +162,7 @@ function attachDeviceSync({
|
|
|
149
162
|
safeEmit("devtool-capabilities", message);
|
|
150
163
|
};
|
|
151
164
|
const handleWatch = message => {
|
|
152
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
165
|
+
if (!allowed() || !isForThisDevice(message.targetDeviceId)) return;
|
|
153
166
|
if (message.watching) {
|
|
154
167
|
// `stream` is only trustworthy once a broker has merged every
|
|
155
168
|
// dashboard's request (see DevToolWatchMessage.consolidated). Without
|
|
@@ -168,11 +181,12 @@ function attachDeviceSync({
|
|
|
168
181
|
}
|
|
169
182
|
};
|
|
170
183
|
const handleRequest = message => {
|
|
171
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
184
|
+
if (!allowed() || !isForThisDevice(message.targetDeviceId)) return;
|
|
172
185
|
sendSnapshot(message.toolId);
|
|
173
186
|
};
|
|
174
187
|
const handleAction = async message => {
|
|
175
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
188
|
+
if (!allowed() || !isForThisDevice(message.targetDeviceId)) return;
|
|
189
|
+
const startedAt = accessGeneration;
|
|
176
190
|
const {
|
|
177
191
|
requestId,
|
|
178
192
|
toolId,
|
|
@@ -200,6 +214,9 @@ function attachDeviceSync({
|
|
|
200
214
|
} catch (error) {
|
|
201
215
|
result.error = error instanceof Error ? error.message : String(error);
|
|
202
216
|
}
|
|
217
|
+
|
|
218
|
+
// A subsequent admission cannot authorize work from the previous session.
|
|
219
|
+
if (startedAt !== accessGeneration || !allowed()) return;
|
|
203
220
|
if (result.ok) {
|
|
204
221
|
const {
|
|
205
222
|
over,
|
|
@@ -214,20 +231,61 @@ function attachDeviceSync({
|
|
|
214
231
|
result.timestamp = Date.now();
|
|
215
232
|
safeEmit("devtool-action-result", result);
|
|
216
233
|
};
|
|
217
|
-
const
|
|
234
|
+
const dropToolWork = () => {
|
|
235
|
+
accessGeneration += 1;
|
|
236
|
+
for (const toolId of [...unsubs.keys()]) unwatchTool(toolId);
|
|
237
|
+
watched.clear();
|
|
238
|
+
for (const timer of Object.values(timers)) clearTimeout(timer);
|
|
239
|
+
const buffered = socket;
|
|
240
|
+
if (buffered.sendBuffer) buffered.sendBuffer = buffered.sendBuffer.filter(packet => !["devtool-sync", "devtool-capabilities", "devtool-action-result", "device-license"].includes(String(packet.data?.[0])));
|
|
241
|
+
};
|
|
242
|
+
const announceAccount = () => {
|
|
243
|
+
const state = _license.LicenseManager.getState();
|
|
244
|
+
socket.emit("device-license", {
|
|
245
|
+
tier: state.tier,
|
|
246
|
+
isPro: state.isPro ? "1" : "0"
|
|
247
|
+
});
|
|
218
248
|
sendCapabilities();
|
|
219
249
|
};
|
|
220
|
-
|
|
250
|
+
const handleAccess = message => {
|
|
251
|
+
brokerReady = message?.protocol === 1 && message.ready === true;
|
|
252
|
+
if (!allowed()) dropToolWork();else announceAccount();
|
|
253
|
+
};
|
|
254
|
+
const offAccount = _license.LicenseManager.subscribe(() => {
|
|
255
|
+
if (!_license.LicenseManager.hasAccountAccess()) {
|
|
256
|
+
if (socket.connected) socket.emit("device-license", {
|
|
257
|
+
tier: "locked",
|
|
258
|
+
isPro: "0"
|
|
259
|
+
});
|
|
260
|
+
dropToolWork();
|
|
261
|
+
} else if (allowed()) announceAccount();
|
|
262
|
+
});
|
|
263
|
+
const handleConnect = () => {
|
|
264
|
+
brokerReady = false;
|
|
265
|
+
socket.emit("buoy:access:request");
|
|
266
|
+
};
|
|
267
|
+
const handleDisconnect = () => {
|
|
268
|
+
brokerReady = false;
|
|
269
|
+
dropToolWork();
|
|
270
|
+
};
|
|
271
|
+
socket.on("buoy:access", handleAccess);
|
|
221
272
|
for (const [toolId, mode] of watched) {
|
|
222
273
|
if (getTools()[toolId]) watchTool(toolId, mode);
|
|
223
274
|
}
|
|
224
275
|
socket.on("connect", handleConnect);
|
|
276
|
+
socket.on("disconnect", handleDisconnect);
|
|
225
277
|
socket.on("devtool-watch", handleWatch);
|
|
226
278
|
socket.on("request-devtool-state", handleRequest);
|
|
227
279
|
socket.on("devtool-action", handleAction);
|
|
280
|
+
if (socket.connected) handleConnect();
|
|
228
281
|
return () => {
|
|
282
|
+
disposed = true;
|
|
283
|
+
offAccount();
|
|
284
|
+
socket.off("buoy:access", handleAccess);
|
|
285
|
+
dropToolWork();
|
|
229
286
|
(0, _crashFlush.unregisterToolFlusher)(flushNow);
|
|
230
287
|
socket.off("connect", handleConnect);
|
|
288
|
+
socket.off("disconnect", handleDisconnect);
|
|
231
289
|
socket.off("devtool-watch", handleWatch);
|
|
232
290
|
socket.off("request-devtool-state", handleRequest);
|
|
233
291
|
socket.off("devtool-action", handleAction);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.watchBrokerAccess = watchBrokerAccess;
|
|
7
|
+
/** Wait for an account-aware broker before publishing any tool capabilities. */
|
|
8
|
+
|
|
9
|
+
function watchBrokerAccess(socket, update) {
|
|
10
|
+
const lock = () => update(false);
|
|
11
|
+
const request = () => {
|
|
12
|
+
lock();
|
|
13
|
+
socket.emit("buoy:access:request");
|
|
14
|
+
};
|
|
15
|
+
const access = message => {
|
|
16
|
+
const info = message;
|
|
17
|
+
update(info?.protocol === 1 && info?.ready === true);
|
|
18
|
+
};
|
|
19
|
+
socket.on("connect", request);
|
|
20
|
+
socket.on("disconnect", lock);
|
|
21
|
+
socket.on("buoy:access", access);
|
|
22
|
+
lock();
|
|
23
|
+
if (socket.connected) request();
|
|
24
|
+
return () => {
|
|
25
|
+
socket.off("connect", request);
|
|
26
|
+
socket.off("disconnect", lock);
|
|
27
|
+
socket.off("buoy:access", access);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.useExternalSync = useExternalSync;
|
|
7
|
+
var _license = require("@buoy-gg/license");
|
|
7
8
|
var _react = require("react");
|
|
8
9
|
var _attachDeviceSync = require("./attachDeviceSync");
|
|
9
10
|
/**
|
|
@@ -41,6 +42,8 @@ function useExternalSync({
|
|
|
41
42
|
throttleMs = 200,
|
|
42
43
|
enableLogs = false
|
|
43
44
|
}) {
|
|
45
|
+
const account = (0, _license.useAccountAccess)();
|
|
46
|
+
const admitted = account.status === "admitted";
|
|
44
47
|
const toolsRef = (0, _react.useRef)(tools);
|
|
45
48
|
toolsRef.current = tools;
|
|
46
49
|
|
|
@@ -52,7 +55,10 @@ function useExternalSync({
|
|
|
52
55
|
// object-reference change.
|
|
53
56
|
const toolIdsKey = Object.keys(tools).sort().join(",");
|
|
54
57
|
(0, _react.useEffect)(() => {
|
|
55
|
-
if (!socket || !deviceId || !enabled)
|
|
58
|
+
if (!socket || !deviceId || !enabled || !admitted) {
|
|
59
|
+
watchedRef.current.clear();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
56
62
|
return (0, _attachDeviceSync.attachDeviceSync)({
|
|
57
63
|
socket,
|
|
58
64
|
deviceId,
|
|
@@ -63,5 +69,5 @@ function useExternalSync({
|
|
|
63
69
|
});
|
|
64
70
|
// toolIdsKey stands in for `tools` (see comment above its definition).
|
|
65
71
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
-
}, [socket, deviceId, enabled, throttleMs, enableLogs, toolIdsKey]);
|
|
72
|
+
}, [socket, deviceId, enabled, admitted, throttleMs, enableLogs, toolIdsKey]);
|
|
67
73
|
}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.buildHandshakeQuery = buildHandshakeQuery;
|
|
7
7
|
exports.getInstanceId = getInstanceId;
|
|
8
8
|
exports.useExternalSyncSocket = useExternalSyncSocket;
|
|
9
|
+
var _brokerAccess = require("./brokerAccess");
|
|
9
10
|
var _react = require("react");
|
|
10
11
|
var _socket = require("socket.io-client");
|
|
11
12
|
var _platformUtils = require("./utils/platformUtils");
|
|
@@ -22,6 +23,8 @@ const isDev = typeof __DEV__ !== "undefined" ? __DEV__ : false;
|
|
|
22
23
|
*/
|
|
23
24
|
let globalSocketInstance = null;
|
|
24
25
|
let currentSocketURL = "";
|
|
26
|
+
let currentDeviceId = "";
|
|
27
|
+
let currentPlatform = "";
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Identifies a single socket INSTANCE. Sent in the handshake query and reused
|
|
@@ -52,12 +55,13 @@ function getInstanceId() {
|
|
|
52
55
|
* runtime (see above). Pure, so the shape is unit-tested.
|
|
53
56
|
*/
|
|
54
57
|
function buildHandshakeQuery(identity) {
|
|
58
|
+
const withoutAccountKey = values => values && Object.fromEntries(Object.entries(values).filter(([key]) => !["BUOY_LICENSE_KEY", "BUOY_KEY", "EXPO_PUBLIC_BUOY_KEY", "VITE_BUOY_KEY", "REACT_APP_BUOY_KEY", "licenseKey"].includes(key)));
|
|
55
59
|
return {
|
|
56
60
|
deviceName: identity.deviceName,
|
|
57
61
|
deviceId: identity.persistentDeviceId,
|
|
58
62
|
platform: identity.platform,
|
|
59
|
-
extraDeviceInfo: JSON.stringify(identity.extraDeviceInfo),
|
|
60
|
-
envVariables: JSON.stringify(identity.envVariables),
|
|
63
|
+
extraDeviceInfo: JSON.stringify(withoutAccountKey(identity.extraDeviceInfo)),
|
|
64
|
+
envVariables: JSON.stringify(withoutAccountKey(identity.envVariables)),
|
|
61
65
|
connectionNonce: makeConnectionNonce(),
|
|
62
66
|
instanceId: getInstanceId()
|
|
63
67
|
};
|
|
@@ -83,7 +87,6 @@ function useExternalSyncSocket({
|
|
|
83
87
|
const socketRef = (0, _react.useRef)(null);
|
|
84
88
|
const [socket, setSocket] = (0, _react.useState)(null);
|
|
85
89
|
const [isConnected, setIsConnected] = (0, _react.useState)(false);
|
|
86
|
-
const initialized = (0, _react.useRef)(false);
|
|
87
90
|
// Derived once per render; stable string, so effect deps stay quiet.
|
|
88
91
|
const resolvedSocketURL = socketURL ?? (0, _getDefaultSocketURL.getDefaultSocketURL)();
|
|
89
92
|
// Tracks whether we've already logged a connection error for the current
|
|
@@ -126,13 +129,11 @@ function useExternalSyncSocket({
|
|
|
126
129
|
(0, _logger.log)(`${logPrefix} Socket connection timeout`, enableLogs, "error");
|
|
127
130
|
};
|
|
128
131
|
|
|
129
|
-
//
|
|
132
|
+
// Reuse the transport, but reattach listeners after every effect setup
|
|
133
|
+
// (including React Strict Mode replay and Fast Refresh).
|
|
130
134
|
(0, _react.useEffect)(() => {
|
|
131
135
|
if (!persistentDeviceId) return;
|
|
132
|
-
if (initialized.current) return;
|
|
133
|
-
initialized.current = true;
|
|
134
136
|
const platformUrl = (0, _platformUtils.getPlatformSpecificURL)(resolvedSocketURL, platform);
|
|
135
|
-
currentSocketURL = platformUrl;
|
|
136
137
|
(0, _syncStatusStore.setSyncStatus)({
|
|
137
138
|
targetURL: platformUrl,
|
|
138
139
|
state: "connecting",
|
|
@@ -140,6 +141,10 @@ function useExternalSyncSocket({
|
|
|
140
141
|
});
|
|
141
142
|
(0, _logger.log)(`${logPrefix} Platform: ${platform}, using URL: ${platformUrl}`, enableLogs);
|
|
142
143
|
try {
|
|
144
|
+
if (globalSocketInstance && (currentSocketURL !== platformUrl || currentDeviceId !== persistentDeviceId || currentPlatform !== platform)) {
|
|
145
|
+
globalSocketInstance.disconnect();
|
|
146
|
+
globalSocketInstance = null;
|
|
147
|
+
}
|
|
143
148
|
if (!globalSocketInstance) {
|
|
144
149
|
(0, _logger.log)(`${logPrefix} Creating new socket instance to ${platformUrl}`, enableLogs);
|
|
145
150
|
globalSocketInstance = (0, _socket.io)(platformUrl, {
|
|
@@ -157,32 +162,34 @@ function useExternalSyncSocket({
|
|
|
157
162
|
reconnectionDelayMax: 5000,
|
|
158
163
|
transports: ["websocket"]
|
|
159
164
|
});
|
|
165
|
+
currentSocketURL = platformUrl;
|
|
166
|
+
currentDeviceId = persistentDeviceId;
|
|
167
|
+
currentPlatform = platform;
|
|
160
168
|
} else {
|
|
161
169
|
(0, _logger.log)(`${logPrefix} Reusing existing socket instance to ${platformUrl}`, enableLogs);
|
|
162
170
|
}
|
|
163
171
|
socketRef.current = globalSocketInstance;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
const activeSocket = socketRef.current;
|
|
173
|
+
setSocket(activeSocket);
|
|
174
|
+
activeSocket.on("connect_error", onConnectError);
|
|
175
|
+
activeSocket.on("connect_timeout", onConnectTimeout);
|
|
176
|
+
setIsConnected(activeSocket.connected);
|
|
177
|
+
if (activeSocket.connected) {
|
|
169
178
|
(0, _logger.log)(`${logPrefix} Socket already connected on init`, enableLogs);
|
|
170
179
|
}
|
|
171
|
-
|
|
172
|
-
|
|
180
|
+
activeSocket.on("connect", onConnect);
|
|
181
|
+
activeSocket.on("disconnect", onDisconnect);
|
|
173
182
|
return () => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
socketRef.current.off("connect_timeout", onConnectTimeout);
|
|
180
|
-
}
|
|
183
|
+
(0, _logger.log)(`${logPrefix} Cleaning up socket event listeners`, enableLogs);
|
|
184
|
+
activeSocket.off("connect", onConnect);
|
|
185
|
+
activeSocket.off("disconnect", onDisconnect);
|
|
186
|
+
activeSocket.off("connect_error", onConnectError);
|
|
187
|
+
activeSocket.off("connect_timeout", onConnectTimeout);
|
|
181
188
|
};
|
|
182
189
|
} catch (error) {
|
|
183
190
|
(0, _logger.log)(`${logPrefix} Failed to initialize socket: ${error}`, enableLogs, "error");
|
|
184
191
|
}
|
|
185
|
-
}, [persistentDeviceId]);
|
|
192
|
+
}, [persistentDeviceId, resolvedSocketURL, platform]);
|
|
186
193
|
|
|
187
194
|
// Update socket query parameters when deviceName changes
|
|
188
195
|
(0, _react.useEffect)(() => {
|
|
@@ -196,43 +203,6 @@ function useExternalSyncSocket({
|
|
|
196
203
|
};
|
|
197
204
|
}
|
|
198
205
|
}, [deviceName, logPrefix, persistentDeviceId, platform, enableLogs]);
|
|
199
|
-
|
|
200
|
-
// Update socket URL when socketURL changes
|
|
201
|
-
(0, _react.useEffect)(() => {
|
|
202
|
-
const platformUrl = (0, _platformUtils.getPlatformSpecificURL)(resolvedSocketURL, platform);
|
|
203
|
-
if (socketRef.current && currentSocketURL !== platformUrl && persistentDeviceId) {
|
|
204
|
-
(0, _logger.log)(`${logPrefix} Socket URL changed from ${currentSocketURL} to ${platformUrl}`, enableLogs);
|
|
205
|
-
try {
|
|
206
|
-
socketRef.current.disconnect();
|
|
207
|
-
currentSocketURL = platformUrl;
|
|
208
|
-
(0, _syncStatusStore.setSyncStatus)({
|
|
209
|
-
targetURL: platformUrl,
|
|
210
|
-
state: "connecting",
|
|
211
|
-
lastError: null
|
|
212
|
-
});
|
|
213
|
-
(0, _logger.log)(`${logPrefix} Creating new socket connection to ${platformUrl}`, enableLogs);
|
|
214
|
-
globalSocketInstance = (0, _socket.io)(platformUrl, {
|
|
215
|
-
autoConnect: true,
|
|
216
|
-
query: buildHandshakeQuery({
|
|
217
|
-
deviceName,
|
|
218
|
-
persistentDeviceId,
|
|
219
|
-
platform,
|
|
220
|
-
extraDeviceInfo,
|
|
221
|
-
envVariables
|
|
222
|
-
}),
|
|
223
|
-
reconnection: true,
|
|
224
|
-
reconnectionAttempts: Infinity,
|
|
225
|
-
reconnectionDelay: 1000,
|
|
226
|
-
reconnectionDelayMax: 5000,
|
|
227
|
-
transports: ["websocket"]
|
|
228
|
-
});
|
|
229
|
-
socketRef.current = globalSocketInstance;
|
|
230
|
-
setSocket(socketRef.current);
|
|
231
|
-
} catch (error) {
|
|
232
|
-
(0, _logger.log)(`${logPrefix} Failed to update socket connection: ${error}`, enableLogs, "error");
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}, [resolvedSocketURL, deviceName, logPrefix, persistentDeviceId, platform, enableLogs]);
|
|
236
206
|
function connect() {
|
|
237
207
|
if (socketRef.current && !socketRef.current.connected) {
|
|
238
208
|
(0, _logger.log)(`${logPrefix} Manually connecting to socket server`, enableLogs);
|
|
@@ -245,10 +215,27 @@ function useExternalSyncSocket({
|
|
|
245
215
|
socketRef.current.disconnect();
|
|
246
216
|
}
|
|
247
217
|
}
|
|
218
|
+
|
|
219
|
+
// A connected transport is not permission to sync. Legacy desktop brokers
|
|
220
|
+
// never send this announcement, so updated SDKs withhold tools from them.
|
|
221
|
+
const [admittedSocket, setAdmittedSocket] = (0, _react.useState)(null);
|
|
222
|
+
(0, _react.useEffect)(() => {
|
|
223
|
+
if (!socket) return;
|
|
224
|
+
return (0, _brokerAccess.watchBrokerAccess)(socket, ready => {
|
|
225
|
+
setAdmittedSocket(ready ? socket : null);
|
|
226
|
+
(0, _syncStatusStore.setSyncStatus)(ready ? {
|
|
227
|
+
state: "connected",
|
|
228
|
+
lastError: null
|
|
229
|
+
} : {
|
|
230
|
+
state: "retrying",
|
|
231
|
+
lastError: "Update Buoy Desktop and sign in to enable desktop sync."
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}, [socket]);
|
|
248
235
|
return {
|
|
249
|
-
socket,
|
|
236
|
+
socket: admittedSocket === socket ? socket : null,
|
|
250
237
|
connect,
|
|
251
238
|
disconnect,
|
|
252
|
-
isConnected
|
|
239
|
+
isConnected: isConnected && admittedSocket === socket && socket !== null
|
|
253
240
|
};
|
|
254
241
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { LicenseManager } from "@buoy-gg/license";
|
|
3
4
|
import { log } from "./utils/logger";
|
|
4
5
|
import { registerToolFlusher, unregisterToolFlusher } from "./crashFlush";
|
|
5
6
|
import { MAX_ACTION_RESULT_BYTES, MAX_SNAPSHOT_EMIT_BYTES, approxSizeOver } from "./emitBudget";
|
|
@@ -25,11 +26,16 @@ export function attachDeviceSync({
|
|
|
25
26
|
throttleMs = 200,
|
|
26
27
|
enableLogs = false
|
|
27
28
|
}) {
|
|
29
|
+
let disposed = false;
|
|
30
|
+
let brokerReady = false;
|
|
31
|
+
let accessGeneration = 0;
|
|
32
|
+
const allowed = () => !disposed && brokerReady && socket.connected && LicenseManager.hasAccountAccess();
|
|
28
33
|
const unsubs = new Map();
|
|
29
34
|
const lastEmit = {};
|
|
30
35
|
const timers = {};
|
|
31
36
|
const isForThisDevice = targetDeviceId => targetDeviceId === "All" || targetDeviceId === deviceId;
|
|
32
37
|
const safeEmit = (event, message) => {
|
|
38
|
+
if (!allowed()) return;
|
|
33
39
|
try {
|
|
34
40
|
socket.emit(event, message);
|
|
35
41
|
} catch (error) {
|
|
@@ -38,10 +44,16 @@ export function attachDeviceSync({
|
|
|
38
44
|
};
|
|
39
45
|
const warnedOversized = new Set();
|
|
40
46
|
const sendSnapshot = toolId => {
|
|
47
|
+
if (!allowed()) return;
|
|
41
48
|
const tool = getTools()[toolId];
|
|
42
49
|
if (!tool) return;
|
|
43
50
|
lastEmit[toolId] = Date.now();
|
|
44
|
-
|
|
51
|
+
let payload;
|
|
52
|
+
try {
|
|
53
|
+
payload = tool.getSnapshot();
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
45
57
|
const {
|
|
46
58
|
over,
|
|
47
59
|
approxBytes
|
|
@@ -104,6 +116,7 @@ export function attachDeviceSync({
|
|
|
104
116
|
/** The mode each currently-subscribed tool is wired for. */
|
|
105
117
|
const activeModes = new Map();
|
|
106
118
|
const watchTool = (toolId, mode) => {
|
|
119
|
+
if (!allowed()) return;
|
|
107
120
|
const tool = getTools()[toolId];
|
|
108
121
|
if (!tool) return;
|
|
109
122
|
if (unsubs.has(toolId)) {
|
|
@@ -145,7 +158,7 @@ export function attachDeviceSync({
|
|
|
145
158
|
safeEmit("devtool-capabilities", message);
|
|
146
159
|
};
|
|
147
160
|
const handleWatch = message => {
|
|
148
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
161
|
+
if (!allowed() || !isForThisDevice(message.targetDeviceId)) return;
|
|
149
162
|
if (message.watching) {
|
|
150
163
|
// `stream` is only trustworthy once a broker has merged every
|
|
151
164
|
// dashboard's request (see DevToolWatchMessage.consolidated). Without
|
|
@@ -164,11 +177,12 @@ export function attachDeviceSync({
|
|
|
164
177
|
}
|
|
165
178
|
};
|
|
166
179
|
const handleRequest = message => {
|
|
167
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
180
|
+
if (!allowed() || !isForThisDevice(message.targetDeviceId)) return;
|
|
168
181
|
sendSnapshot(message.toolId);
|
|
169
182
|
};
|
|
170
183
|
const handleAction = async message => {
|
|
171
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
184
|
+
if (!allowed() || !isForThisDevice(message.targetDeviceId)) return;
|
|
185
|
+
const startedAt = accessGeneration;
|
|
172
186
|
const {
|
|
173
187
|
requestId,
|
|
174
188
|
toolId,
|
|
@@ -196,6 +210,9 @@ export function attachDeviceSync({
|
|
|
196
210
|
} catch (error) {
|
|
197
211
|
result.error = error instanceof Error ? error.message : String(error);
|
|
198
212
|
}
|
|
213
|
+
|
|
214
|
+
// A subsequent admission cannot authorize work from the previous session.
|
|
215
|
+
if (startedAt !== accessGeneration || !allowed()) return;
|
|
199
216
|
if (result.ok) {
|
|
200
217
|
const {
|
|
201
218
|
over,
|
|
@@ -210,20 +227,61 @@ export function attachDeviceSync({
|
|
|
210
227
|
result.timestamp = Date.now();
|
|
211
228
|
safeEmit("devtool-action-result", result);
|
|
212
229
|
};
|
|
213
|
-
const
|
|
230
|
+
const dropToolWork = () => {
|
|
231
|
+
accessGeneration += 1;
|
|
232
|
+
for (const toolId of [...unsubs.keys()]) unwatchTool(toolId);
|
|
233
|
+
watched.clear();
|
|
234
|
+
for (const timer of Object.values(timers)) clearTimeout(timer);
|
|
235
|
+
const buffered = socket;
|
|
236
|
+
if (buffered.sendBuffer) buffered.sendBuffer = buffered.sendBuffer.filter(packet => !["devtool-sync", "devtool-capabilities", "devtool-action-result", "device-license"].includes(String(packet.data?.[0])));
|
|
237
|
+
};
|
|
238
|
+
const announceAccount = () => {
|
|
239
|
+
const state = LicenseManager.getState();
|
|
240
|
+
socket.emit("device-license", {
|
|
241
|
+
tier: state.tier,
|
|
242
|
+
isPro: state.isPro ? "1" : "0"
|
|
243
|
+
});
|
|
214
244
|
sendCapabilities();
|
|
215
245
|
};
|
|
216
|
-
|
|
246
|
+
const handleAccess = message => {
|
|
247
|
+
brokerReady = message?.protocol === 1 && message.ready === true;
|
|
248
|
+
if (!allowed()) dropToolWork();else announceAccount();
|
|
249
|
+
};
|
|
250
|
+
const offAccount = LicenseManager.subscribe(() => {
|
|
251
|
+
if (!LicenseManager.hasAccountAccess()) {
|
|
252
|
+
if (socket.connected) socket.emit("device-license", {
|
|
253
|
+
tier: "locked",
|
|
254
|
+
isPro: "0"
|
|
255
|
+
});
|
|
256
|
+
dropToolWork();
|
|
257
|
+
} else if (allowed()) announceAccount();
|
|
258
|
+
});
|
|
259
|
+
const handleConnect = () => {
|
|
260
|
+
brokerReady = false;
|
|
261
|
+
socket.emit("buoy:access:request");
|
|
262
|
+
};
|
|
263
|
+
const handleDisconnect = () => {
|
|
264
|
+
brokerReady = false;
|
|
265
|
+
dropToolWork();
|
|
266
|
+
};
|
|
267
|
+
socket.on("buoy:access", handleAccess);
|
|
217
268
|
for (const [toolId, mode] of watched) {
|
|
218
269
|
if (getTools()[toolId]) watchTool(toolId, mode);
|
|
219
270
|
}
|
|
220
271
|
socket.on("connect", handleConnect);
|
|
272
|
+
socket.on("disconnect", handleDisconnect);
|
|
221
273
|
socket.on("devtool-watch", handleWatch);
|
|
222
274
|
socket.on("request-devtool-state", handleRequest);
|
|
223
275
|
socket.on("devtool-action", handleAction);
|
|
276
|
+
if (socket.connected) handleConnect();
|
|
224
277
|
return () => {
|
|
278
|
+
disposed = true;
|
|
279
|
+
offAccount();
|
|
280
|
+
socket.off("buoy:access", handleAccess);
|
|
281
|
+
dropToolWork();
|
|
225
282
|
unregisterToolFlusher(flushNow);
|
|
226
283
|
socket.off("connect", handleConnect);
|
|
284
|
+
socket.off("disconnect", handleDisconnect);
|
|
227
285
|
socket.off("devtool-watch", handleWatch);
|
|
228
286
|
socket.off("request-devtool-state", handleRequest);
|
|
229
287
|
socket.off("devtool-action", handleAction);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/** Wait for an account-aware broker before publishing any tool capabilities. */
|
|
4
|
+
|
|
5
|
+
export function watchBrokerAccess(socket, update) {
|
|
6
|
+
const lock = () => update(false);
|
|
7
|
+
const request = () => {
|
|
8
|
+
lock();
|
|
9
|
+
socket.emit("buoy:access:request");
|
|
10
|
+
};
|
|
11
|
+
const access = message => {
|
|
12
|
+
const info = message;
|
|
13
|
+
update(info?.protocol === 1 && info?.ready === true);
|
|
14
|
+
};
|
|
15
|
+
socket.on("connect", request);
|
|
16
|
+
socket.on("disconnect", lock);
|
|
17
|
+
socket.on("buoy:access", access);
|
|
18
|
+
lock();
|
|
19
|
+
if (socket.connected) request();
|
|
20
|
+
return () => {
|
|
21
|
+
socket.off("connect", request);
|
|
22
|
+
socket.off("disconnect", lock);
|
|
23
|
+
socket.off("buoy:access", access);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { useAccountAccess } from "@buoy-gg/license";
|
|
3
4
|
import { useEffect, useRef } from "react";
|
|
4
5
|
import { attachDeviceSync } from "./attachDeviceSync";
|
|
5
6
|
/**
|
|
@@ -37,6 +38,8 @@ export function useExternalSync({
|
|
|
37
38
|
throttleMs = 200,
|
|
38
39
|
enableLogs = false
|
|
39
40
|
}) {
|
|
41
|
+
const account = useAccountAccess();
|
|
42
|
+
const admitted = account.status === "admitted";
|
|
40
43
|
const toolsRef = useRef(tools);
|
|
41
44
|
toolsRef.current = tools;
|
|
42
45
|
|
|
@@ -48,7 +51,10 @@ export function useExternalSync({
|
|
|
48
51
|
// object-reference change.
|
|
49
52
|
const toolIdsKey = Object.keys(tools).sort().join(",");
|
|
50
53
|
useEffect(() => {
|
|
51
|
-
if (!socket || !deviceId || !enabled)
|
|
54
|
+
if (!socket || !deviceId || !enabled || !admitted) {
|
|
55
|
+
watchedRef.current.clear();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
52
58
|
return attachDeviceSync({
|
|
53
59
|
socket,
|
|
54
60
|
deviceId,
|
|
@@ -59,5 +65,5 @@ export function useExternalSync({
|
|
|
59
65
|
});
|
|
60
66
|
// toolIdsKey stands in for `tools` (see comment above its definition).
|
|
61
67
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
62
|
-
}, [socket, deviceId, enabled, throttleMs, enableLogs, toolIdsKey]);
|
|
68
|
+
}, [socket, deviceId, enabled, admitted, throttleMs, enableLogs, toolIdsKey]);
|
|
63
69
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { watchBrokerAccess } from "./brokerAccess";
|
|
3
4
|
import { useEffect, useRef, useState } from "react";
|
|
4
5
|
import { io as socketIO } from "socket.io-client";
|
|
5
6
|
import { getPlatformSpecificURL } from "./utils/platformUtils";
|
|
@@ -16,6 +17,8 @@ const isDev = typeof __DEV__ !== "undefined" ? __DEV__ : false;
|
|
|
16
17
|
*/
|
|
17
18
|
let globalSocketInstance = null;
|
|
18
19
|
let currentSocketURL = "";
|
|
20
|
+
let currentDeviceId = "";
|
|
21
|
+
let currentPlatform = "";
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Identifies a single socket INSTANCE. Sent in the handshake query and reused
|
|
@@ -46,12 +49,13 @@ export function getInstanceId() {
|
|
|
46
49
|
* runtime (see above). Pure, so the shape is unit-tested.
|
|
47
50
|
*/
|
|
48
51
|
export function buildHandshakeQuery(identity) {
|
|
52
|
+
const withoutAccountKey = values => values && Object.fromEntries(Object.entries(values).filter(([key]) => !["BUOY_LICENSE_KEY", "BUOY_KEY", "EXPO_PUBLIC_BUOY_KEY", "VITE_BUOY_KEY", "REACT_APP_BUOY_KEY", "licenseKey"].includes(key)));
|
|
49
53
|
return {
|
|
50
54
|
deviceName: identity.deviceName,
|
|
51
55
|
deviceId: identity.persistentDeviceId,
|
|
52
56
|
platform: identity.platform,
|
|
53
|
-
extraDeviceInfo: JSON.stringify(identity.extraDeviceInfo),
|
|
54
|
-
envVariables: JSON.stringify(identity.envVariables),
|
|
57
|
+
extraDeviceInfo: JSON.stringify(withoutAccountKey(identity.extraDeviceInfo)),
|
|
58
|
+
envVariables: JSON.stringify(withoutAccountKey(identity.envVariables)),
|
|
55
59
|
connectionNonce: makeConnectionNonce(),
|
|
56
60
|
instanceId: getInstanceId()
|
|
57
61
|
};
|
|
@@ -77,7 +81,6 @@ export function useExternalSyncSocket({
|
|
|
77
81
|
const socketRef = useRef(null);
|
|
78
82
|
const [socket, setSocket] = useState(null);
|
|
79
83
|
const [isConnected, setIsConnected] = useState(false);
|
|
80
|
-
const initialized = useRef(false);
|
|
81
84
|
// Derived once per render; stable string, so effect deps stay quiet.
|
|
82
85
|
const resolvedSocketURL = socketURL ?? getDefaultSocketURL();
|
|
83
86
|
// Tracks whether we've already logged a connection error for the current
|
|
@@ -120,13 +123,11 @@ export function useExternalSyncSocket({
|
|
|
120
123
|
log(`${logPrefix} Socket connection timeout`, enableLogs, "error");
|
|
121
124
|
};
|
|
122
125
|
|
|
123
|
-
//
|
|
126
|
+
// Reuse the transport, but reattach listeners after every effect setup
|
|
127
|
+
// (including React Strict Mode replay and Fast Refresh).
|
|
124
128
|
useEffect(() => {
|
|
125
129
|
if (!persistentDeviceId) return;
|
|
126
|
-
if (initialized.current) return;
|
|
127
|
-
initialized.current = true;
|
|
128
130
|
const platformUrl = getPlatformSpecificURL(resolvedSocketURL, platform);
|
|
129
|
-
currentSocketURL = platformUrl;
|
|
130
131
|
setSyncStatus({
|
|
131
132
|
targetURL: platformUrl,
|
|
132
133
|
state: "connecting",
|
|
@@ -134,6 +135,10 @@ export function useExternalSyncSocket({
|
|
|
134
135
|
});
|
|
135
136
|
log(`${logPrefix} Platform: ${platform}, using URL: ${platformUrl}`, enableLogs);
|
|
136
137
|
try {
|
|
138
|
+
if (globalSocketInstance && (currentSocketURL !== platformUrl || currentDeviceId !== persistentDeviceId || currentPlatform !== platform)) {
|
|
139
|
+
globalSocketInstance.disconnect();
|
|
140
|
+
globalSocketInstance = null;
|
|
141
|
+
}
|
|
137
142
|
if (!globalSocketInstance) {
|
|
138
143
|
log(`${logPrefix} Creating new socket instance to ${platformUrl}`, enableLogs);
|
|
139
144
|
globalSocketInstance = socketIO(platformUrl, {
|
|
@@ -151,32 +156,34 @@ export function useExternalSyncSocket({
|
|
|
151
156
|
reconnectionDelayMax: 5000,
|
|
152
157
|
transports: ["websocket"]
|
|
153
158
|
});
|
|
159
|
+
currentSocketURL = platformUrl;
|
|
160
|
+
currentDeviceId = persistentDeviceId;
|
|
161
|
+
currentPlatform = platform;
|
|
154
162
|
} else {
|
|
155
163
|
log(`${logPrefix} Reusing existing socket instance to ${platformUrl}`, enableLogs);
|
|
156
164
|
}
|
|
157
165
|
socketRef.current = globalSocketInstance;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
const activeSocket = socketRef.current;
|
|
167
|
+
setSocket(activeSocket);
|
|
168
|
+
activeSocket.on("connect_error", onConnectError);
|
|
169
|
+
activeSocket.on("connect_timeout", onConnectTimeout);
|
|
170
|
+
setIsConnected(activeSocket.connected);
|
|
171
|
+
if (activeSocket.connected) {
|
|
163
172
|
log(`${logPrefix} Socket already connected on init`, enableLogs);
|
|
164
173
|
}
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
activeSocket.on("connect", onConnect);
|
|
175
|
+
activeSocket.on("disconnect", onDisconnect);
|
|
167
176
|
return () => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
socketRef.current.off("connect_timeout", onConnectTimeout);
|
|
174
|
-
}
|
|
177
|
+
log(`${logPrefix} Cleaning up socket event listeners`, enableLogs);
|
|
178
|
+
activeSocket.off("connect", onConnect);
|
|
179
|
+
activeSocket.off("disconnect", onDisconnect);
|
|
180
|
+
activeSocket.off("connect_error", onConnectError);
|
|
181
|
+
activeSocket.off("connect_timeout", onConnectTimeout);
|
|
175
182
|
};
|
|
176
183
|
} catch (error) {
|
|
177
184
|
log(`${logPrefix} Failed to initialize socket: ${error}`, enableLogs, "error");
|
|
178
185
|
}
|
|
179
|
-
}, [persistentDeviceId]);
|
|
186
|
+
}, [persistentDeviceId, resolvedSocketURL, platform]);
|
|
180
187
|
|
|
181
188
|
// Update socket query parameters when deviceName changes
|
|
182
189
|
useEffect(() => {
|
|
@@ -190,43 +197,6 @@ export function useExternalSyncSocket({
|
|
|
190
197
|
};
|
|
191
198
|
}
|
|
192
199
|
}, [deviceName, logPrefix, persistentDeviceId, platform, enableLogs]);
|
|
193
|
-
|
|
194
|
-
// Update socket URL when socketURL changes
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
const platformUrl = getPlatformSpecificURL(resolvedSocketURL, platform);
|
|
197
|
-
if (socketRef.current && currentSocketURL !== platformUrl && persistentDeviceId) {
|
|
198
|
-
log(`${logPrefix} Socket URL changed from ${currentSocketURL} to ${platformUrl}`, enableLogs);
|
|
199
|
-
try {
|
|
200
|
-
socketRef.current.disconnect();
|
|
201
|
-
currentSocketURL = platformUrl;
|
|
202
|
-
setSyncStatus({
|
|
203
|
-
targetURL: platformUrl,
|
|
204
|
-
state: "connecting",
|
|
205
|
-
lastError: null
|
|
206
|
-
});
|
|
207
|
-
log(`${logPrefix} Creating new socket connection to ${platformUrl}`, enableLogs);
|
|
208
|
-
globalSocketInstance = socketIO(platformUrl, {
|
|
209
|
-
autoConnect: true,
|
|
210
|
-
query: buildHandshakeQuery({
|
|
211
|
-
deviceName,
|
|
212
|
-
persistentDeviceId,
|
|
213
|
-
platform,
|
|
214
|
-
extraDeviceInfo,
|
|
215
|
-
envVariables
|
|
216
|
-
}),
|
|
217
|
-
reconnection: true,
|
|
218
|
-
reconnectionAttempts: Infinity,
|
|
219
|
-
reconnectionDelay: 1000,
|
|
220
|
-
reconnectionDelayMax: 5000,
|
|
221
|
-
transports: ["websocket"]
|
|
222
|
-
});
|
|
223
|
-
socketRef.current = globalSocketInstance;
|
|
224
|
-
setSocket(socketRef.current);
|
|
225
|
-
} catch (error) {
|
|
226
|
-
log(`${logPrefix} Failed to update socket connection: ${error}`, enableLogs, "error");
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}, [resolvedSocketURL, deviceName, logPrefix, persistentDeviceId, platform, enableLogs]);
|
|
230
200
|
function connect() {
|
|
231
201
|
if (socketRef.current && !socketRef.current.connected) {
|
|
232
202
|
log(`${logPrefix} Manually connecting to socket server`, enableLogs);
|
|
@@ -239,10 +209,27 @@ export function useExternalSyncSocket({
|
|
|
239
209
|
socketRef.current.disconnect();
|
|
240
210
|
}
|
|
241
211
|
}
|
|
212
|
+
|
|
213
|
+
// A connected transport is not permission to sync. Legacy desktop brokers
|
|
214
|
+
// never send this announcement, so updated SDKs withhold tools from them.
|
|
215
|
+
const [admittedSocket, setAdmittedSocket] = useState(null);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (!socket) return;
|
|
218
|
+
return watchBrokerAccess(socket, ready => {
|
|
219
|
+
setAdmittedSocket(ready ? socket : null);
|
|
220
|
+
setSyncStatus(ready ? {
|
|
221
|
+
state: "connected",
|
|
222
|
+
lastError: null
|
|
223
|
+
} : {
|
|
224
|
+
state: "retrying",
|
|
225
|
+
lastError: "Update Buoy Desktop and sign in to enable desktop sync."
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}, [socket]);
|
|
242
229
|
return {
|
|
243
|
-
socket,
|
|
230
|
+
socket: admittedSocket === socket ? socket : null,
|
|
244
231
|
connect,
|
|
245
232
|
disconnect,
|
|
246
|
-
isConnected
|
|
233
|
+
isConnected: isConnected && admittedSocket === socket && socket !== null
|
|
247
234
|
};
|
|
248
235
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachDeviceSync.d.ts","sourceRoot":"","sources":["../../src/attachDeviceSync.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"attachDeviceSync.d.ts","sourceRoot":"","sources":["../../src/attachDeviceSync.ts"],"names":[],"mappings":"AAQA,OAAO,EAQL,KAAK,eAAe,EACrB,MAAM,SAAS,CAAC;AAIjB,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IACrD,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC;IACnE,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC;CACrE,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE7C,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAChD;;;;OAIG;IACH,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,UAAgB,EAChB,UAAkB,GACnB,EAAE,uBAAuB,GAAG,MAAM,IAAI,CAsStC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Wait for an account-aware broker before publishing any tool capabilities. */
|
|
2
|
+
export interface AccessSocket {
|
|
3
|
+
connected: boolean;
|
|
4
|
+
on(event: string, callback: (...args: any[]) => void): unknown;
|
|
5
|
+
off(event: string, callback: (...args: any[]) => void): unknown;
|
|
6
|
+
emit(event: string): unknown;
|
|
7
|
+
}
|
|
8
|
+
export declare function watchBrokerAccess(socket: AccessSocket, update: (ready: boolean) => void): () => void;
|
|
9
|
+
//# sourceMappingURL=brokerAccess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brokerAccess.d.ts","sourceRoot":"","sources":["../../src/brokerAccess.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC;IAC/D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC;IAChE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B;AACD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAC/B,MAAM,IAAI,CAoBZ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useExternalSync.d.ts","sourceRoot":"","sources":["../../src/useExternalSync.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useExternalSync.d.ts","sourceRoot":"","sources":["../../src/useExternalSync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,MAAM,EACN,QAAQ,EACR,OAAc,EACd,UAAgB,EAChB,UAAkB,GACnB,EAAE,oBAAoB,QA4BtB"}
|
|
@@ -31,7 +31,7 @@ export declare function buildHandshakeQuery(identity: HandshakeIdentity): Record
|
|
|
31
31
|
* - Connection state tracking
|
|
32
32
|
*/
|
|
33
33
|
export declare function useExternalSyncSocket({ deviceName, socketURL, persistentDeviceId, extraDeviceInfo, envVariables, platform, enableLogs, }: UseExternalSyncSocketProps): {
|
|
34
|
-
socket: Socket
|
|
34
|
+
socket: Socket | null;
|
|
35
35
|
connect: () => void;
|
|
36
36
|
disconnect: () => void;
|
|
37
37
|
isConnected: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useExternalSyncSocket.d.ts","sourceRoot":"","sources":["../../src/useExternalSyncSocket.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useExternalSyncSocket.d.ts","sourceRoot":"","sources":["../../src/useExternalSyncSocket.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkB,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAM1D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAC;AA4B1D;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAItC;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAavF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,UAAU,EACV,SAAS,EACT,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,UAAkB,GACnB,EAAE,0BAA0B,GAAG;IAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;CACtB,CA4LA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/external-sync",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.38",
|
|
4
4
|
"description": "Generic device-to-desktop sync for Buoy dev tools",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"sideEffects": false,
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"socket.io-client": "^4.8.1"
|
|
29
|
+
"socket.io-client": "^4.8.1",
|
|
30
|
+
"@buoy-gg/license": "^7.0.38"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
33
|
"react": "*",
|