@buoy-gg/external-sync 7.0.17 → 7.0.18
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/lib/commonjs/attachDeviceSync.js +209 -0
- package/lib/commonjs/emitBudget.js +57 -0
- package/lib/commonjs/types.js +14 -1
- package/lib/commonjs/useExternalSync.js +10 -226
- package/lib/module/attachDeviceSync.js +205 -0
- package/lib/module/emitBudget.js +52 -0
- package/lib/module/types.js +14 -1
- package/lib/module/useExternalSync.js +10 -226
- package/lib/typescript/attachDeviceSync.d.ts +32 -0
- package/lib/typescript/attachDeviceSync.d.ts.map +1 -0
- package/lib/typescript/emitBudget.d.ts +24 -0
- package/lib/typescript/emitBudget.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +34 -1
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/useExternalSync.d.ts +1 -1
- package/lib/typescript/useExternalSync.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.attachDeviceSync = attachDeviceSync;
|
|
7
|
+
var _logger = require("./utils/logger");
|
|
8
|
+
var _emitBudget = require("./emitBudget");
|
|
9
|
+
var _types = require("./types");
|
|
10
|
+
const LOG = "[ExternalSync]";
|
|
11
|
+
|
|
12
|
+
/** Minimal socket surface the device-side bridge needs. */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* "capture" records into the tool's store without pushing snapshots; "stream"
|
|
16
|
+
* also emits on every change. See `DevToolWatchMessage.stream`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Device-side sync bridge (no React). A tool's adapter is only subscribed
|
|
21
|
+
* while a dashboard is watching it (`devtool-watch`).
|
|
22
|
+
*/
|
|
23
|
+
function attachDeviceSync({
|
|
24
|
+
socket,
|
|
25
|
+
deviceId,
|
|
26
|
+
getTools,
|
|
27
|
+
watched,
|
|
28
|
+
throttleMs = 200,
|
|
29
|
+
enableLogs = false
|
|
30
|
+
}) {
|
|
31
|
+
const unsubs = new Map();
|
|
32
|
+
const lastEmit = {};
|
|
33
|
+
const timers = {};
|
|
34
|
+
const isForThisDevice = targetDeviceId => targetDeviceId === "All" || targetDeviceId === deviceId;
|
|
35
|
+
const safeEmit = (event, message) => {
|
|
36
|
+
try {
|
|
37
|
+
socket.emit(event, message);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
(0, _logger.log)(`${LOG} Failed to emit "${event}": ${error}. Payload must be JSON-serializable.`, enableLogs, "error");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const warnedOversized = new Set();
|
|
43
|
+
const sendSnapshot = toolId => {
|
|
44
|
+
const tool = getTools()[toolId];
|
|
45
|
+
if (!tool) return;
|
|
46
|
+
lastEmit[toolId] = Date.now();
|
|
47
|
+
const payload = tool.getSnapshot();
|
|
48
|
+
const {
|
|
49
|
+
over,
|
|
50
|
+
approxBytes
|
|
51
|
+
} = (0, _emitBudget.approxSizeOver)(payload, _emitBudget.MAX_SNAPSHOT_EMIT_BYTES);
|
|
52
|
+
if (over) {
|
|
53
|
+
if (!warnedOversized.has(toolId)) {
|
|
54
|
+
warnedOversized.add(toolId);
|
|
55
|
+
console.warn(`${LOG} "${toolId}" snapshot ≈${(approxBytes / 1e6).toFixed(1)}MB exceeds the ${_emitBudget.MAX_SNAPSHOT_EMIT_BYTES / 1e6}MB sync budget — dropped (dashboard will be stale for this tool). The adapter should ship a wire-form snapshot (summaries + on-demand detail).`);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const message = {
|
|
60
|
+
toolId,
|
|
61
|
+
persistentDeviceId: deviceId,
|
|
62
|
+
timestamp: lastEmit[toolId],
|
|
63
|
+
version: tool.version,
|
|
64
|
+
kind: "snapshot",
|
|
65
|
+
payload
|
|
66
|
+
};
|
|
67
|
+
safeEmit("devtool-sync", message);
|
|
68
|
+
};
|
|
69
|
+
const scheduleSnapshot = toolId => {
|
|
70
|
+
if (timers[toolId]) return;
|
|
71
|
+
const elapsed = Date.now() - (lastEmit[toolId] ?? 0);
|
|
72
|
+
if (elapsed >= throttleMs) {
|
|
73
|
+
sendSnapshot(toolId);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
timers[toolId] = setTimeout(() => {
|
|
77
|
+
delete timers[toolId];
|
|
78
|
+
sendSnapshot(toolId);
|
|
79
|
+
}, throttleMs - elapsed);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** The mode each currently-subscribed tool is wired for. */
|
|
83
|
+
const activeModes = new Map();
|
|
84
|
+
const watchTool = (toolId, mode) => {
|
|
85
|
+
const tool = getTools()[toolId];
|
|
86
|
+
if (!tool) return;
|
|
87
|
+
if (unsubs.has(toolId)) {
|
|
88
|
+
if (activeModes.get(toolId) === mode) return;
|
|
89
|
+
// Mode changed (a streaming dashboard left, or arrived) — rewire, but
|
|
90
|
+
// do NOT drop the subscription in between: releasing it would take the
|
|
91
|
+
// store's subscriber count to 0 and stop capture mid-flight.
|
|
92
|
+
const previous = unsubs.get(toolId);
|
|
93
|
+
unsubs.set(toolId, tool.subscribe(mode === "stream" ? () => scheduleSnapshot(toolId) : () => {}));
|
|
94
|
+
activeModes.set(toolId, mode);
|
|
95
|
+
previous?.();
|
|
96
|
+
(0, _logger.log)(`${LOG} "${toolId}" watch mode → ${mode}`, enableLogs);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
unsubs.set(toolId, tool.subscribe(mode === "stream" ? () => scheduleSnapshot(toolId) : () => {}));
|
|
100
|
+
activeModes.set(toolId, mode);
|
|
101
|
+
(0, _logger.log)(`${LOG} Watching "${toolId}" (${mode})`, enableLogs);
|
|
102
|
+
};
|
|
103
|
+
const unwatchTool = toolId => {
|
|
104
|
+
unsubs.get(toolId)?.();
|
|
105
|
+
unsubs.delete(toolId);
|
|
106
|
+
activeModes.delete(toolId);
|
|
107
|
+
if (timers[toolId]) {
|
|
108
|
+
clearTimeout(timers[toolId]);
|
|
109
|
+
delete timers[toolId];
|
|
110
|
+
}
|
|
111
|
+
(0, _logger.log)(`${LOG} Stopped watching "${toolId}"`, enableLogs);
|
|
112
|
+
};
|
|
113
|
+
const sendCapabilities = () => {
|
|
114
|
+
const message = {
|
|
115
|
+
persistentDeviceId: deviceId,
|
|
116
|
+
protocolVersion: _types.SYNC_PROTOCOL_VERSION,
|
|
117
|
+
tools: Object.entries(getTools()).map(([toolId, tool]) => ({
|
|
118
|
+
toolId,
|
|
119
|
+
version: tool.version,
|
|
120
|
+
actions: Object.keys(tool.actions ?? {})
|
|
121
|
+
}))
|
|
122
|
+
};
|
|
123
|
+
safeEmit("devtool-capabilities", message);
|
|
124
|
+
};
|
|
125
|
+
const handleWatch = message => {
|
|
126
|
+
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
127
|
+
if (message.watching) {
|
|
128
|
+
// `stream` is only trustworthy once a broker has merged every
|
|
129
|
+
// dashboard's request (see DevToolWatchMessage.consolidated). Without
|
|
130
|
+
// that, an unmerged capture request could quiet a dashboard that is
|
|
131
|
+
// actively painting, so default to streaming.
|
|
132
|
+
const mode = message.consolidated === true && message.stream === false ? "capture" : "stream";
|
|
133
|
+
watched.set(message.toolId, mode);
|
|
134
|
+
watchTool(message.toolId, mode);
|
|
135
|
+
// Only a streaming audience gets the priming snapshot. A capture-only
|
|
136
|
+
// client reads with `request-devtool-state` when it actually wants one;
|
|
137
|
+
// pushing here would hand it the very payload it asked not to receive.
|
|
138
|
+
if (mode === "stream") sendSnapshot(message.toolId);
|
|
139
|
+
} else {
|
|
140
|
+
watched.delete(message.toolId);
|
|
141
|
+
unwatchTool(message.toolId);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const handleRequest = message => {
|
|
145
|
+
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
146
|
+
sendSnapshot(message.toolId);
|
|
147
|
+
};
|
|
148
|
+
const handleAction = async message => {
|
|
149
|
+
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
150
|
+
const {
|
|
151
|
+
requestId,
|
|
152
|
+
toolId,
|
|
153
|
+
action,
|
|
154
|
+
params
|
|
155
|
+
} = message;
|
|
156
|
+
const result = {
|
|
157
|
+
requestId,
|
|
158
|
+
toolId,
|
|
159
|
+
action,
|
|
160
|
+
persistentDeviceId: deviceId,
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
ok: false
|
|
163
|
+
};
|
|
164
|
+
const tool = getTools()[toolId];
|
|
165
|
+
const fn = tool?.actions?.[action];
|
|
166
|
+
if (!fn) {
|
|
167
|
+
result.error = tool ? `Unknown action "${action}" on tool "${toolId}"` : `Unknown tool "${toolId}"`;
|
|
168
|
+
safeEmit("devtool-action-result", result);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
result.result = await fn(params);
|
|
173
|
+
result.ok = true;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
176
|
+
}
|
|
177
|
+
if (result.ok) {
|
|
178
|
+
const {
|
|
179
|
+
over,
|
|
180
|
+
approxBytes
|
|
181
|
+
} = (0, _emitBudget.approxSizeOver)(result.result, _emitBudget.MAX_ACTION_RESULT_BYTES);
|
|
182
|
+
if (over) {
|
|
183
|
+
result.ok = false;
|
|
184
|
+
result.result = undefined;
|
|
185
|
+
result.error = `Result ≈${(approxBytes / 1e6).toFixed(1)}MB exceeds the ${_emitBudget.MAX_ACTION_RESULT_BYTES / 1e6}MB sync budget. Narrow the request (e.g. includeValues:false, a limit, or a more specific action).`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
result.timestamp = Date.now();
|
|
189
|
+
safeEmit("devtool-action-result", result);
|
|
190
|
+
};
|
|
191
|
+
const handleConnect = () => {
|
|
192
|
+
sendCapabilities();
|
|
193
|
+
};
|
|
194
|
+
if (socket.connected) sendCapabilities();
|
|
195
|
+
for (const [toolId, mode] of watched) {
|
|
196
|
+
if (getTools()[toolId]) watchTool(toolId, mode);
|
|
197
|
+
}
|
|
198
|
+
socket.on("connect", handleConnect);
|
|
199
|
+
socket.on("devtool-watch", handleWatch);
|
|
200
|
+
socket.on("request-devtool-state", handleRequest);
|
|
201
|
+
socket.on("devtool-action", handleAction);
|
|
202
|
+
return () => {
|
|
203
|
+
socket.off("connect", handleConnect);
|
|
204
|
+
socket.off("devtool-watch", handleWatch);
|
|
205
|
+
socket.off("request-devtool-state", handleRequest);
|
|
206
|
+
socket.off("devtool-action", handleAction);
|
|
207
|
+
for (const toolId of [...unsubs.keys()]) unwatchTool(toolId);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.MAX_SNAPSHOT_EMIT_BYTES = exports.MAX_ACTION_RESULT_BYTES = void 0;
|
|
7
|
+
exports.approxSizeOver = approxSizeOver;
|
|
8
|
+
/**
|
|
9
|
+
* Emit-layer byte budgets — the choke-point guard.
|
|
10
|
+
*
|
|
11
|
+
* Every adapter's snapshot and every action result funnels through
|
|
12
|
+
* socket.emit, whose serializer walks the ENTIRE payload synchronously on
|
|
13
|
+
* the JS thread (and JSON.stringify does not honor structural sharing).
|
|
14
|
+
* Per-adapter guards get missed — the redux adapter shipping raw state
|
|
15
|
+
* trees froze and native-OOM-crashed enterprise apps whenever a dashboard
|
|
16
|
+
* was connected. One budget here closes every adapter's hole at once:
|
|
17
|
+
* oversized payloads are dropped (snapshots) or replaced with an error
|
|
18
|
+
* (action results) instead of being serialized.
|
|
19
|
+
*
|
|
20
|
+
* The estimator is O(limit) — early-abort walk, O(1) for giant strings —
|
|
21
|
+
* never O(payload). Copied per-package by design (see events/redux copies).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Max approx bytes for one tool snapshot emit. */
|
|
25
|
+
const MAX_SNAPSHOT_EMIT_BYTES = exports.MAX_SNAPSHOT_EMIT_BYTES = 2 * 1024 * 1024;
|
|
26
|
+
/** Max approx bytes for one action-result emit (explicit requests get more). */
|
|
27
|
+
const MAX_ACTION_RESULT_BYTES = exports.MAX_ACTION_RESULT_BYTES = 8 * 1024 * 1024;
|
|
28
|
+
function approxSizeOver(value, limit) {
|
|
29
|
+
let bytes = 0;
|
|
30
|
+
const stack = [value];
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
while (stack.length > 0) {
|
|
33
|
+
if (bytes > limit) return {
|
|
34
|
+
over: true,
|
|
35
|
+
approxBytes: bytes
|
|
36
|
+
};
|
|
37
|
+
const v = stack.pop();
|
|
38
|
+
if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
|
|
39
|
+
if (seen.has(v)) continue;
|
|
40
|
+
seen.add(v);
|
|
41
|
+
if (Array.isArray(v)) {
|
|
42
|
+
bytes += 2 + v.length;
|
|
43
|
+
for (let i = 0; i < v.length; i++) stack.push(v[i]);
|
|
44
|
+
} else {
|
|
45
|
+
bytes += 2;
|
|
46
|
+
for (const k in v) {
|
|
47
|
+
bytes += k.length + 4;
|
|
48
|
+
stack.push(v[k]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else bytes += 4;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
over: bytes > limit,
|
|
55
|
+
approxBytes: bytes
|
|
56
|
+
};
|
|
57
|
+
}
|
package/lib/commonjs/types.js
CHANGED
|
@@ -61,7 +61,20 @@ const SYNC_PROTOCOL_VERSION = exports.SYNC_PROTOCOL_VERSION = 1;
|
|
|
61
61
|
// DASHBOARD -> DEVICE MESSAGES
|
|
62
62
|
// ============================================================
|
|
63
63
|
|
|
64
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Server-consolidated signal that a tool gained/lost its audience.
|
|
66
|
+
*
|
|
67
|
+
* `watching` arms CAPTURE: the device subscribes to the tool, which is what
|
|
68
|
+
* makes its store start recording. `stream` decides whether the device also
|
|
69
|
+
* PUSHES a snapshot on every change (throttled), which is the expensive half —
|
|
70
|
+
* it serializes the whole payload on the JS thread ~5x/sec.
|
|
71
|
+
*
|
|
72
|
+
* Those are separate because their audiences are: a dashboard renders every
|
|
73
|
+
* snapshot, so it wants both. A request/response client (MCP) reads on demand
|
|
74
|
+
* via `request-devtool-state` and would throw the stream away — but it still
|
|
75
|
+
* needs the device to be RECORDING between its reads, or it only ever sees an
|
|
76
|
+
* empty list and concludes the app made no requests.
|
|
77
|
+
*/
|
|
65
78
|
|
|
66
79
|
/** Explicit request for a tool's current state (works even while unwatched). */
|
|
67
80
|
|
|
@@ -5,61 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.useExternalSync = useExternalSync;
|
|
7
7
|
var _react = require("react");
|
|
8
|
-
var
|
|
9
|
-
var _types = require("./types");
|
|
10
|
-
const LOG = "[ExternalSync]";
|
|
11
|
-
|
|
12
|
-
// ==========================================================================
|
|
13
|
-
// Emit-layer byte budgets — the choke-point guard.
|
|
14
|
-
//
|
|
15
|
-
// Every adapter's snapshot and every action result funnels through
|
|
16
|
-
// socket.emit, whose serializer walks the ENTIRE payload synchronously on
|
|
17
|
-
// the JS thread (and JSON.stringify does not honor structural sharing).
|
|
18
|
-
// Per-adapter guards get missed — the redux adapter shipping raw state
|
|
19
|
-
// trees froze and native-OOM-crashed enterprise apps whenever a dashboard
|
|
20
|
-
// was connected. One budget here closes every adapter's hole at once:
|
|
21
|
-
// oversized payloads are dropped (snapshots) or replaced with an error
|
|
22
|
-
// (action results) instead of being serialized.
|
|
23
|
-
//
|
|
24
|
-
// The estimator is O(limit) — early-abort walk, O(1) for giant strings —
|
|
25
|
-
// never O(payload). Copied per-package by design (see events/redux copies).
|
|
26
|
-
// ==========================================================================
|
|
27
|
-
|
|
28
|
-
/** Max approx bytes for one tool snapshot emit. */
|
|
29
|
-
const MAX_SNAPSHOT_EMIT_BYTES = 2 * 1024 * 1024;
|
|
30
|
-
/** Max approx bytes for one action-result emit (explicit requests get more). */
|
|
31
|
-
const MAX_ACTION_RESULT_BYTES = 8 * 1024 * 1024;
|
|
32
|
-
function approxSizeOver(value, limit) {
|
|
33
|
-
let bytes = 0;
|
|
34
|
-
const stack = [value];
|
|
35
|
-
const seen = new Set();
|
|
36
|
-
while (stack.length > 0) {
|
|
37
|
-
if (bytes > limit) return {
|
|
38
|
-
over: true,
|
|
39
|
-
approxBytes: bytes
|
|
40
|
-
};
|
|
41
|
-
const v = stack.pop();
|
|
42
|
-
if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
|
|
43
|
-
if (seen.has(v)) continue;
|
|
44
|
-
seen.add(v);
|
|
45
|
-
if (Array.isArray(v)) {
|
|
46
|
-
bytes += 2 + v.length;
|
|
47
|
-
for (let i = 0; i < v.length; i++) stack.push(v[i]);
|
|
48
|
-
} else {
|
|
49
|
-
bytes += 2;
|
|
50
|
-
for (const k in v) {
|
|
51
|
-
bytes += k.length + 4;
|
|
52
|
-
stack.push(v[k]);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
} else bytes += 4;
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
over: bytes > limit,
|
|
59
|
-
approxBytes: bytes
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
8
|
+
var _attachDeviceSync = require("./attachDeviceSync");
|
|
63
9
|
/**
|
|
64
10
|
* Device-side sync bridge: syncs any number of dev tools to the desktop
|
|
65
11
|
* dashboard and executes actions invoked remotely.
|
|
@@ -100,183 +46,21 @@ function useExternalSync({
|
|
|
100
46
|
|
|
101
47
|
// Watch state persists across effect re-runs (e.g. when tools are
|
|
102
48
|
// added/removed) so an already-watched tool stays subscribed.
|
|
103
|
-
const watchedRef = (0, _react.useRef)(new
|
|
49
|
+
const watchedRef = (0, _react.useRef)(new Map());
|
|
104
50
|
|
|
105
51
|
// Re-run the effect only when tools are added/removed, not on every
|
|
106
52
|
// object-reference change.
|
|
107
53
|
const toolIdsKey = Object.keys(tools).sort().join(",");
|
|
108
54
|
(0, _react.useEffect)(() => {
|
|
109
55
|
if (!socket || !deviceId || !enabled) return;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
(0, _logger.log)(`${LOG} Failed to emit "${event}": ${error}. Payload must be JSON-serializable.`, enableLogs, "error");
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// One warning per tool per session — the drop repeats every snapshot.
|
|
123
|
-
const warnedOversized = new Set();
|
|
124
|
-
const sendSnapshot = toolId => {
|
|
125
|
-
const tool = toolsRef.current[toolId];
|
|
126
|
-
if (!tool) return;
|
|
127
|
-
lastEmit[toolId] = Date.now();
|
|
128
|
-
const payload = tool.getSnapshot();
|
|
129
|
-
|
|
130
|
-
// Emit-layer budget: never hand an unbounded payload to the socket
|
|
131
|
-
// serializer. A dropped snapshot means a stale dashboard panel for
|
|
132
|
-
// that tool; serializing it would freeze (and can OOM-kill) the app.
|
|
133
|
-
const {
|
|
134
|
-
over,
|
|
135
|
-
approxBytes
|
|
136
|
-
} = approxSizeOver(payload, MAX_SNAPSHOT_EMIT_BYTES);
|
|
137
|
-
if (over) {
|
|
138
|
-
if (!warnedOversized.has(toolId)) {
|
|
139
|
-
warnedOversized.add(toolId);
|
|
140
|
-
console.warn(`${LOG} "${toolId}" snapshot ≈${(approxBytes / 1e6).toFixed(1)}MB exceeds the ${MAX_SNAPSHOT_EMIT_BYTES / 1e6}MB sync budget — dropped (dashboard will be stale for this tool). The adapter should ship a wire-form snapshot (summaries + on-demand detail).`);
|
|
141
|
-
}
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const message = {
|
|
145
|
-
toolId,
|
|
146
|
-
persistentDeviceId: deviceId,
|
|
147
|
-
timestamp: lastEmit[toolId],
|
|
148
|
-
version: tool.version,
|
|
149
|
-
kind: "snapshot",
|
|
150
|
-
payload
|
|
151
|
-
};
|
|
152
|
-
safeEmit("devtool-sync", message);
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// Leading + trailing throttle: emit immediately when outside the window,
|
|
156
|
-
// otherwise schedule a flush for the window's end so the dashboard always
|
|
157
|
-
// converges to the latest state.
|
|
158
|
-
const scheduleSnapshot = toolId => {
|
|
159
|
-
if (timers[toolId]) return;
|
|
160
|
-
const elapsed = Date.now() - (lastEmit[toolId] ?? 0);
|
|
161
|
-
if (elapsed >= throttleMs) {
|
|
162
|
-
sendSnapshot(toolId);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
timers[toolId] = setTimeout(() => {
|
|
166
|
-
delete timers[toolId];
|
|
167
|
-
sendSnapshot(toolId);
|
|
168
|
-
}, throttleMs - elapsed);
|
|
169
|
-
};
|
|
170
|
-
const watchTool = toolId => {
|
|
171
|
-
const tool = toolsRef.current[toolId];
|
|
172
|
-
if (!tool || unsubs.has(toolId)) return;
|
|
173
|
-
unsubs.set(toolId, tool.subscribe(() => scheduleSnapshot(toolId)));
|
|
174
|
-
(0, _logger.log)(`${LOG} Watching "${toolId}"`, enableLogs);
|
|
175
|
-
};
|
|
176
|
-
const unwatchTool = toolId => {
|
|
177
|
-
unsubs.get(toolId)?.();
|
|
178
|
-
unsubs.delete(toolId);
|
|
179
|
-
if (timers[toolId]) {
|
|
180
|
-
clearTimeout(timers[toolId]);
|
|
181
|
-
delete timers[toolId];
|
|
182
|
-
}
|
|
183
|
-
(0, _logger.log)(`${LOG} Stopped watching "${toolId}"`, enableLogs);
|
|
184
|
-
};
|
|
185
|
-
const sendCapabilities = () => {
|
|
186
|
-
const message = {
|
|
187
|
-
persistentDeviceId: deviceId,
|
|
188
|
-
protocolVersion: _types.SYNC_PROTOCOL_VERSION,
|
|
189
|
-
tools: Object.entries(toolsRef.current).map(([toolId, tool]) => ({
|
|
190
|
-
toolId,
|
|
191
|
-
version: tool.version,
|
|
192
|
-
actions: Object.keys(tool.actions ?? {})
|
|
193
|
-
}))
|
|
194
|
-
};
|
|
195
|
-
safeEmit("devtool-capabilities", message);
|
|
196
|
-
};
|
|
197
|
-
const handleWatch = message => {
|
|
198
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
199
|
-
if (message.watching) {
|
|
200
|
-
watchedRef.current.add(message.toolId);
|
|
201
|
-
watchTool(message.toolId);
|
|
202
|
-
sendSnapshot(message.toolId);
|
|
203
|
-
} else {
|
|
204
|
-
watchedRef.current.delete(message.toolId);
|
|
205
|
-
unwatchTool(message.toolId);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
const handleRequest = message => {
|
|
209
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
210
|
-
sendSnapshot(message.toolId);
|
|
211
|
-
};
|
|
212
|
-
const handleAction = async message => {
|
|
213
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
214
|
-
const {
|
|
215
|
-
requestId,
|
|
216
|
-
toolId,
|
|
217
|
-
action,
|
|
218
|
-
params
|
|
219
|
-
} = message;
|
|
220
|
-
const result = {
|
|
221
|
-
requestId,
|
|
222
|
-
toolId,
|
|
223
|
-
action,
|
|
224
|
-
persistentDeviceId: deviceId,
|
|
225
|
-
timestamp: Date.now(),
|
|
226
|
-
ok: false
|
|
227
|
-
};
|
|
228
|
-
const tool = toolsRef.current[toolId];
|
|
229
|
-
const fn = tool?.actions?.[action];
|
|
230
|
-
if (!fn) {
|
|
231
|
-
result.error = tool ? `Unknown action "${action}" on tool "${toolId}"` : `Unknown tool "${toolId}"`;
|
|
232
|
-
safeEmit("devtool-action-result", result);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
try {
|
|
236
|
-
result.result = await fn(params);
|
|
237
|
-
result.ok = true;
|
|
238
|
-
} catch (error) {
|
|
239
|
-
result.error = error instanceof Error ? error.message : String(error);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Same budget for action results: e.g. redux getState({includeValues:
|
|
243
|
-
// true}) on a huge tree would otherwise OOM through this exact emit.
|
|
244
|
-
if (result.ok) {
|
|
245
|
-
const {
|
|
246
|
-
over,
|
|
247
|
-
approxBytes
|
|
248
|
-
} = approxSizeOver(result.result, MAX_ACTION_RESULT_BYTES);
|
|
249
|
-
if (over) {
|
|
250
|
-
result.ok = false;
|
|
251
|
-
result.result = undefined;
|
|
252
|
-
result.error = `Result ≈${(approxBytes / 1e6).toFixed(1)}MB exceeds the ${MAX_ACTION_RESULT_BYTES / 1e6}MB sync budget. Narrow the request (e.g. includeValues:false, a limit, or a more specific action).`;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
result.timestamp = Date.now();
|
|
256
|
-
safeEmit("devtool-action-result", result);
|
|
257
|
-
};
|
|
258
|
-
const handleConnect = () => {
|
|
259
|
-
// The server replies with current watch state for this device.
|
|
260
|
-
sendCapabilities();
|
|
261
|
-
};
|
|
262
|
-
if (socket.connected) sendCapabilities();
|
|
263
|
-
|
|
264
|
-
// Restore subscriptions for tools that were watched before this effect
|
|
265
|
-
// re-ran (or that arrived after the watch message).
|
|
266
|
-
for (const toolId of watchedRef.current) {
|
|
267
|
-
if (toolsRef.current[toolId]) watchTool(toolId);
|
|
268
|
-
}
|
|
269
|
-
socket.on("connect", handleConnect);
|
|
270
|
-
socket.on("devtool-watch", handleWatch);
|
|
271
|
-
socket.on("request-devtool-state", handleRequest);
|
|
272
|
-
socket.on("devtool-action", handleAction);
|
|
273
|
-
return () => {
|
|
274
|
-
socket.off("connect", handleConnect);
|
|
275
|
-
socket.off("devtool-watch", handleWatch);
|
|
276
|
-
socket.off("request-devtool-state", handleRequest);
|
|
277
|
-
socket.off("devtool-action", handleAction);
|
|
278
|
-
for (const toolId of [...unsubs.keys()]) unwatchTool(toolId);
|
|
279
|
-
};
|
|
56
|
+
return (0, _attachDeviceSync.attachDeviceSync)({
|
|
57
|
+
socket,
|
|
58
|
+
deviceId,
|
|
59
|
+
getTools: () => toolsRef.current,
|
|
60
|
+
watched: watchedRef.current,
|
|
61
|
+
throttleMs,
|
|
62
|
+
enableLogs
|
|
63
|
+
});
|
|
280
64
|
// toolIdsKey stands in for `tools` (see comment above its definition).
|
|
281
65
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
282
66
|
}, [socket, deviceId, enabled, throttleMs, enableLogs, toolIdsKey]);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { log } from "./utils/logger";
|
|
4
|
+
import { MAX_ACTION_RESULT_BYTES, MAX_SNAPSHOT_EMIT_BYTES, approxSizeOver } from "./emitBudget";
|
|
5
|
+
import { SYNC_PROTOCOL_VERSION } from "./types";
|
|
6
|
+
const LOG = "[ExternalSync]";
|
|
7
|
+
|
|
8
|
+
/** Minimal socket surface the device-side bridge needs. */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* "capture" records into the tool's store without pushing snapshots; "stream"
|
|
12
|
+
* also emits on every change. See `DevToolWatchMessage.stream`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Device-side sync bridge (no React). A tool's adapter is only subscribed
|
|
17
|
+
* while a dashboard is watching it (`devtool-watch`).
|
|
18
|
+
*/
|
|
19
|
+
export function attachDeviceSync({
|
|
20
|
+
socket,
|
|
21
|
+
deviceId,
|
|
22
|
+
getTools,
|
|
23
|
+
watched,
|
|
24
|
+
throttleMs = 200,
|
|
25
|
+
enableLogs = false
|
|
26
|
+
}) {
|
|
27
|
+
const unsubs = new Map();
|
|
28
|
+
const lastEmit = {};
|
|
29
|
+
const timers = {};
|
|
30
|
+
const isForThisDevice = targetDeviceId => targetDeviceId === "All" || targetDeviceId === deviceId;
|
|
31
|
+
const safeEmit = (event, message) => {
|
|
32
|
+
try {
|
|
33
|
+
socket.emit(event, message);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
log(`${LOG} Failed to emit "${event}": ${error}. Payload must be JSON-serializable.`, enableLogs, "error");
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const warnedOversized = new Set();
|
|
39
|
+
const sendSnapshot = toolId => {
|
|
40
|
+
const tool = getTools()[toolId];
|
|
41
|
+
if (!tool) return;
|
|
42
|
+
lastEmit[toolId] = Date.now();
|
|
43
|
+
const payload = tool.getSnapshot();
|
|
44
|
+
const {
|
|
45
|
+
over,
|
|
46
|
+
approxBytes
|
|
47
|
+
} = approxSizeOver(payload, MAX_SNAPSHOT_EMIT_BYTES);
|
|
48
|
+
if (over) {
|
|
49
|
+
if (!warnedOversized.has(toolId)) {
|
|
50
|
+
warnedOversized.add(toolId);
|
|
51
|
+
console.warn(`${LOG} "${toolId}" snapshot ≈${(approxBytes / 1e6).toFixed(1)}MB exceeds the ${MAX_SNAPSHOT_EMIT_BYTES / 1e6}MB sync budget — dropped (dashboard will be stale for this tool). The adapter should ship a wire-form snapshot (summaries + on-demand detail).`);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const message = {
|
|
56
|
+
toolId,
|
|
57
|
+
persistentDeviceId: deviceId,
|
|
58
|
+
timestamp: lastEmit[toolId],
|
|
59
|
+
version: tool.version,
|
|
60
|
+
kind: "snapshot",
|
|
61
|
+
payload
|
|
62
|
+
};
|
|
63
|
+
safeEmit("devtool-sync", message);
|
|
64
|
+
};
|
|
65
|
+
const scheduleSnapshot = toolId => {
|
|
66
|
+
if (timers[toolId]) return;
|
|
67
|
+
const elapsed = Date.now() - (lastEmit[toolId] ?? 0);
|
|
68
|
+
if (elapsed >= throttleMs) {
|
|
69
|
+
sendSnapshot(toolId);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
timers[toolId] = setTimeout(() => {
|
|
73
|
+
delete timers[toolId];
|
|
74
|
+
sendSnapshot(toolId);
|
|
75
|
+
}, throttleMs - elapsed);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The mode each currently-subscribed tool is wired for. */
|
|
79
|
+
const activeModes = new Map();
|
|
80
|
+
const watchTool = (toolId, mode) => {
|
|
81
|
+
const tool = getTools()[toolId];
|
|
82
|
+
if (!tool) return;
|
|
83
|
+
if (unsubs.has(toolId)) {
|
|
84
|
+
if (activeModes.get(toolId) === mode) return;
|
|
85
|
+
// Mode changed (a streaming dashboard left, or arrived) — rewire, but
|
|
86
|
+
// do NOT drop the subscription in between: releasing it would take the
|
|
87
|
+
// store's subscriber count to 0 and stop capture mid-flight.
|
|
88
|
+
const previous = unsubs.get(toolId);
|
|
89
|
+
unsubs.set(toolId, tool.subscribe(mode === "stream" ? () => scheduleSnapshot(toolId) : () => {}));
|
|
90
|
+
activeModes.set(toolId, mode);
|
|
91
|
+
previous?.();
|
|
92
|
+
log(`${LOG} "${toolId}" watch mode → ${mode}`, enableLogs);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
unsubs.set(toolId, tool.subscribe(mode === "stream" ? () => scheduleSnapshot(toolId) : () => {}));
|
|
96
|
+
activeModes.set(toolId, mode);
|
|
97
|
+
log(`${LOG} Watching "${toolId}" (${mode})`, enableLogs);
|
|
98
|
+
};
|
|
99
|
+
const unwatchTool = toolId => {
|
|
100
|
+
unsubs.get(toolId)?.();
|
|
101
|
+
unsubs.delete(toolId);
|
|
102
|
+
activeModes.delete(toolId);
|
|
103
|
+
if (timers[toolId]) {
|
|
104
|
+
clearTimeout(timers[toolId]);
|
|
105
|
+
delete timers[toolId];
|
|
106
|
+
}
|
|
107
|
+
log(`${LOG} Stopped watching "${toolId}"`, enableLogs);
|
|
108
|
+
};
|
|
109
|
+
const sendCapabilities = () => {
|
|
110
|
+
const message = {
|
|
111
|
+
persistentDeviceId: deviceId,
|
|
112
|
+
protocolVersion: SYNC_PROTOCOL_VERSION,
|
|
113
|
+
tools: Object.entries(getTools()).map(([toolId, tool]) => ({
|
|
114
|
+
toolId,
|
|
115
|
+
version: tool.version,
|
|
116
|
+
actions: Object.keys(tool.actions ?? {})
|
|
117
|
+
}))
|
|
118
|
+
};
|
|
119
|
+
safeEmit("devtool-capabilities", message);
|
|
120
|
+
};
|
|
121
|
+
const handleWatch = message => {
|
|
122
|
+
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
123
|
+
if (message.watching) {
|
|
124
|
+
// `stream` is only trustworthy once a broker has merged every
|
|
125
|
+
// dashboard's request (see DevToolWatchMessage.consolidated). Without
|
|
126
|
+
// that, an unmerged capture request could quiet a dashboard that is
|
|
127
|
+
// actively painting, so default to streaming.
|
|
128
|
+
const mode = message.consolidated === true && message.stream === false ? "capture" : "stream";
|
|
129
|
+
watched.set(message.toolId, mode);
|
|
130
|
+
watchTool(message.toolId, mode);
|
|
131
|
+
// Only a streaming audience gets the priming snapshot. A capture-only
|
|
132
|
+
// client reads with `request-devtool-state` when it actually wants one;
|
|
133
|
+
// pushing here would hand it the very payload it asked not to receive.
|
|
134
|
+
if (mode === "stream") sendSnapshot(message.toolId);
|
|
135
|
+
} else {
|
|
136
|
+
watched.delete(message.toolId);
|
|
137
|
+
unwatchTool(message.toolId);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const handleRequest = message => {
|
|
141
|
+
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
142
|
+
sendSnapshot(message.toolId);
|
|
143
|
+
};
|
|
144
|
+
const handleAction = async message => {
|
|
145
|
+
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
146
|
+
const {
|
|
147
|
+
requestId,
|
|
148
|
+
toolId,
|
|
149
|
+
action,
|
|
150
|
+
params
|
|
151
|
+
} = message;
|
|
152
|
+
const result = {
|
|
153
|
+
requestId,
|
|
154
|
+
toolId,
|
|
155
|
+
action,
|
|
156
|
+
persistentDeviceId: deviceId,
|
|
157
|
+
timestamp: Date.now(),
|
|
158
|
+
ok: false
|
|
159
|
+
};
|
|
160
|
+
const tool = getTools()[toolId];
|
|
161
|
+
const fn = tool?.actions?.[action];
|
|
162
|
+
if (!fn) {
|
|
163
|
+
result.error = tool ? `Unknown action "${action}" on tool "${toolId}"` : `Unknown tool "${toolId}"`;
|
|
164
|
+
safeEmit("devtool-action-result", result);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
result.result = await fn(params);
|
|
169
|
+
result.ok = true;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
172
|
+
}
|
|
173
|
+
if (result.ok) {
|
|
174
|
+
const {
|
|
175
|
+
over,
|
|
176
|
+
approxBytes
|
|
177
|
+
} = approxSizeOver(result.result, MAX_ACTION_RESULT_BYTES);
|
|
178
|
+
if (over) {
|
|
179
|
+
result.ok = false;
|
|
180
|
+
result.result = undefined;
|
|
181
|
+
result.error = `Result ≈${(approxBytes / 1e6).toFixed(1)}MB exceeds the ${MAX_ACTION_RESULT_BYTES / 1e6}MB sync budget. Narrow the request (e.g. includeValues:false, a limit, or a more specific action).`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
result.timestamp = Date.now();
|
|
185
|
+
safeEmit("devtool-action-result", result);
|
|
186
|
+
};
|
|
187
|
+
const handleConnect = () => {
|
|
188
|
+
sendCapabilities();
|
|
189
|
+
};
|
|
190
|
+
if (socket.connected) sendCapabilities();
|
|
191
|
+
for (const [toolId, mode] of watched) {
|
|
192
|
+
if (getTools()[toolId]) watchTool(toolId, mode);
|
|
193
|
+
}
|
|
194
|
+
socket.on("connect", handleConnect);
|
|
195
|
+
socket.on("devtool-watch", handleWatch);
|
|
196
|
+
socket.on("request-devtool-state", handleRequest);
|
|
197
|
+
socket.on("devtool-action", handleAction);
|
|
198
|
+
return () => {
|
|
199
|
+
socket.off("connect", handleConnect);
|
|
200
|
+
socket.off("devtool-watch", handleWatch);
|
|
201
|
+
socket.off("request-devtool-state", handleRequest);
|
|
202
|
+
socket.off("devtool-action", handleAction);
|
|
203
|
+
for (const toolId of [...unsubs.keys()]) unwatchTool(toolId);
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Emit-layer byte budgets — the choke-point guard.
|
|
5
|
+
*
|
|
6
|
+
* Every adapter's snapshot and every action result funnels through
|
|
7
|
+
* socket.emit, whose serializer walks the ENTIRE payload synchronously on
|
|
8
|
+
* the JS thread (and JSON.stringify does not honor structural sharing).
|
|
9
|
+
* Per-adapter guards get missed — the redux adapter shipping raw state
|
|
10
|
+
* trees froze and native-OOM-crashed enterprise apps whenever a dashboard
|
|
11
|
+
* was connected. One budget here closes every adapter's hole at once:
|
|
12
|
+
* oversized payloads are dropped (snapshots) or replaced with an error
|
|
13
|
+
* (action results) instead of being serialized.
|
|
14
|
+
*
|
|
15
|
+
* The estimator is O(limit) — early-abort walk, O(1) for giant strings —
|
|
16
|
+
* never O(payload). Copied per-package by design (see events/redux copies).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Max approx bytes for one tool snapshot emit. */
|
|
20
|
+
export const MAX_SNAPSHOT_EMIT_BYTES = 2 * 1024 * 1024;
|
|
21
|
+
/** Max approx bytes for one action-result emit (explicit requests get more). */
|
|
22
|
+
export const MAX_ACTION_RESULT_BYTES = 8 * 1024 * 1024;
|
|
23
|
+
export function approxSizeOver(value, limit) {
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
const stack = [value];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
while (stack.length > 0) {
|
|
28
|
+
if (bytes > limit) return {
|
|
29
|
+
over: true,
|
|
30
|
+
approxBytes: bytes
|
|
31
|
+
};
|
|
32
|
+
const v = stack.pop();
|
|
33
|
+
if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
|
|
34
|
+
if (seen.has(v)) continue;
|
|
35
|
+
seen.add(v);
|
|
36
|
+
if (Array.isArray(v)) {
|
|
37
|
+
bytes += 2 + v.length;
|
|
38
|
+
for (let i = 0; i < v.length; i++) stack.push(v[i]);
|
|
39
|
+
} else {
|
|
40
|
+
bytes += 2;
|
|
41
|
+
for (const k in v) {
|
|
42
|
+
bytes += k.length + 4;
|
|
43
|
+
stack.push(v[k]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else bytes += 4;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
over: bytes > limit,
|
|
50
|
+
approxBytes: bytes
|
|
51
|
+
};
|
|
52
|
+
}
|
package/lib/module/types.js
CHANGED
|
@@ -57,7 +57,20 @@ export const SYNC_PROTOCOL_VERSION = 1;
|
|
|
57
57
|
// DASHBOARD -> DEVICE MESSAGES
|
|
58
58
|
// ============================================================
|
|
59
59
|
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Server-consolidated signal that a tool gained/lost its audience.
|
|
62
|
+
*
|
|
63
|
+
* `watching` arms CAPTURE: the device subscribes to the tool, which is what
|
|
64
|
+
* makes its store start recording. `stream` decides whether the device also
|
|
65
|
+
* PUSHES a snapshot on every change (throttled), which is the expensive half —
|
|
66
|
+
* it serializes the whole payload on the JS thread ~5x/sec.
|
|
67
|
+
*
|
|
68
|
+
* Those are separate because their audiences are: a dashboard renders every
|
|
69
|
+
* snapshot, so it wants both. A request/response client (MCP) reads on demand
|
|
70
|
+
* via `request-devtool-state` and would throw the stream away — but it still
|
|
71
|
+
* needs the device to be RECORDING between its reads, or it only ever sees an
|
|
72
|
+
* empty list and concludes the app made no requests.
|
|
73
|
+
*/
|
|
61
74
|
|
|
62
75
|
/** Explicit request for a tool's current state (works even while unwatched). */
|
|
63
76
|
|
|
@@ -1,61 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import { SYNC_PROTOCOL_VERSION } from "./types";
|
|
6
|
-
const LOG = "[ExternalSync]";
|
|
7
|
-
|
|
8
|
-
// ==========================================================================
|
|
9
|
-
// Emit-layer byte budgets — the choke-point guard.
|
|
10
|
-
//
|
|
11
|
-
// Every adapter's snapshot and every action result funnels through
|
|
12
|
-
// socket.emit, whose serializer walks the ENTIRE payload synchronously on
|
|
13
|
-
// the JS thread (and JSON.stringify does not honor structural sharing).
|
|
14
|
-
// Per-adapter guards get missed — the redux adapter shipping raw state
|
|
15
|
-
// trees froze and native-OOM-crashed enterprise apps whenever a dashboard
|
|
16
|
-
// was connected. One budget here closes every adapter's hole at once:
|
|
17
|
-
// oversized payloads are dropped (snapshots) or replaced with an error
|
|
18
|
-
// (action results) instead of being serialized.
|
|
19
|
-
//
|
|
20
|
-
// The estimator is O(limit) — early-abort walk, O(1) for giant strings —
|
|
21
|
-
// never O(payload). Copied per-package by design (see events/redux copies).
|
|
22
|
-
// ==========================================================================
|
|
23
|
-
|
|
24
|
-
/** Max approx bytes for one tool snapshot emit. */
|
|
25
|
-
const MAX_SNAPSHOT_EMIT_BYTES = 2 * 1024 * 1024;
|
|
26
|
-
/** Max approx bytes for one action-result emit (explicit requests get more). */
|
|
27
|
-
const MAX_ACTION_RESULT_BYTES = 8 * 1024 * 1024;
|
|
28
|
-
function approxSizeOver(value, limit) {
|
|
29
|
-
let bytes = 0;
|
|
30
|
-
const stack = [value];
|
|
31
|
-
const seen = new Set();
|
|
32
|
-
while (stack.length > 0) {
|
|
33
|
-
if (bytes > limit) return {
|
|
34
|
-
over: true,
|
|
35
|
-
approxBytes: bytes
|
|
36
|
-
};
|
|
37
|
-
const v = stack.pop();
|
|
38
|
-
if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
|
|
39
|
-
if (seen.has(v)) continue;
|
|
40
|
-
seen.add(v);
|
|
41
|
-
if (Array.isArray(v)) {
|
|
42
|
-
bytes += 2 + v.length;
|
|
43
|
-
for (let i = 0; i < v.length; i++) stack.push(v[i]);
|
|
44
|
-
} else {
|
|
45
|
-
bytes += 2;
|
|
46
|
-
for (const k in v) {
|
|
47
|
-
bytes += k.length + 4;
|
|
48
|
-
stack.push(v[k]);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
} else bytes += 4;
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
over: bytes > limit,
|
|
55
|
-
approxBytes: bytes
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
4
|
+
import { attachDeviceSync } from "./attachDeviceSync";
|
|
59
5
|
/**
|
|
60
6
|
* Device-side sync bridge: syncs any number of dev tools to the desktop
|
|
61
7
|
* dashboard and executes actions invoked remotely.
|
|
@@ -96,183 +42,21 @@ export function useExternalSync({
|
|
|
96
42
|
|
|
97
43
|
// Watch state persists across effect re-runs (e.g. when tools are
|
|
98
44
|
// added/removed) so an already-watched tool stays subscribed.
|
|
99
|
-
const watchedRef = useRef(new
|
|
45
|
+
const watchedRef = useRef(new Map());
|
|
100
46
|
|
|
101
47
|
// Re-run the effect only when tools are added/removed, not on every
|
|
102
48
|
// object-reference change.
|
|
103
49
|
const toolIdsKey = Object.keys(tools).sort().join(",");
|
|
104
50
|
useEffect(() => {
|
|
105
51
|
if (!socket || !deviceId || !enabled) return;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
log(`${LOG} Failed to emit "${event}": ${error}. Payload must be JSON-serializable.`, enableLogs, "error");
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// One warning per tool per session — the drop repeats every snapshot.
|
|
119
|
-
const warnedOversized = new Set();
|
|
120
|
-
const sendSnapshot = toolId => {
|
|
121
|
-
const tool = toolsRef.current[toolId];
|
|
122
|
-
if (!tool) return;
|
|
123
|
-
lastEmit[toolId] = Date.now();
|
|
124
|
-
const payload = tool.getSnapshot();
|
|
125
|
-
|
|
126
|
-
// Emit-layer budget: never hand an unbounded payload to the socket
|
|
127
|
-
// serializer. A dropped snapshot means a stale dashboard panel for
|
|
128
|
-
// that tool; serializing it would freeze (and can OOM-kill) the app.
|
|
129
|
-
const {
|
|
130
|
-
over,
|
|
131
|
-
approxBytes
|
|
132
|
-
} = approxSizeOver(payload, MAX_SNAPSHOT_EMIT_BYTES);
|
|
133
|
-
if (over) {
|
|
134
|
-
if (!warnedOversized.has(toolId)) {
|
|
135
|
-
warnedOversized.add(toolId);
|
|
136
|
-
console.warn(`${LOG} "${toolId}" snapshot ≈${(approxBytes / 1e6).toFixed(1)}MB exceeds the ${MAX_SNAPSHOT_EMIT_BYTES / 1e6}MB sync budget — dropped (dashboard will be stale for this tool). The adapter should ship a wire-form snapshot (summaries + on-demand detail).`);
|
|
137
|
-
}
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const message = {
|
|
141
|
-
toolId,
|
|
142
|
-
persistentDeviceId: deviceId,
|
|
143
|
-
timestamp: lastEmit[toolId],
|
|
144
|
-
version: tool.version,
|
|
145
|
-
kind: "snapshot",
|
|
146
|
-
payload
|
|
147
|
-
};
|
|
148
|
-
safeEmit("devtool-sync", message);
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
// Leading + trailing throttle: emit immediately when outside the window,
|
|
152
|
-
// otherwise schedule a flush for the window's end so the dashboard always
|
|
153
|
-
// converges to the latest state.
|
|
154
|
-
const scheduleSnapshot = toolId => {
|
|
155
|
-
if (timers[toolId]) return;
|
|
156
|
-
const elapsed = Date.now() - (lastEmit[toolId] ?? 0);
|
|
157
|
-
if (elapsed >= throttleMs) {
|
|
158
|
-
sendSnapshot(toolId);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
timers[toolId] = setTimeout(() => {
|
|
162
|
-
delete timers[toolId];
|
|
163
|
-
sendSnapshot(toolId);
|
|
164
|
-
}, throttleMs - elapsed);
|
|
165
|
-
};
|
|
166
|
-
const watchTool = toolId => {
|
|
167
|
-
const tool = toolsRef.current[toolId];
|
|
168
|
-
if (!tool || unsubs.has(toolId)) return;
|
|
169
|
-
unsubs.set(toolId, tool.subscribe(() => scheduleSnapshot(toolId)));
|
|
170
|
-
log(`${LOG} Watching "${toolId}"`, enableLogs);
|
|
171
|
-
};
|
|
172
|
-
const unwatchTool = toolId => {
|
|
173
|
-
unsubs.get(toolId)?.();
|
|
174
|
-
unsubs.delete(toolId);
|
|
175
|
-
if (timers[toolId]) {
|
|
176
|
-
clearTimeout(timers[toolId]);
|
|
177
|
-
delete timers[toolId];
|
|
178
|
-
}
|
|
179
|
-
log(`${LOG} Stopped watching "${toolId}"`, enableLogs);
|
|
180
|
-
};
|
|
181
|
-
const sendCapabilities = () => {
|
|
182
|
-
const message = {
|
|
183
|
-
persistentDeviceId: deviceId,
|
|
184
|
-
protocolVersion: SYNC_PROTOCOL_VERSION,
|
|
185
|
-
tools: Object.entries(toolsRef.current).map(([toolId, tool]) => ({
|
|
186
|
-
toolId,
|
|
187
|
-
version: tool.version,
|
|
188
|
-
actions: Object.keys(tool.actions ?? {})
|
|
189
|
-
}))
|
|
190
|
-
};
|
|
191
|
-
safeEmit("devtool-capabilities", message);
|
|
192
|
-
};
|
|
193
|
-
const handleWatch = message => {
|
|
194
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
195
|
-
if (message.watching) {
|
|
196
|
-
watchedRef.current.add(message.toolId);
|
|
197
|
-
watchTool(message.toolId);
|
|
198
|
-
sendSnapshot(message.toolId);
|
|
199
|
-
} else {
|
|
200
|
-
watchedRef.current.delete(message.toolId);
|
|
201
|
-
unwatchTool(message.toolId);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
const handleRequest = message => {
|
|
205
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
206
|
-
sendSnapshot(message.toolId);
|
|
207
|
-
};
|
|
208
|
-
const handleAction = async message => {
|
|
209
|
-
if (!isForThisDevice(message.targetDeviceId)) return;
|
|
210
|
-
const {
|
|
211
|
-
requestId,
|
|
212
|
-
toolId,
|
|
213
|
-
action,
|
|
214
|
-
params
|
|
215
|
-
} = message;
|
|
216
|
-
const result = {
|
|
217
|
-
requestId,
|
|
218
|
-
toolId,
|
|
219
|
-
action,
|
|
220
|
-
persistentDeviceId: deviceId,
|
|
221
|
-
timestamp: Date.now(),
|
|
222
|
-
ok: false
|
|
223
|
-
};
|
|
224
|
-
const tool = toolsRef.current[toolId];
|
|
225
|
-
const fn = tool?.actions?.[action];
|
|
226
|
-
if (!fn) {
|
|
227
|
-
result.error = tool ? `Unknown action "${action}" on tool "${toolId}"` : `Unknown tool "${toolId}"`;
|
|
228
|
-
safeEmit("devtool-action-result", result);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
result.result = await fn(params);
|
|
233
|
-
result.ok = true;
|
|
234
|
-
} catch (error) {
|
|
235
|
-
result.error = error instanceof Error ? error.message : String(error);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Same budget for action results: e.g. redux getState({includeValues:
|
|
239
|
-
// true}) on a huge tree would otherwise OOM through this exact emit.
|
|
240
|
-
if (result.ok) {
|
|
241
|
-
const {
|
|
242
|
-
over,
|
|
243
|
-
approxBytes
|
|
244
|
-
} = approxSizeOver(result.result, MAX_ACTION_RESULT_BYTES);
|
|
245
|
-
if (over) {
|
|
246
|
-
result.ok = false;
|
|
247
|
-
result.result = undefined;
|
|
248
|
-
result.error = `Result ≈${(approxBytes / 1e6).toFixed(1)}MB exceeds the ${MAX_ACTION_RESULT_BYTES / 1e6}MB sync budget. Narrow the request (e.g. includeValues:false, a limit, or a more specific action).`;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
result.timestamp = Date.now();
|
|
252
|
-
safeEmit("devtool-action-result", result);
|
|
253
|
-
};
|
|
254
|
-
const handleConnect = () => {
|
|
255
|
-
// The server replies with current watch state for this device.
|
|
256
|
-
sendCapabilities();
|
|
257
|
-
};
|
|
258
|
-
if (socket.connected) sendCapabilities();
|
|
259
|
-
|
|
260
|
-
// Restore subscriptions for tools that were watched before this effect
|
|
261
|
-
// re-ran (or that arrived after the watch message).
|
|
262
|
-
for (const toolId of watchedRef.current) {
|
|
263
|
-
if (toolsRef.current[toolId]) watchTool(toolId);
|
|
264
|
-
}
|
|
265
|
-
socket.on("connect", handleConnect);
|
|
266
|
-
socket.on("devtool-watch", handleWatch);
|
|
267
|
-
socket.on("request-devtool-state", handleRequest);
|
|
268
|
-
socket.on("devtool-action", handleAction);
|
|
269
|
-
return () => {
|
|
270
|
-
socket.off("connect", handleConnect);
|
|
271
|
-
socket.off("devtool-watch", handleWatch);
|
|
272
|
-
socket.off("request-devtool-state", handleRequest);
|
|
273
|
-
socket.off("devtool-action", handleAction);
|
|
274
|
-
for (const toolId of [...unsubs.keys()]) unwatchTool(toolId);
|
|
275
|
-
};
|
|
52
|
+
return attachDeviceSync({
|
|
53
|
+
socket,
|
|
54
|
+
deviceId,
|
|
55
|
+
getTools: () => toolsRef.current,
|
|
56
|
+
watched: watchedRef.current,
|
|
57
|
+
throttleMs,
|
|
58
|
+
enableLogs
|
|
59
|
+
});
|
|
276
60
|
// toolIdsKey stands in for `tools` (see comment above its definition).
|
|
277
61
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
278
62
|
}, [socket, deviceId, enabled, throttleMs, enableLogs, toolIdsKey]);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ToolSyncAdapter } from "./types";
|
|
2
|
+
/** Minimal socket surface the device-side bridge needs. */
|
|
3
|
+
export type SyncSocket = {
|
|
4
|
+
connected: boolean;
|
|
5
|
+
emit: (event: string, ...args: unknown[]) => unknown;
|
|
6
|
+
on: (event: string, listener: (...args: any[]) => void) => unknown;
|
|
7
|
+
off: (event: string, listener: (...args: any[]) => void) => unknown;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* "capture" records into the tool's store without pushing snapshots; "stream"
|
|
11
|
+
* also emits on every change. See `DevToolWatchMessage.stream`.
|
|
12
|
+
*/
|
|
13
|
+
export type WatchMode = "capture" | "stream";
|
|
14
|
+
export type AttachDeviceSyncOptions = {
|
|
15
|
+
socket: SyncSocket;
|
|
16
|
+
deviceId: string;
|
|
17
|
+
getTools: () => Record<string, ToolSyncAdapter>;
|
|
18
|
+
/**
|
|
19
|
+
* Survives attach/detach so an already-watched tool stays subscribed — and
|
|
20
|
+
* at the same MODE, or a re-attach would silently start streaming to a
|
|
21
|
+
* client that only asked to record.
|
|
22
|
+
*/
|
|
23
|
+
watched: Map<string, WatchMode>;
|
|
24
|
+
throttleMs?: number;
|
|
25
|
+
enableLogs?: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Device-side sync bridge (no React). A tool's adapter is only subscribed
|
|
29
|
+
* while a dashboard is watching it (`devtool-watch`).
|
|
30
|
+
*/
|
|
31
|
+
export declare function attachDeviceSync({ socket, deviceId, getTools, watched, throttleMs, enableLogs, }: AttachDeviceSyncOptions): () => void;
|
|
32
|
+
//# sourceMappingURL=attachDeviceSync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachDeviceSync.d.ts","sourceRoot":"","sources":["../../src/attachDeviceSync.ts"],"names":[],"mappings":"AAMA,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,CA2NtC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit-layer byte budgets — the choke-point guard.
|
|
3
|
+
*
|
|
4
|
+
* Every adapter's snapshot and every action result funnels through
|
|
5
|
+
* socket.emit, whose serializer walks the ENTIRE payload synchronously on
|
|
6
|
+
* the JS thread (and JSON.stringify does not honor structural sharing).
|
|
7
|
+
* Per-adapter guards get missed — the redux adapter shipping raw state
|
|
8
|
+
* trees froze and native-OOM-crashed enterprise apps whenever a dashboard
|
|
9
|
+
* was connected. One budget here closes every adapter's hole at once:
|
|
10
|
+
* oversized payloads are dropped (snapshots) or replaced with an error
|
|
11
|
+
* (action results) instead of being serialized.
|
|
12
|
+
*
|
|
13
|
+
* The estimator is O(limit) — early-abort walk, O(1) for giant strings —
|
|
14
|
+
* never O(payload). Copied per-package by design (see events/redux copies).
|
|
15
|
+
*/
|
|
16
|
+
/** Max approx bytes for one tool snapshot emit. */
|
|
17
|
+
export declare const MAX_SNAPSHOT_EMIT_BYTES: number;
|
|
18
|
+
/** Max approx bytes for one action-result emit (explicit requests get more). */
|
|
19
|
+
export declare const MAX_ACTION_RESULT_BYTES: number;
|
|
20
|
+
export declare function approxSizeOver(value: unknown, limit: number): {
|
|
21
|
+
over: boolean;
|
|
22
|
+
approxBytes: number;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=emitBudget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emitBudget.d.ts","sourceRoot":"","sources":["../../src/emitBudget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,mDAAmD;AACnD,eAAO,MAAM,uBAAuB,QAAkB,CAAC;AACvD,gFAAgF;AAChF,eAAO,MAAM,uBAAuB,QAAkB,CAAC;AAEvD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,GACZ;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CA2BxC"}
|
|
@@ -60,11 +60,44 @@ export interface DevToolActionResultMessage {
|
|
|
60
60
|
/** Present when not ok. */
|
|
61
61
|
error?: string;
|
|
62
62
|
}
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Server-consolidated signal that a tool gained/lost its audience.
|
|
65
|
+
*
|
|
66
|
+
* `watching` arms CAPTURE: the device subscribes to the tool, which is what
|
|
67
|
+
* makes its store start recording. `stream` decides whether the device also
|
|
68
|
+
* PUSHES a snapshot on every change (throttled), which is the expensive half —
|
|
69
|
+
* it serializes the whole payload on the JS thread ~5x/sec.
|
|
70
|
+
*
|
|
71
|
+
* Those are separate because their audiences are: a dashboard renders every
|
|
72
|
+
* snapshot, so it wants both. A request/response client (MCP) reads on demand
|
|
73
|
+
* via `request-devtool-state` and would throw the stream away — but it still
|
|
74
|
+
* needs the device to be RECORDING between its reads, or it only ever sees an
|
|
75
|
+
* empty list and concludes the app made no requests.
|
|
76
|
+
*/
|
|
64
77
|
export interface DevToolWatchMessage {
|
|
65
78
|
toolId: string;
|
|
66
79
|
targetDeviceId: string;
|
|
67
80
|
watching: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Push snapshots on change. Defaults to TRUE when absent, so a dashboard
|
|
83
|
+
* that predates this field behaves exactly as before.
|
|
84
|
+
*
|
|
85
|
+
* Only honored alongside `consolidated` — see below.
|
|
86
|
+
*/
|
|
87
|
+
stream?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Set by the BROKER to mean "I merged every dashboard's request and this is
|
|
90
|
+
* the mode the device should be in".
|
|
91
|
+
*
|
|
92
|
+
* It exists because the desktop and the MCP server each embed their own copy
|
|
93
|
+
* of the broker and ship on separate cycles, so a new client can end up
|
|
94
|
+
* talking through an old broker. An old broker forwards a dashboard's watch
|
|
95
|
+
* message verbatim — it has no idea modes exist — so an agent's
|
|
96
|
+
* `stream: false` would reach the device unmerged and silently stop the
|
|
97
|
+
* developer's panel from updating. Requiring this flag makes that
|
|
98
|
+
* combination degrade to plain streaming (today's behavior) instead.
|
|
99
|
+
*/
|
|
100
|
+
consolidated?: boolean;
|
|
68
101
|
}
|
|
69
102
|
/** Explicit request for a tool's current state (works even while unwatched). */
|
|
70
103
|
export interface DevToolRequestMessage {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA0B/C,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,+EAA+E;AAC/E,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC;AAMlC,mFAAmF;AACnF,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEzE;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAChD,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACtC;AAMD,4EAA4E;AAC5E,MAAM,WAAW,yBAAyB;IACxC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,8CAA8C;AAC9C,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,OAAO,CAAC;IACZ,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useExternalSync.d.ts","sourceRoot":"","sources":["../../src/useExternalSync.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"useExternalSync.d.ts","sourceRoot":"","sources":["../../src/useExternalSync.ts"],"names":[],"mappings":"AAEA,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,QA0BtB"}
|