@buoy-gg/external-sync 7.0.41 → 7.0.42

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.
Files changed (48) hide show
  1. package/lib/commonjs/attachDeviceSync.js +1 -294
  2. package/lib/commonjs/brokerAccess.js +1 -29
  3. package/lib/commonjs/crashFlush.js +1 -73
  4. package/lib/commonjs/emitBudget.js +1 -57
  5. package/lib/commonjs/index.js +1 -83
  6. package/lib/commonjs/realNow.js +1 -0
  7. package/lib/commonjs/syncStatusStore.js +1 -40
  8. package/lib/commonjs/types.js +1 -97
  9. package/lib/commonjs/useExternalSync.js +1 -73
  10. package/lib/commonjs/useExternalSyncSocket.js +1 -241
  11. package/lib/commonjs/utils/getDefaultSocketURL.js +1 -84
  12. package/lib/commonjs/utils/getDefaultSocketURL.web.js +1 -27
  13. package/lib/commonjs/utils/logger.js +1 -24
  14. package/lib/commonjs/utils/platformUtils.js +1 -50
  15. package/lib/commonjs/utils/platformUtils.web.js +1 -9
  16. package/lib/module/attachDeviceSync.js +1 -290
  17. package/lib/module/brokerAccess.js +1 -25
  18. package/lib/module/crashFlush.js +1 -67
  19. package/lib/module/emitBudget.js +1 -52
  20. package/lib/module/index.js +1 -29
  21. package/lib/module/realNow.js +1 -0
  22. package/lib/module/syncStatusStore.js +1 -34
  23. package/lib/module/types.js +1 -93
  24. package/lib/module/useExternalSync.js +1 -69
  25. package/lib/module/useExternalSyncSocket.js +1 -235
  26. package/lib/module/utils/getDefaultSocketURL.js +1 -76
  27. package/lib/module/utils/getDefaultSocketURL.web.js +1 -19
  28. package/lib/module/utils/logger.js +1 -20
  29. package/lib/module/utils/platformUtils.js +1 -45
  30. package/lib/module/utils/platformUtils.web.js +1 -5
  31. package/lib/typescript/realNow.d.ts +27 -0
  32. package/lib/typescript/syncStatusStore.d.ts +0 -7
  33. package/lib/web/index.mjs +1 -2
  34. package/package.json +2 -2
  35. package/lib/typescript/attachDeviceSync.d.ts.map +0 -1
  36. package/lib/typescript/brokerAccess.d.ts.map +0 -1
  37. package/lib/typescript/crashFlush.d.ts.map +0 -1
  38. package/lib/typescript/emitBudget.d.ts.map +0 -1
  39. package/lib/typescript/index.d.ts.map +0 -1
  40. package/lib/typescript/syncStatusStore.d.ts.map +0 -1
  41. package/lib/typescript/types.d.ts.map +0 -1
  42. package/lib/typescript/useExternalSync.d.ts.map +0 -1
  43. package/lib/typescript/useExternalSyncSocket.d.ts.map +0 -1
  44. package/lib/typescript/utils/getDefaultSocketURL.d.ts.map +0 -1
  45. package/lib/typescript/utils/getDefaultSocketURL.web.d.ts.map +0 -1
  46. package/lib/typescript/utils/logger.d.ts.map +0 -1
  47. package/lib/typescript/utils/platformUtils.d.ts.map +0 -1
  48. package/lib/typescript/utils/platformUtils.web.d.ts.map +0 -1
