@buoy-gg/external-sync 7.0.10 → 7.0.11
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.
|
@@ -9,6 +9,57 @@ var _logger = require("./utils/logger");
|
|
|
9
9
|
var _types = require("./types");
|
|
10
10
|
const LOG = "[ExternalSync]";
|
|
11
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
|
+
|
|
12
63
|
/**
|
|
13
64
|
* Device-side sync bridge: syncs any number of dev tools to the desktop
|
|
14
65
|
* dashboard and executes actions invoked remotely.
|
|
@@ -67,17 +118,36 @@ function useExternalSync({
|
|
|
67
118
|
(0, _logger.log)(`${LOG} Failed to emit "${event}": ${error}. Payload must be JSON-serializable.`, enableLogs, "error");
|
|
68
119
|
}
|
|
69
120
|
};
|
|
121
|
+
|
|
122
|
+
// One warning per tool per session — the drop repeats every snapshot.
|
|
123
|
+
const warnedOversized = new Set();
|
|
70
124
|
const sendSnapshot = toolId => {
|
|
71
125
|
const tool = toolsRef.current[toolId];
|
|
72
126
|
if (!tool) return;
|
|
73
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
|
+
}
|
|
74
144
|
const message = {
|
|
75
145
|
toolId,
|
|
76
146
|
persistentDeviceId: deviceId,
|
|
77
147
|
timestamp: lastEmit[toolId],
|
|
78
148
|
version: tool.version,
|
|
79
149
|
kind: "snapshot",
|
|
80
|
-
payload
|
|
150
|
+
payload
|
|
81
151
|
};
|
|
82
152
|
safeEmit("devtool-sync", message);
|
|
83
153
|
};
|
|
@@ -168,6 +238,20 @@ function useExternalSync({
|
|
|
168
238
|
} catch (error) {
|
|
169
239
|
result.error = error instanceof Error ? error.message : String(error);
|
|
170
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
|
+
}
|
|
171
255
|
result.timestamp = Date.now();
|
|
172
256
|
safeEmit("devtool-action-result", result);
|
|
173
257
|
};
|
|
@@ -5,6 +5,57 @@ import { log } from "./utils/logger";
|
|
|
5
5
|
import { SYNC_PROTOCOL_VERSION } from "./types";
|
|
6
6
|
const LOG = "[ExternalSync]";
|
|
7
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
|
+
|
|
8
59
|
/**
|
|
9
60
|
* Device-side sync bridge: syncs any number of dev tools to the desktop
|
|
10
61
|
* dashboard and executes actions invoked remotely.
|
|
@@ -63,17 +114,36 @@ export function useExternalSync({
|
|
|
63
114
|
log(`${LOG} Failed to emit "${event}": ${error}. Payload must be JSON-serializable.`, enableLogs, "error");
|
|
64
115
|
}
|
|
65
116
|
};
|
|
117
|
+
|
|
118
|
+
// One warning per tool per session — the drop repeats every snapshot.
|
|
119
|
+
const warnedOversized = new Set();
|
|
66
120
|
const sendSnapshot = toolId => {
|
|
67
121
|
const tool = toolsRef.current[toolId];
|
|
68
122
|
if (!tool) return;
|
|
69
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
|
+
}
|
|
70
140
|
const message = {
|
|
71
141
|
toolId,
|
|
72
142
|
persistentDeviceId: deviceId,
|
|
73
143
|
timestamp: lastEmit[toolId],
|
|
74
144
|
version: tool.version,
|
|
75
145
|
kind: "snapshot",
|
|
76
|
-
payload
|
|
146
|
+
payload
|
|
77
147
|
};
|
|
78
148
|
safeEmit("devtool-sync", message);
|
|
79
149
|
};
|
|
@@ -164,6 +234,20 @@ export function useExternalSync({
|
|
|
164
234
|
} catch (error) {
|
|
165
235
|
result.error = error instanceof Error ? error.message : String(error);
|
|
166
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
|
+
}
|
|
167
251
|
result.timestamp = Date.now();
|
|
168
252
|
safeEmit("devtool-action-result", result);
|
|
169
253
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useExternalSync.d.ts","sourceRoot":"","sources":["../../src/useExternalSync.ts"],"names":[],"mappings":"AAEA,OAAO,EAQL,KAAK,oBAAoB,EAC1B,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"useExternalSync.d.ts","sourceRoot":"","sources":["../../src/useExternalSync.ts"],"names":[],"mappings":"AAEA,OAAO,EAQL,KAAK,oBAAoB,EAC1B,MAAM,SAAS,CAAC;AAyDjB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,MAAM,EACN,QAAQ,EACR,OAAc,EACd,UAAgB,EAChB,UAAkB,GACnB,EAAE,oBAAoB,QAkNtB"}
|