@@ -1,290 +1 @@
1
- "use strict";
2
-
3
- import { LicenseManager } from "@buoy-gg/license";
4
- import { log } from "./utils/logger";
5
- import { registerToolFlusher, unregisterToolFlusher } from "./crashFlush";
6
- import { MAX_ACTION_RESULT_BYTES, MAX_SNAPSHOT_EMIT_BYTES, approxSizeOver } from "./emitBudget";
7
- import { SYNC_PROTOCOL_VERSION } from "./types";
8
- const LOG = "[ExternalSync]";
9
-
10
- /** Minimal socket surface the device-side bridge needs. */
11
-
12
- /**
13
- * "capture" records into the tool's store without pushing snapshots; "stream"
14
- * also emits on every change. See `DevToolWatchMessage.stream`.
15
- */
16
-
17
- /**
18
- * Device-side sync bridge (no React). A tool's adapter is only subscribed
19
- * while a dashboard is watching it (`devtool-watch`).
20
- */
21
- export function attachDeviceSync({
22
- socket,
23
- deviceId,
24
- getTools,
25
- watched,
26
- throttleMs = 200,
27
- enableLogs = false
28
- }) {
29
- let disposed = false;
30
- let brokerReady = false;
31
- let accessGeneration = 0;
32
- const allowed = () => !disposed && brokerReady && socket.connected && LicenseManager.hasAccountAccess();
33
- const unsubs = new Map();
34
- const lastEmit = {};
35
- const timers = {};
36
- const isForThisDevice = targetDeviceId => targetDeviceId === "All" || targetDeviceId === deviceId;
37
- const safeEmit = (event, message) => {
38
- if (!allowed()) return;
39
- try {
40
- socket.emit(event, message);
41
- } catch (error) {
42
- log(`${LOG} Failed to emit "${event}": ${error}. Payload must be JSON-serializable.`, enableLogs, "error");
43
- }
44
- };
45
- const warnedOversized = new Set();
46
- const sendSnapshot = toolId => {
47
- if (!allowed()) return;
48
- const tool = getTools()[toolId];
49
- if (!tool) return;
50
- lastEmit[toolId] = Date.now();
51
- let payload;
52
- try {
53
- payload = tool.getSnapshot();
54
- } catch {
55
- return;
56
- }
57
- const {
58
- over,
59
- approxBytes
60
- } = approxSizeOver(payload, MAX_SNAPSHOT_EMIT_BYTES);
61
- if (over) {
62
- if (!warnedOversized.has(toolId)) {
63
- warnedOversized.add(toolId);
64
- 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).`);
65
- }
66
- return;
67
- }
68
- const message = {
69
- toolId,
70
- persistentDeviceId: deviceId,
71
- timestamp: lastEmit[toolId],
72
- version: tool.version,
73
- kind: "snapshot",
74
- payload
75
- };
76
- safeEmit("devtool-sync", message);
77
- };
78
- const scheduleSnapshot = toolId => {
79
- if (timers[toolId]) return;
80
- const elapsed = Date.now() - (lastEmit[toolId] ?? 0);
81
- if (elapsed >= throttleMs) {
82
- sendSnapshot(toolId);
83
- return;
84
- }
85
- timers[toolId] = setTimeout(() => {
86
- delete timers[toolId];
87
- sendSnapshot(toolId);
88
- }, throttleMs - elapsed);
89
- };
90
-
91
- /**
92
- * Emit a tool's snapshot immediately, cancelling any throttled push.
93
- * The escape hatch for "the tree is about to die, get this out now" — see
94
- * ./crashFlush. Ignores watch MODE: a crash flush is worth sending even to a
95
- * dashboard that only asked to record.
96
- *
97
- * It does NOT ignore "is anyone there". Building a snapshot is the expensive
98
- * part of this tool (deep-copying every captured entry, then walking it to
99
- * price it), and `socket.emit` on a disconnected socket queues into
100
- * socket.io's unbounded send buffer. Without these two guards, an app logging
101
- * errors in a loop with no dashboard attached would pay that cost several
102
- * times a second and grow the buffer until it ran out of memory — in a
103
- * customer's app, for no observer.
104
- */
105
- const flushNow = toolId => {
106
- if (!socket.connected) return;
107
- if (!unsubs.has(toolId)) return; // nobody is watching this tool
108
- if (timers[toolId]) {
109
- clearTimeout(timers[toolId]);
110
- delete timers[toolId];
111
- }
112
- sendSnapshot(toolId);
113
- };
114
- registerToolFlusher(flushNow);
115
-
116
- /** The mode each currently-subscribed tool is wired for. */
117
- const activeModes = new Map();
118
- const watchTool = (toolId, mode) => {
119
- if (!allowed()) return;
120
- const tool = getTools()[toolId];
121
- if (!tool) return;
122
- if (unsubs.has(toolId)) {
123
- if (activeModes.get(toolId) === mode) return;
124
- // Mode changed (a streaming dashboard left, or arrived) — rewire, but
125
- // do NOT drop the subscription in between: releasing it would take the
126
- // store's subscriber count to 0 and stop capture mid-flight.
127
- const previous = unsubs.get(toolId);
128
- unsubs.set(toolId, tool.subscribe(mode === "stream" ? () => scheduleSnapshot(toolId) : () => {}));
129
- activeModes.set(toolId, mode);
130
- previous?.();
131
- log(`${LOG} "${toolId}" watch mode → ${mode}`, enableLogs);
132
- return;
133
- }
134
- unsubs.set(toolId, tool.subscribe(mode === "stream" ? () => scheduleSnapshot(toolId) : () => {}));
135
- activeModes.set(toolId, mode);
136
- log(`${LOG} Watching "${toolId}" (${mode})`, enableLogs);
137
- };
138
- const unwatchTool = toolId => {
139
- unsubs.get(toolId)?.();
140
- unsubs.delete(toolId);
141
- activeModes.delete(toolId);
142
- if (timers[toolId]) {
143
- clearTimeout(timers[toolId]);
144
- delete timers[toolId];
145
- }
146
- log(`${LOG} Stopped watching "${toolId}"`, enableLogs);
147
- };
148
- const sendCapabilities = () => {
149
- const message = {
150
- persistentDeviceId: deviceId,
151
- protocolVersion: SYNC_PROTOCOL_VERSION,
152
- tools: Object.entries(getTools()).map(([toolId, tool]) => ({
153
- toolId,
154
- version: tool.version,
155
- actions: Object.keys(tool.actions ?? {})
156
- }))
157
- };
158
- safeEmit("devtool-capabilities", message);
159
- };
160
- const handleWatch = message => {
161
- if (!allowed() || !isForThisDevice(message.targetDeviceId)) return;
162
- if (message.watching) {
163
- // `stream` is only trustworthy once a broker has merged every
164
- // dashboard's request (see DevToolWatchMessage.consolidated). Without
165
- // that, an unmerged capture request could quiet a dashboard that is
166
- // actively painting, so default to streaming.
167
- const mode = message.consolidated === true && message.stream === false ? "capture" : "stream";
168
- watched.set(message.toolId, mode);
169
- watchTool(message.toolId, mode);
170
- // Only a streaming audience gets the priming snapshot. A capture-only
171
- // client reads with `request-devtool-state` when it actually wants one;
172
- // pushing here would hand it the very payload it asked not to receive.
173
- if (mode === "stream") sendSnapshot(message.toolId);
174
- } else {
175
- watched.delete(message.toolId);
176
- unwatchTool(message.toolId);
177
- }
178
- };
179
- const handleRequest = message => {
180
- if (!allowed() || !isForThisDevice(message.targetDeviceId)) return;
181
- sendSnapshot(message.toolId);
182
- };
183
- const handleAction = async message => {
184
- if (!allowed() || !isForThisDevice(message.targetDeviceId)) return;
185
- const startedAt = accessGeneration;
186
- const {
187
- requestId,
188
- toolId,
189
- action,
190
- params
191
- } = message;
192
- const result = {
193
- requestId,
194
- toolId,
195
- action,
196
- persistentDeviceId: deviceId,
197
- timestamp: Date.now(),
198
- ok: false
199
- };
200
- const tool = getTools()[toolId];
201
- const fn = tool?.actions?.[action];
202
- if (!fn) {
203
- result.error = tool ? `Unknown action "${action}" on tool "${toolId}"` : `Unknown tool "${toolId}"`;
204
- safeEmit("devtool-action-result", result);
205
- return;
206
- }
207
- try {
208
- result.result = await fn(params);
209
- result.ok = true;
210
- } catch (error) {
211
- result.error = error instanceof Error ? error.message : String(error);
212
- }
213
-
214
- // A subsequent admission cannot authorize work from the previous session.
215
- if (startedAt !== accessGeneration || !allowed()) return;
216
- if (result.ok) {
217
- const {
218
- over,
219
- approxBytes
220
- } = approxSizeOver(result.result, MAX_ACTION_RESULT_BYTES);
221
- if (over) {
222
- result.ok = false;
223
- result.result = undefined;
224
- 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).`;
225
- }
226
- }
227
- result.timestamp = Date.now();
228
- safeEmit("devtool-action-result", result);
229
- };
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
- });
244
- sendCapabilities();
245
- };
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);
268
- for (const [toolId, mode] of watched) {
269
- if (getTools()[toolId]) watchTool(toolId, mode);
270
- }
271
- socket.on("connect", handleConnect);
272
- socket.on("disconnect", handleDisconnect);
273
- socket.on("devtool-watch", handleWatch);
274
- socket.on("request-devtool-state", handleRequest);
275
- socket.on("devtool-action", handleAction);
276
- if (socket.connected) handleConnect();
277
- return () => {
278
- disposed = true;
279
- offAccount();
280
- socket.off("buoy:access", handleAccess);
281
- dropToolWork();
282
- unregisterToolFlusher(flushNow);
283
- socket.off("connect", handleConnect);
284
- socket.off("disconnect", handleDisconnect);
285
- socket.off("devtool-watch", handleWatch);
286
- socket.off("request-devtool-state", handleRequest);
287
- socket.off("devtool-action", handleAction);
288
- for (const toolId of [...unsubs.keys()]) unwatchTool(toolId);
289
- };
290
- }
1
+ "use strict";import{LicenseManager as w}from"@buoy-gg/license";import{log as A}from"./utils/logger";import{registerToolFlusher as Q,unregisterToolFlusher as Z}from"./crashFlush";import{MAX_ACTION_RESULT_BYTES as G,MAX_SNAPSHOT_EMIT_BYTES as X,approxSizeOver as H}from"./emitBudget";import{SYNC_PROTOCOL_VERSION as k}from"./types";import{realNow as T}from"./realNow";const p="[ExternalSync]";export function attachDeviceSync({socket:n,deviceId:h,getTools:f,watched:m,throttleMs:N=200,enableLogs:v=false}){let _=false;let y=false;let O=0;const i=()=>!_&&y&&n.connected&&w.hasAccountAccess();const c=new Map;const B={};const o={};const M=e=>e==="All"||e===h;const b=(e,t)=>{if(!i())return;try{n.emit(e,t)}catch(r){A(`${p} Failed to emit "${e}": ${r}. Payload must be JSON-serializable.`,v,"error")}};const q=new Set;const u=e=>{if(!i())return;const t=f()[e];if(!t)return;B[e]=T();let r;try{r=t.getSnapshot()}catch{return}const{over:a,approxBytes:l}=H(r,X);if(a){if(!q.has(e)){q.add(e);console.warn(`${p} "${e}" snapshot \u2248${(l/1e6).toFixed(1)}MB exceeds the ${X/1e6}MB sync budget \u2014 dropped (dashboard will be stale for this tool). The adapter should ship a wire-form snapshot (summaries + on-demand detail).`)}return}const D={toolId:e,persistentDeviceId:h,timestamp:B[e],version:t.version,kind:"snapshot",payload:r};b("devtool-sync",D)};const F=e=>{if(o[e])return;const t=T()-(B[e]??0);if(t>=N){u(e);return}o[e]=setTimeout(()=>{delete o[e];u(e)},N-t)};const P=e=>{if(!n.connected)return;if(!c.has(e))return;if(o[e]){clearTimeout(o[e]);delete o[e]}u(e)};Q(P);const $=new Map;const R=(e,t)=>{if(!i())return;const r=f()[e];if(!r)return;if(c.has(e)){if($.get(e)===t)return;const a=c.get(e);c.set(e,r.subscribe(t==="stream"?()=>F(e):()=>{}));$.set(e,t);a?.();A(`${p} "${e}" watch mode \u2192 ${t}`,v);return}c.set(e,r.subscribe(t==="stream"?()=>F(e):()=>{}));$.set(e,t);A(`${p} Watching "${e}" (${t})`,v)};const x=e=>{c.get(e)?.();c.delete(e);$.delete(e);if(o[e]){clearTimeout(o[e]);delete o[e]}A(`${p} Stopped watching "${e}"`,v)};const J=()=>{const e={persistentDeviceId:h,protocolVersion:k,tools:Object.entries(f()).map(([t,r])=>({toolId:t,version:r.version,actions:Object.keys(r.actions??{})}))};b("devtool-capabilities",e)};const C=e=>{if(!i()||!M(e.targetDeviceId))return;if(e.watching){const t=e.consolidated===true&&e.stream===false?"capture":"stream";m.set(e.toolId,t);R(e.toolId,t);if(t==="stream")u(e.toolId)}else{m.delete(e.toolId);x(e.toolId)}};const g=e=>{if(!i()||!M(e.targetDeviceId))return;u(e.toolId)};const j=async e=>{if(!i()||!M(e.targetDeviceId))return;const t=O;const{requestId:r,toolId:a,action:l,params:D}=e;const s={requestId:r,toolId:a,action:l,persistentDeviceId:h,timestamp:T(),ok:false};const W=f()[a];const Y=W?.actions?.[l];if(!Y){s.error=W?`Unknown action "${l}" on tool "${a}"`:`Unknown tool "${a}"`;b("devtool-action-result",s);return}try{s.result=await Y(D);s.ok=true}catch(d){s.error=d instanceof Error?d.message:String(d)}if(t!==O||!i())return;if(s.ok){const{over:d,approxBytes:K}=H(s.result,G);if(d){s.ok=false;s.result=void 0;s.error=`Result \u2248${(K/1e6).toFixed(1)}MB exceeds the ${G/1e6}MB sync budget. Narrow the request (e.g. includeValues:false, a limit, or a more specific action).`}}s.timestamp=T();b("devtool-action-result",s)};const S=()=>{O+=1;for(const t of[...c.keys()])x(t);m.clear();for(const t of Object.values(o))clearTimeout(t);const e=n;if(e.sendBuffer)e.sendBuffer=e.sendBuffer.filter(t=>!["devtool-sync","devtool-capabilities","devtool-action-result","device-license"].includes(String(t.data?.[0])))};const z=()=>{const e=w.getState();n.emit("device-license",{tier:e.tier,isPro:e.isPro?"1":"0"});J()};const U=e=>{y=e?.protocol===1&&e.ready===true;if(!i())S();else z()};const L=w.subscribe(()=>{if(!w.hasAccountAccess()){if(n.connected)n.emit("device-license",{tier:"locked",isPro:"0"});S()}else if(i())z()});const E=()=>{y=false;n.emit("buoy:access:request")};const V=()=>{y=false;S()};n.on("buoy:access",U);for(const[e,t]of m){if(f()[e])R(e,t)}n.on("connect",E);n.on("disconnect",V);n.on("devtool-watch",C);n.on("request-devtool-state",g);n.on("devtool-action",j);if(n.connected)E();return()=>{_=true;L();n.off("buoy:access",U);S();Z(P);n.off("connect",E);n.off("disconnect",V);n.off("devtool-watch",C);n.off("request-devtool-state",g);n.off("devtool-action",j);for(const e of[...c.keys()])x(e)}}
@@ -1,25 +1 @@
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
+ "use strict";export function watchBrokerAccess(c,e){const n=()=>e(false);const o=()=>{n();c.emit("buoy:access:request")};const s=r=>{const t=r;e(t?.protocol===1&&t?.ready===true)};c.on("connect",o);c.on("disconnect",n);c.on("buoy:access",s);n();if(c.connected)o();return()=>{c.off("connect",o);c.off("disconnect",n);c.off("buoy:access",s)}}
@@ -1,67 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * crashFlush — a global "push this tool's snapshot RIGHT NOW" seam.
5
- *
6
- * Why a global and not an import: the device sync bridge lives in the React
7
- * tree (`useExternalSync`), but the moment worth capturing most — a fatal
8
- * render error — is the moment that tree dies. React unmounts the root right
9
- * after the error is reported, which detaches the bridge and cancels the
10
- * normal throttled snapshot push, so the crash never reaches the dashboard or
11
- * the MCP server. The app then looks like it simply stopped answering.
12
- *
13
- * A tool that knows it is about to lose the tree (see @buoy-gg/console's fatal
14
- * hook) calls {@link flushToolSyncNow} synchronously from its error handler,
15
- * while the socket is still attached, and the snapshot goes out immediately.
16
- *
17
- * It is a global because the tools deliberately do NOT depend on this package
18
- * (their adapters are duck-typed and picked up by auto-discovery), so there is
19
- * no import edge to hang a registry on. Callers must treat it as best-effort:
20
- * absent bridge = no-op.
21
- */
22
-
23
- const FLUSH_KEY = "__buoyFlushToolSync";
24
-
25
- /** Emit `toolId`'s current snapshot now, bypassing the throttle. */
26
-
27
- /**
28
- * A SET, not a single slot: React can mount a second provider (or re-run the
29
- * attach effect setup before the old one's cleanup) so two bridges are briefly
30
- * live at once. A last-writer-wins slot would let the second registration
31
- * silently disable the first's crash flush — failing in exactly the moment it
32
- * is the only thing that gets the crash out, with no error and no log.
33
- */
34
-
35
- function flushers() {
36
- const g = globalThis;
37
- if (!g[FLUSH_KEY]) g[FLUSH_KEY] = new Set();
38
- return g[FLUSH_KEY];
39
- }
40
-
41
- /** Install a bridge's flusher. Called by `attachDeviceSync`. */
42
- export function registerToolFlusher(flush) {
43
- flushers().add(flush);
44
- }
45
-
46
- /** Remove one bridge's flusher, leaving any others intact. */
47
- export function unregisterToolFlusher(flush) {
48
- flushers().delete(flush);
49
- }
50
-
51
- /**
52
- * Push a tool's snapshot immediately through every attached sync bridge.
53
- * Never throws — a crash path must not crash.
54
- */
55
- export function flushToolSyncNow(toolId) {
56
- try {
57
- for (const flush of flushers()) {
58
- try {
59
- flush(toolId);
60
- } catch {
61
- // One bad bridge must not stop the others.
62
- }
63
- }
64
- } catch {
65
- // Best-effort by design.
66
- }
67
- }
1
+ "use strict";const t="__buoyFlushToolSync";function e(){const o=globalThis;if(!o[t])o[t]=new Set;return o[t]}export function registerToolFlusher(o){e().add(o)}export function unregisterToolFlusher(o){e().delete(o)}export function flushToolSyncNow(o){try{for(const n of e()){try{n(o)}catch{}}}catch{}}
@@ -1,52 +1 @@
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
- }
1
+ "use strict";export const MAX_SNAPSHOT_EMIT_BYTES=2*1024*1024;export const MAX_ACTION_RESULT_BYTES=8*1024*1024;export function approxSizeOver(p,s){let t=0;const o=[p];const r=new Set;while(o.length>0){if(t>s)return{over:true,approxBytes:t};const e=o.pop();if(e===null||e===void 0)t+=4;else if(typeof e==="string")t+=e.length+2;else if(typeof e==="number")t+=8;else if(typeof e==="boolean")t+=5;else if(typeof e==="object"){if(r.has(e))continue;r.add(e);if(Array.isArray(e)){t+=2+e.length;for(let n=0;n<e.length;n++)o.push(e[n])}else{t+=2;for(const n in e){t+=n.length+4;o.push(e[n])}}}else t+=4}return{over:t>s,approxBytes:t}}
@@ -1,29 +1 @@
1
- "use strict";
2
-
3
- // ============================================================
4
- // HOOKS
5
- // ============================================================
6
- export { useExternalSync } from "./useExternalSync";
7
- export { useExternalSyncSocket } from "./useExternalSyncSocket";
8
-
9
- // ============================================================
10
- // BROKER URL DERIVATION
11
- // ============================================================
12
- export { getDefaultSocketURL, getMetroHost, DEFAULT_BROKER_PORT } from "./utils/getDefaultSocketURL";
13
-
14
- // ============================================================
15
- // CRASH FLUSH (get a tool's snapshot out before the tree dies)
16
- // ============================================================
17
- export { flushToolSyncNow, registerToolFlusher, unregisterToolFlusher } from "./crashFlush";
18
- // ============================================================
19
- // SYNC STATUS (read by the floating menu's settings modal)
20
- // ============================================================
21
- export { getSyncStatus, setSyncStatus, subscribeSyncStatus } from "./syncStatusStore";
22
- // ============================================================
23
- // PROTOCOL
24
- // ============================================================
25
- export { SYNC_PROTOCOL_VERSION } from "./types";
26
-
27
- // ============================================================
28
- // TYPES
29
- // ============================================================
1
+ "use strict";export{useExternalSync}from"./useExternalSync";export{useExternalSyncSocket}from"./useExternalSyncSocket";export{getDefaultSocketURL,getMetroHost,DEFAULT_BROKER_PORT}from"./utils/getDefaultSocketURL";export{flushToolSyncNow,registerToolFlusher,unregisterToolFlusher}from"./crashFlush";export{getSyncStatus,setSyncStatus,subscribeSyncStatus}from"./syncStatusStore";export{SYNC_PROTOCOL_VERSION}from"./types";
@@ -0,0 +1 @@
1
+ "use strict";export function realNow(){const e=globalThis.__buoyRealNow;return typeof e==="function"?e():Date.now()}function l(){const e=globalThis.__buoyRealTimers;return e??globalThis}export function realSetTimeout(e,t){return l().setTimeout(e,t)}export function realClearTimeout(e){l().clearTimeout(e)}export function withRealClock(e){const t=globalThis.__buoyWithRealClock;return typeof t==="function"?t(e):e()}const s=["_resetPingTimeout","_hasPingExpired"];export function pinSocketTimers(e){const t=e?.io;if(!t||t.__buoyRealTimers)return;const i=o=>{if(!o)return;o.setTimeoutFn=realSetTimeout;o.clearTimeoutFn=realClearTimeout;for(const n of s){const r=o[n];if(typeof r!=="function"||r.__buoyReal)continue;const c=function(...a){return withRealClock(()=>r.apply(this,a))};c.__buoyReal=true;o[n]=c}};i(t);i(t.engine);const u=t.open;if(typeof u==="function"){t.open=function(...o){const n=u.apply(this,o);i(this?.engine);return n}}t.__buoyRealTimers=true}
@@ -1,34 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * Module-level desktop-sync status, written by useExternalSyncSocket and
5
- * read by the floating menu's settings modal (via useSyncExternalStore).
6
- * Lives here rather than in core because only the socket hook knows the
7
- * platform-rewritten URL and every connection transition — including for
8
- * apps that call the hook directly instead of using zero-config sync.
9
- */
10
-
11
- let status = {
12
- targetURL: null,
13
- state: "idle",
14
- lastError: null,
15
- since: 0
16
- };
17
- const listeners = new Set();
18
- export function setSyncStatus(patch) {
19
- status = {
20
- ...status,
21
- ...patch,
22
- since: Date.now()
23
- };
24
- listeners.forEach(listener => listener());
25
- }
26
- export function getSyncStatus() {
27
- return status;
28
- }
29
- export function subscribeSyncStatus(listener) {
30
- listeners.add(listener);
31
- return () => {
32
- listeners.delete(listener);
33
- };
34
- }
1
+ "use strict";import{realNow as s}from"./realNow";let e={targetURL:null,state:"idle",lastError:null,since:0};const n=new Set;export function setSyncStatus(t){e={...e,...t,since:s()};n.forEach(r=>r())}export function getSyncStatus(){return e}export function subscribeSyncStatus(t){n.add(t);return()=>{n.delete(t)}}
@@ -1,93 +1 @@
1
- "use strict";
2
-
3
- // ============================================================
4
- // DEVTOOL SYNC PROTOCOL (v1)
5
- //
6
- // Topology: device is a Socket.IO client; the desktop app hosts
7
- // the server (broker) and one or more dashboard UIs. The server
8
- // routes messages between dashboards and devices.
9
- //
10
- // Device -> server/dashboard events:
11
- // "devtool-capabilities" DeviceCapabilitiesMessage
12
- // "devtool-sync" DevToolSyncMessage
13
- // "devtool-action-result" DevToolActionResultMessage
14
- //
15
- // Dashboard/server -> device events:
16
- // "devtool-watch" DevToolWatchMessage
17
- // "request-devtool-state" DevToolRequestMessage
18
- // "devtool-action" DevToolActionMessage
19
- //
20
- // Backpressure contract: the device sends nothing for a tool until
21
- // it receives `devtool-watch {watching: true}` for it. The SERVER is
22
- // responsible for consolidating watch state across dashboards (and
23
- // for sending `watching: false` when the last watching dashboard
24
- // closes a panel or disconnects). The device just obeys.
25
- // ============================================================
26
-
27
- /** Version of the envelope/event contract itself (not tool payloads). */
28
- export const SYNC_PROTOCOL_VERSION = 1;
29
-
30
- /** What a sync payload represents. "events"/"diff" reserved for future use. */
31
-
32
- // ============================================================
33
- // TOOL ADAPTER
34
- // ============================================================
35
-
36
- /** A remote-invocable action. Thrown errors are reported back to the dashboard. */
37
-
38
- /**
39
- * The contract each dev tool implements to become remotely syncable.
40
- * Works with BaseEventStore or any pattern exposing subscribe + getSnapshot.
41
- *
42
- * Payloads (snapshots and action results) must be JSON-serializable —
43
- * no functions, circular references, or class instances.
44
- */
45
-
46
- // ============================================================
47
- // DEVICE -> DASHBOARD MESSAGES
48
- // ============================================================
49
-
50
- /** Announces available tools + actions on connect and when tools change. */
51
-
52
- /** Carries a tool's state to the dashboard. */
53
-
54
- /** Response to a `devtool-action` request. */
55
-
56
- // ============================================================
57
- // DASHBOARD -> DEVICE MESSAGES
58
- // ============================================================
59
-
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
- */
74
-
75
- /** Explicit request for a tool's current state (works even while unwatched). */
76
-
77
- /** Invokes a registered action on the device. */
78
-
79
- // ============================================================
80
- // HOOK PROPS
81
- // ============================================================
82
-
83
- // ============================================================
84
- // PLATFORM
85
- // ============================================================
86
-
87
- /**
88
- * Valid platform operating systems that can be used with React Native
89
- */
90
-
91
- // ============================================================
92
- // SOCKET HOOK PROPS
93
- // ============================================================
1
+ "use strict";export const SYNC_PROTOCOL_VERSION=1;