@buoy-gg/external-sync 7.0.41 → 7.0.43

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 +3 -6
  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,69 +1 @@
1
- "use strict";
2
-
3
- import { useAccountAccess } from "@buoy-gg/license";
4
- import { useEffect, useRef } from "react";
5
- import { attachDeviceSync } from "./attachDeviceSync";
6
- /**
7
- * Device-side sync bridge: syncs any number of dev tools to the desktop
8
- * dashboard and executes actions invoked remotely.
9
- *
10
- * Backpressure: a tool's adapter is only subscribed while a dashboard is
11
- * watching it (`devtool-watch`), so unwatched tools cost nothing. The server
12
- * re-sends current watch state whenever it receives `devtool-capabilities`,
13
- * which the device announces on every (re)connect.
14
- *
15
- * Usage:
16
- * ```ts
17
- * useExternalSync({
18
- * tools: {
19
- * storage: {
20
- * version: 1,
21
- * getSnapshot: () => storageEventStore.getEvents(),
22
- * subscribe: (cb) => storageEventStore.subscribeToEvents(cb),
23
- * actions: {
24
- * clearEvents: () => storageEventStore.clearEvents(),
25
- * },
26
- * },
27
- * },
28
- * socket,
29
- * deviceId: 'my-device-id',
30
- * });
31
- * ```
32
- */
33
- export function useExternalSync({
34
- tools,
35
- socket,
36
- deviceId,
37
- enabled = true,
38
- throttleMs = 200,
39
- enableLogs = false
40
- }) {
41
- const account = useAccountAccess();
42
- const admitted = account.status === "admitted";
43
- const toolsRef = useRef(tools);
44
- toolsRef.current = tools;
45
-
46
- // Watch state persists across effect re-runs (e.g. when tools are
47
- // added/removed) so an already-watched tool stays subscribed.
48
- const watchedRef = useRef(new Map());
49
-
50
- // Re-run the effect only when tools are added/removed, not on every
51
- // object-reference change.
52
- const toolIdsKey = Object.keys(tools).sort().join(",");
53
- useEffect(() => {
54
- if (!socket || !deviceId || !enabled || !admitted) {
55
- watchedRef.current.clear();
56
- return;
57
- }
58
- return attachDeviceSync({
59
- socket,
60
- deviceId,
61
- getTools: () => toolsRef.current,
62
- watched: watchedRef.current,
63
- throttleMs,
64
- enableLogs
65
- });
66
- // toolIdsKey stands in for `tools` (see comment above its definition).
67
- // eslint-disable-next-line react-hooks/exhaustive-deps
68
- }, [socket, deviceId, enabled, admitted, throttleMs, enableLogs, toolIdsKey]);
69
- }
1
+ "use strict";import{useAccountAccess as p}from"@buoy-gg/license";import{useEffect as y,useRef as a}from"react";import{attachDeviceSync as w}from"./attachDeviceSync";export function useExternalSync({tools:t,socket:r,deviceId:c,enabled:e=true,throttleMs:n=200,enableLogs:u=false}){const i=p();const o=i.status==="admitted";const s=a(t);s.current=t;const f=a(new Map);const m=Object.keys(t).sort().join(",");y(()=>{if(!r||!c||!e||!o){f.current.clear();return}return w({socket:r,deviceId:c,getTools:()=>s.current,watched:f.current,throttleMs:n,enableLogs:u})},[r,c,e,o,n,u,m])}
@@ -1,235 +1 @@
1
- "use strict";
2
-
3
- import { watchBrokerAccess } from "./brokerAccess";
4
- import { useEffect, useRef, useState } from "react";
5
- import { io as socketIO } from "socket.io-client";
6
- import { getPlatformSpecificURL } from "./utils/platformUtils";
7
- import { getDefaultSocketURL } from "./utils/getDefaultSocketURL";
8
- import { log } from "./utils/logger";
9
- import { setSyncStatus } from "./syncStatusStore";
10
- // True only in development. Guarded so it doesn't throw in environments
11
- // (web/electron bundles) where __DEV__ may be undefined.
12
- const isDev = typeof __DEV__ !== "undefined" ? __DEV__ : false;
13
-
14
- /**
15
- * Singleton socket instance that persists across component renders.
16
- * Multiple components share the same socket connection.
17
- */
18
- let globalSocketInstance = null;
19
- let currentSocketURL = "";
20
- let currentDeviceId = "";
21
- let currentPlatform = "";
22
-
23
- /**
24
- * Identifies a single socket INSTANCE. Sent in the handshake query and reused
25
- * for the life of the instance, so it survives socket.io reconnects but
26
- * changes whenever we create a brand-new socket (remount / Fast Refresh /
27
- * URL change). The dashboard uses this to tell a flapping connection that's
28
- * merely reconnecting (stable nonce) from an app that keeps recreating its
29
- * socket (changing nonce).
30
- */
31
- const makeConnectionNonce = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
32
-
33
- /**
34
- * Identifies this JS RUNTIME, not this socket. Held on globalThis so every
35
- * socket the runtime opens (Fast Refresh, remounts, a double-mounted dev
36
- * tools layer) carries the same value, while a second app that pinned the
37
- * same deviceId carries a different one. That difference is the only way
38
- * the broker can tell "one device, many sockets" from "many devices, one
39
- * id" — it flags the latter as a collision in the device list.
40
- */
41
- export function getInstanceId() {
42
- const g = globalThis;
43
- if (!g.__buoyInstanceId) g.__buoyInstanceId = makeConnectionNonce();
44
- return g.__buoyInstanceId;
45
- }
46
- /**
47
- * The socket.io handshake query — the ONE place the device says who it is.
48
- * `connectionNonce` is fresh per socket instance; `instanceId` is fixed per
49
- * runtime (see above). Pure, so the shape is unit-tested.
50
- */
51
- export function buildHandshakeQuery(identity) {
52
- const withoutAccountKey = values => values && Object.fromEntries(Object.entries(values).filter(([key]) => !["BUOY_LICENSE_KEY", "BUOY_KEY", "EXPO_PUBLIC_BUOY_KEY", "VITE_BUOY_KEY", "REACT_APP_BUOY_KEY", "licenseKey"].includes(key)));
53
- return {
54
- deviceName: identity.deviceName,
55
- deviceId: identity.persistentDeviceId,
56
- platform: identity.platform,
57
- extraDeviceInfo: JSON.stringify(withoutAccountKey(identity.extraDeviceInfo)),
58
- envVariables: JSON.stringify(withoutAccountKey(identity.envVariables)),
59
- connectionNonce: makeConnectionNonce(),
60
- instanceId: getInstanceId()
61
- };
62
- }
63
-
64
- /**
65
- * Hook that manages a Socket.IO connection for device-to-dashboard communication.
66
- *
67
- * Features:
68
- * - Singleton pattern for socket connection
69
- * - Platform-specific URL handling (Android emulator -> 10.0.2.2)
70
- * - Connection state tracking
71
- */
72
- export function useExternalSyncSocket({
73
- deviceName,
74
- socketURL,
75
- persistentDeviceId,
76
- extraDeviceInfo,
77
- envVariables,
78
- platform,
79
- enableLogs = false
80
- }) {
81
- const socketRef = useRef(null);
82
- const [socket, setSocket] = useState(null);
83
- const [isConnected, setIsConnected] = useState(false);
84
- // Derived once per render; stable string, so effect deps stay quiet.
85
- const resolvedSocketURL = socketURL ?? getDefaultSocketURL();
86
- // Tracks whether we've already logged a connection error for the current
87
- // outage, so the reconnection backoff doesn't spam an error on every retry.
88
- const hasLoggedConnectError = useRef(false);
89
- const logPrefix = `[${deviceName}]`;
90
- const onConnect = () => {
91
- log(`${logPrefix} Socket connected successfully`, enableLogs);
92
- hasLoggedConnectError.current = false;
93
- setIsConnected(true);
94
- setSyncStatus({
95
- state: "connected",
96
- lastError: null
97
- });
98
- };
99
- const onDisconnect = reason => {
100
- log(`${logPrefix} Socket disconnected. Reason: ${reason}`, enableLogs);
101
- setIsConnected(false);
102
- setSyncStatus({
103
- state: "retrying"
104
- });
105
- };
106
- const onConnectError = error => {
107
- setSyncStatus({
108
- state: "retrying",
109
- lastError: error.message
110
- });
111
- // Never surface this in production builds.
112
- if (!isDev) return;
113
- // Reconnection retries indefinitely; only log the first failure of an
114
- // outage to avoid flooding the console until the server comes back up.
115
- if (hasLoggedConnectError.current) return;
116
- hasLoggedConnectError.current = true;
117
- // A localhost target on an Android device means a USB (adb reverse)
118
- // setup — name the exact command instead of the generic hint.
119
- const hint = platform === "android" && /localhost|127\.0\.0\.1/.test(currentSocketURL) ? `If this device is connected over USB, run: adb reverse tcp:42831 tcp:42831. ` : `If it's running on another machine or port, pass socketURL via the externalSync prop. `;
120
- log(`[buoy] desktop sync: can't reach ${currentSocketURL} (is Buoy Desktop running?). ` + `Retrying in the background. ${hint}(${error.message})`, enableLogs, "warn");
121
- };
122
- const onConnectTimeout = () => {
123
- log(`${logPrefix} Socket connection timeout`, enableLogs, "error");
124
- };
125
-
126
- // Reuse the transport, but reattach listeners after every effect setup
127
- // (including React Strict Mode replay and Fast Refresh).
128
- useEffect(() => {
129
- if (!persistentDeviceId) return;
130
- const platformUrl = getPlatformSpecificURL(resolvedSocketURL, platform);
131
- setSyncStatus({
132
- targetURL: platformUrl,
133
- state: "connecting",
134
- lastError: null
135
- });
136
- log(`${logPrefix} Platform: ${platform}, using URL: ${platformUrl}`, enableLogs);
137
- try {
138
- if (globalSocketInstance && (currentSocketURL !== platformUrl || currentDeviceId !== persistentDeviceId || currentPlatform !== platform)) {
139
- globalSocketInstance.disconnect();
140
- globalSocketInstance = null;
141
- }
142
- if (!globalSocketInstance) {
143
- log(`${logPrefix} Creating new socket instance to ${platformUrl}`, enableLogs);
144
- globalSocketInstance = socketIO(platformUrl, {
145
- autoConnect: true,
146
- query: buildHandshakeQuery({
147
- deviceName,
148
- persistentDeviceId,
149
- platform,
150
- extraDeviceInfo,
151
- envVariables
152
- }),
153
- reconnection: true,
154
- reconnectionAttempts: Infinity,
155
- reconnectionDelay: 1000,
156
- reconnectionDelayMax: 5000,
157
- transports: ["websocket"]
158
- });
159
- currentSocketURL = platformUrl;
160
- currentDeviceId = persistentDeviceId;
161
- currentPlatform = platform;
162
- } else {
163
- log(`${logPrefix} Reusing existing socket instance to ${platformUrl}`, enableLogs);
164
- }
165
- socketRef.current = globalSocketInstance;
166
- const activeSocket = socketRef.current;
167
- setSocket(activeSocket);
168
- activeSocket.on("connect_error", onConnectError);
169
- activeSocket.on("connect_timeout", onConnectTimeout);
170
- setIsConnected(activeSocket.connected);
171
- if (activeSocket.connected) {
172
- log(`${logPrefix} Socket already connected on init`, enableLogs);
173
- }
174
- activeSocket.on("connect", onConnect);
175
- activeSocket.on("disconnect", onDisconnect);
176
- return () => {
177
- log(`${logPrefix} Cleaning up socket event listeners`, enableLogs);
178
- activeSocket.off("connect", onConnect);
179
- activeSocket.off("disconnect", onDisconnect);
180
- activeSocket.off("connect_error", onConnectError);
181
- activeSocket.off("connect_timeout", onConnectTimeout);
182
- };
183
- } catch (error) {
184
- log(`${logPrefix} Failed to initialize socket: ${error}`, enableLogs, "error");
185
- }
186
- }, [persistentDeviceId, resolvedSocketURL, platform]);
187
-
188
- // Update socket query parameters when deviceName changes
189
- useEffect(() => {
190
- if (socketRef.current && socketRef.current.io.opts.query && persistentDeviceId) {
191
- log(`${logPrefix} Updating device name in socket connection`, enableLogs);
192
- socketRef.current.io.opts.query = {
193
- ...socketRef.current.io.opts.query,
194
- deviceName,
195
- deviceId: persistentDeviceId,
196
- platform
197
- };
198
- }
199
- }, [deviceName, logPrefix, persistentDeviceId, platform, enableLogs]);
200
- function connect() {
201
- if (socketRef.current && !socketRef.current.connected) {
202
- log(`${logPrefix} Manually connecting to socket server`, enableLogs);
203
- socketRef.current.connect();
204
- }
205
- }
206
- function disconnect() {
207
- if (socketRef.current && socketRef.current.connected) {
208
- log(`${logPrefix} Manually disconnecting from socket server`, enableLogs);
209
- socketRef.current.disconnect();
210
- }
211
- }
212
-
213
- // A connected transport is not permission to sync. Legacy desktop brokers
214
- // never send this announcement, so updated SDKs withhold tools from them.
215
- const [admittedSocket, setAdmittedSocket] = useState(null);
216
- useEffect(() => {
217
- if (!socket) return;
218
- return watchBrokerAccess(socket, ready => {
219
- setAdmittedSocket(ready ? socket : null);
220
- setSyncStatus(ready ? {
221
- state: "connected",
222
- lastError: null
223
- } : {
224
- state: "retrying",
225
- lastError: "Update Buoy Desktop and sign in to enable desktop sync."
226
- });
227
- });
228
- }, [socket]);
229
- return {
230
- socket: admittedSocket === socket ? socket : null,
231
- connect,
232
- disconnect,
233
- isConnected: isConnected && admittedSocket === socket && socket !== null
234
- };
235
- }
1
+ "use strict";import{watchBrokerAccess as w}from"./brokerAccess";import{useEffect as $,useRef as O,useState as g}from"react";import{io as M}from"socket.io-client";import{getPlatformSpecificURL as N}from"./utils/platformUtils";import{getDefaultSocketURL as V}from"./utils/getDefaultSocketURL";import{log as r}from"./utils/logger";import{setSyncStatus as f}from"./syncStatusStore";import{pinSocketTimers as q,realClearTimeout as j,realSetTimeout as J}from"./realNow";const b=typeof __DEV__!=="undefined"?__DEV__:false;let l=null;let m="";let v="";let x="";let _=0;let d=null;const z=2e3;const B=()=>`${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`;export function getInstanceId(){const o=globalThis;if(!o.__buoyInstanceId)o.__buoyInstanceId=B();return o.__buoyInstanceId}export function buildHandshakeQuery(o){const k=i=>i&&Object.fromEntries(Object.entries(i).filter(([S])=>!["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY","licenseKey"].includes(S)));return{deviceName:o.deviceName,deviceId:o.persistentDeviceId,platform:o.platform,extraDeviceInfo:JSON.stringify(k(o.extraDeviceInfo)),envVariables:JSON.stringify(k(o.envVariables)),connectionNonce:B(),instanceId:getInstanceId()}}export function useExternalSyncSocket({deviceName:o,socketURL:k,persistentDeviceId:i,extraDeviceInfo:S,envVariables:Y,platform:u,enableLogs:n=false}){const s=O(null);const[a,A]=g(null);const[P,y]=g(false);const p=k??V();const E=O(false);const c=`[${o}]`;const U=()=>{r(`${c} Socket connected successfully`,n);E.current=false;y(true);f({state:"connected",lastError:null})};const I=e=>{r(`${c} Socket disconnected. Reason: ${e}`,n);y(false);f({state:"retrying"})};const C=e=>{f({state:"retrying",lastError:e.message});if(!b)return;if(E.current)return;E.current=true;const t=u==="android"&&/localhost|127\.0\.0\.1/.test(m)?`If this device is connected over USB, run: adb reverse tcp:42831 tcp:42831. `:`If it's running on another machine or port, pass socketURL via the externalSync prop. `;r(`[buoy] desktop sync: can't reach ${m} (is Buoy Desktop running?). Retrying in the background. ${t}(${e.message})`,n,"warn")};const h=()=>{r(`${c} Socket connection timeout`,n,"error")};$(()=>{if(!i)return;const e=N(p,u);f({targetURL:e,state:"connecting",lastError:null});r(`${c} Platform: ${u}, using URL: ${e}`,n);try{if(l&&(m!==e||v!==i||x!==u)){l.disconnect();l=null}if(!l){r(`${c} Creating new socket instance to ${e}`,n);l=M(e,{autoConnect:true,query:buildHandshakeQuery({deviceName:o,persistentDeviceId:i,platform:u,extraDeviceInfo:S,envVariables:Y}),reconnection:true,reconnectionAttempts:Infinity,reconnectionDelay:1e3,reconnectionDelayMax:5e3,transports:["websocket"]});q(l);m=e;v=i;x=u}else{r(`${c} Reusing existing socket instance to ${e}`,n)}s.current=l;const t=s.current;_++;if(d){j(d);d=null}A(t);t.on("connect_error",C);t.on("connect_timeout",h);y(t.connected);if(t.connected){r(`${c} Socket already connected on init`,n)}t.on("connect",U);t.on("disconnect",I);return()=>{r(`${c} Cleaning up socket event listeners`,n);t.off("connect",U);t.off("disconnect",I);t.off("connect_error",C);t.off("connect_timeout",h);_--;if(_===0){d=J(()=>{d=null;if(_>0||l!==t)return;r(`${c} No longer used; disconnecting`,n);t.disconnect();l=null;f({state:"idle",lastError:null})},z)}}}catch(t){r(`${c} Failed to initialize socket: ${t}`,n,"error")}},[i,p,u]);$(()=>{if(s.current&&s.current.io.opts.query&&i){r(`${c} Updating device name in socket connection`,n);s.current.io.opts.query={...s.current.io.opts.query,deviceName:o,deviceId:i,platform:u}}},[o,c,i,u,n]);function T(){if(s.current&&!s.current.connected){r(`${c} Manually connecting to socket server`,n);s.current.connect()}}function D(){if(s.current&&s.current.connected){r(`${c} Manually disconnecting from socket server`,n);s.current.disconnect()}}const[R,K]=g(null);$(()=>{if(!a)return;return w(a,e=>{K(e?a:null);f(e?{state:"connected",lastError:null}:{state:"retrying",lastError:"Update Buoy Desktop and sign in to enable desktop sync."})})},[a]);return{socket:R===a?a:null,connect:T,disconnect:D,isConnected:P&&R===a&&a!==null}}
@@ -1,76 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * Default broker-URL derivation. Kept in its own module (and never imported
5
- * from types.ts) because the deep react-native require below must not reach
6
- * non-Metro bundles — the desktop app imports this package's types only.
7
- */
8
-
9
- /** Port Buoy Desktop's broker listens on. */
10
- export const DEFAULT_BROKER_PORT = 42831;
11
- const HOST_RE = /^https?:\/\/([^:/]+)/;
12
-
13
- /**
14
- * Host out of a bundle's scriptURL, or null when there isn't one.
15
- *
16
- * Release bundles are loaded from disk (`file:///.../main.jsbundle`), so this
17
- * returns null and callers fall back to localhost — correct for simulators,
18
- * emulators, and Android over USB (`adb reverse tcp:42831 tcp:42831`). A
19
- * physical iOS device in a release build has no derivable host at all and
20
- * must be given an explicit socketURL.
21
- */
22
- export function hostFromScriptURL(scriptURL) {
23
- return scriptURL?.match(HOST_RE)?.[1] ?? null;
24
- }
25
-
26
- /** Broker URL for a host, falling back to localhost when none is known. */
27
- export function socketURLForHost(host) {
28
- return `http://${host ?? "localhost"}:${DEFAULT_BROKER_PORT}`;
29
- }
30
-
31
- /**
32
- * Host of the Metro dev server that served this bundle — the machine most
33
- * likely running Buoy Desktop. On a physical device this is the computer's
34
- * LAN IP, where "localhost" would be the phone itself.
35
- *
36
- * scriptURL is the only source that works everywhere we've tested:
37
- * NativeModules.SourceCode.scriptURL is undefined on bridgeless, and
38
- * expo-constants hostUri/experienceUrl/linkingUri are all undefined in
39
- * Expo dev-client builds. Returns null on web or if RN internals move.
40
- */
41
- export function getMetroHost() {
42
- try {
43
- // The public route to the SourceCode TurboModule that
44
- // react-native/Libraries/NativeModules/specs/NativeSourceCode wraps. Deep
45
- // imports warn on RN 0.80+; react-native-web has no registry and falls
46
- // through to null.
47
- // eslint-disable-next-line @typescript-eslint/no-var-requires
48
- const {
49
- TurboModuleRegistry
50
- } = require("react-native");
51
- const NativeSourceCode = TurboModuleRegistry?.get?.("SourceCode");
52
- const host = hostFromScriptURL(NativeSourceCode?.getConstants?.().scriptURL);
53
- if (host) return host;
54
- } catch {
55
- // web / RN internals moved — try the old-architecture module
56
- }
57
- try {
58
- const {
59
- NativeModules
60
- } = require("react-native");
61
- const host = hostFromScriptURL(NativeModules?.SourceCode?.getConstants?.().scriptURL ?? NativeModules?.SourceCode?.scriptURL);
62
- if (host) return host;
63
- } catch {
64
- // not React Native
65
- }
66
- return null;
67
- }
68
-
69
- /**
70
- * Default Buoy Desktop broker URL: the Metro host (falling back to
71
- * localhost) on port 42831. Android-emulator 10.0.2.2 rewriting is applied
72
- * later by getPlatformSpecificURL, so localhost still works there.
73
- */
74
- export function getDefaultSocketURL() {
75
- return socketURLForHost(getMetroHost());
76
- }
1
+ "use strict";export const DEFAULT_BROKER_PORT=42831;const r=/^https?:\/\/([^:/]+)/;export function hostFromScriptURL(t){return t?.match(r)?.[1]??null}export function socketURLForHost(t){return`http://${t??"localhost"}:${DEFAULT_BROKER_PORT}`}export function getMetroHost(){try{const{TurboModuleRegistry:t}=require("react-native");const e=t?.get?.("SourceCode");const o=hostFromScriptURL(e?.getConstants?.().scriptURL);if(o)return o}catch{}try{const{NativeModules:t}=require("react-native");const e=hostFromScriptURL(t?.SourceCode?.getConstants?.().scriptURL??t?.SourceCode?.scriptURL);if(e)return e}catch{}return null}export function getDefaultSocketURL(){return socketURLForHost(getMetroHost())}
@@ -1,19 +1 @@
1
- "use strict";
2
-
3
- export const DEFAULT_BROKER_PORT = 42831;
4
- export function hostFromScriptURL(url) {
5
- try {
6
- return url && /^https?:/.test(url) ? new URL(url).hostname : null;
7
- } catch {
8
- return null;
9
- }
10
- }
11
- export function socketURLForHost(host) {
12
- return `http://${host ?? "localhost"}:${DEFAULT_BROKER_PORT}`;
13
- }
14
- export function getMetroHost() {
15
- return null;
16
- }
17
- export function getDefaultSocketURL() {
18
- return socketURLForHost(typeof location === "undefined" ? null : location.hostname);
19
- }
1
+ "use strict";export const DEFAULT_BROKER_PORT=42831;export function hostFromScriptURL(t){try{return t&&/^https?:/.test(t)?new URL(t).hostname:null}catch{return null}}export function socketURLForHost(t){return`http://${t??"localhost"}:${DEFAULT_BROKER_PORT}`}export function getMetroHost(){return null}export function getDefaultSocketURL(){return socketURLForHost(typeof location==="undefined"?null:location.hostname)}
@@ -1,20 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * Helper function for controlled logging.
5
- * Only shows logs when enableLogs is true.
6
- * Always shows warnings and errors regardless of enableLogs setting.
7
- */
8
- export function log(message, enableLogs, type = "log") {
9
- if (!enableLogs && type === "log") return;
10
- switch (type) {
11
- case "warn":
12
- console.warn(message);
13
- break;
14
- case "error":
15
- console.error(message);
16
- break;
17
- default:
18
- console.log(message);
19
- }
20
- }
1
+ "use strict";export function log(o,c,r="log"){if(!c&&r==="log")return;switch(r){case"warn":console.warn(o);break;case"error":console.error(o);break;default:console.log(o)}}
@@ -1,45 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * True only on an actual Android EMULATOR, detected via the build fingerprint
5
- * (same heuristic react-native-device-info uses), with no native dependency.
6
- * Physical devices must NOT get the 10.0.2.2 rewrite: over USB (`adb reverse`)
7
- * the Metro-derived host is "localhost", and rewriting it to the
8
- * emulator-only 10.0.2.2 alias breaks the connection — keeping localhost lets
9
- * `adb reverse tcp:42831 tcp:42831` forward it to the dev machine.
10
- *
11
- * The react-native require stays lexically inside try{} (Metro optional-dep
12
- * rule) and this module is never imported from types.ts, so non-Metro
13
- * bundles (the desktop app) are unaffected.
14
- */
15
- const isAndroidEmulator = () => {
16
- try {
17
- const {
18
- Platform
19
- } = require("react-native");
20
- if (Platform.OS !== "android") return false;
21
- const fingerprint = Platform.constants?.Fingerprint ?? "";
22
- return /generic|emulator|goldfish|ranchu|sdk_gphone|vbox/i.test(fingerprint);
23
- } catch {
24
- return false;
25
- }
26
- };
27
-
28
- /**
29
- * Get platform-specific URL for socket connection.
30
- * On the Android emulator (only), replaces localhost with 10.0.2.2 —
31
- * the emulator's alias for the host machine's loopback.
32
- */
33
- export const getPlatformSpecificURL = (baseUrl, platform) => {
34
- try {
35
- const url = new URL(baseUrl);
36
- if (platform === "android" && (url.hostname === "localhost" || url.hostname === "127.0.0.1") && isAndroidEmulator()) {
37
- url.hostname = "10.0.2.2";
38
- return url.toString();
39
- }
40
- return baseUrl;
41
- } catch (e) {
42
- console.warn("Error getting platform-specific URL:", e);
43
- return baseUrl;
44
- }
45
- };
1
+ "use strict";const e=()=>{try{const{Platform:t}=require("react-native");if(t.OS!=="android")return false;const n=t.constants?.Fingerprint??"";return/generic|emulator|goldfish|ranchu|sdk_gphone|vbox/i.test(n)}catch{return false}};export const getPlatformSpecificURL=(t,n)=>{try{const r=new URL(t);if(n==="android"&&(r.hostname==="localhost"||r.hostname==="127.0.0.1")&&e()){r.hostname="10.0.2.2";return r.toString()}return t}catch(r){console.warn("Error getting platform-specific URL:",r);return t}};
@@ -1,5 +1 @@
1
- "use strict";
2
-
3
- export function getPlatformSpecificURL(baseUrl, _platform) {
4
- return baseUrl;
5
- }
1
+ "use strict";export function getPlatformSpecificURL(t,r){return t}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The device's real clock.
3
+ *
4
+ * @buoy-gg/clock can override the APP's clock (Date.now, new Date). Buoy's
5
+ * own bookkeeping must keep real time regardless: license expiry, sync
6
+ * throttles and request deadlines would otherwise lock tools or stall when
7
+ * someone travels a month ahead. The clock package publishes the real clock
8
+ * on `globalThis.__buoyRealNow`; without it, Date.now is already real.
9
+ */
10
+ export declare function realNow(): number;
11
+ /**
12
+ * setTimeout that a clock jump never fires early. @buoy-gg/clock tracks the
13
+ * app's timers so a jump ahead can run the ones that come due; Buoy's own
14
+ * deadlines and heartbeats must not be among them.
15
+ */
16
+ export declare function realSetTimeout(callback: () => void, ms: number): ReturnType<typeof setTimeout>;
17
+ export declare function realClearTimeout(handle: unknown): void;
18
+ /** Run `fn` on the real clock (a no-op scope when @buoy-gg/clock is absent). */
19
+ export declare function withRealClock<T>(fn: () => T): T;
20
+ /**
21
+ * Keep a socket.io socket on real time: its Manager and engine get real
22
+ * timers (engine.io's heartbeat is a ping-timeout setTimeout a jump would
23
+ * fire) and the engine's clock-reading methods run in a real-clock scope. The
24
+ * engine is rebuilt on every reconnect, so `open` re-pins it.
25
+ */
26
+ export declare function pinSocketTimers(socket: unknown): void;
27
+ //# sourceMappingURL=realNow.d.ts.map
@@ -1,10 +1,3 @@
1
- /**
2
- * Module-level desktop-sync status, written by useExternalSyncSocket and
3
- * read by the floating menu's settings modal (via useSyncExternalStore).
4
- * Lives here rather than in core because only the socket hook knows the
5
- * platform-rewritten URL and every connection transition — including for
6
- * apps that call the hook directly instead of using zero-config sync.
7
- */
8
1
  export interface SyncStatus {
9
2
  /** Final (platform-rewritten) broker URL being dialed; null before init. */
10
3
  targetURL: string | null;
package/lib/web/index.mjs CHANGED
@@ -1,2 +1 @@
1
- "use client";
2
- var S=process.env.NODE_ENV!=="production";typeof window<"u"&&typeof globalThis.global>"u"&&(globalThis.global=globalThis);import{useAccountAccess as me}from"@buoy-gg/license/web";import{useEffect as ve,useRef as ae}from"react";import{LicenseManager as I}from"@buoy-gg/license/web";function l(t,c,n="log"){if(!(!c&&n==="log"))switch(n){case"warn":console.warn(t);break;case"error":console.error(t);break;default:console.log(t)}}var q="__buoyFlushToolSync";function K(){let t=globalThis;return t[q]||(t[q]=new Set),t[q]}function V(t){K().add(t)}function W(t){K().delete(t)}function he(t){try{for(let c of K())try{c(t)}catch{}}catch{}}function j(t,c){let n=0,i=[t],h=new Set;for(;i.length>0;){if(n>c)return{over:!0,approxBytes:n};let o=i.pop();if(o==null)n+=4;else if(typeof o=="string")n+=o.length+2;else if(typeof o=="number")n+=8;else if(typeof o=="boolean")n+=5;else if(typeof o=="object"){if(h.has(o))continue;if(h.add(o),Array.isArray(o)){n+=2+o.length;for(let s=0;s<o.length;s++)i.push(o[s])}else{n+=2;for(let s in o)n+=s.length+4,i.push(o[s])}}else n+=4}return{over:n>c,approxBytes:n}}var re=1;var P="[ExternalSync]";function ie({socket:t,deviceId:c,getTools:n,watched:i,throttleMs:h=200,enableLogs:o=!1}){let s=!1,a=!1,p=0,g=()=>!s&&a&&t.connected&&I.hasAccountAccess(),m=new Map,k={},v={},w=e=>e==="All"||e===c,u=(e,r)=>{if(g())try{t.emit(e,r)}catch(y){l(`${P} Failed to emit "${e}": ${y}. Payload must be JSON-serializable.`,o,"error")}},M=new Set,x=e=>{if(!g())return;let r=n()[e];if(!r)return;k[e]=Date.now();let y;try{y=r.getSnapshot()}catch{return}let{over:T,approxBytes:$}=j(y,2097152);if(T){M.has(e)||(M.add(e),console.warn(`${P} "${e}" snapshot \u2248${($/1e6).toFixed(1)}MB exceeds the ${2097152/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}let Y={toolId:e,persistentDeviceId:c,timestamp:k[e],version:r.version,kind:"snapshot",payload:y};u("devtool-sync",Y)},A=e=>{if(v[e])return;let r=Date.now()-(k[e]??0);if(r>=h){x(e);return}v[e]=setTimeout(()=>{delete v[e],x(e)},h-r)},O=e=>{t.connected&&m.has(e)&&(v[e]&&(clearTimeout(v[e]),delete v[e]),x(e))};V(O);let D=new Map,C=(e,r)=>{if(!g())return;let y=n()[e];if(y){if(m.has(e)){if(D.get(e)===r)return;let T=m.get(e);m.set(e,y.subscribe(r==="stream"?()=>A(e):()=>{})),D.set(e,r),T?.(),l(`${P} "${e}" watch mode \u2192 ${r}`,o);return}m.set(e,y.subscribe(r==="stream"?()=>A(e):()=>{})),D.set(e,r),l(`${P} Watching "${e}" (${r})`,o)}},E=e=>{m.get(e)?.(),m.delete(e),D.delete(e),v[e]&&(clearTimeout(v[e]),delete v[e]),l(`${P} Stopped watching "${e}"`,o)},N=()=>{let e={persistentDeviceId:c,protocolVersion:1,tools:Object.entries(n()).map(([r,y])=>({toolId:r,version:y.version,actions:Object.keys(y.actions??{})}))};u("devtool-capabilities",e)},f=e=>{if(!(!g()||!w(e.targetDeviceId)))if(e.watching){let r=e.consolidated===!0&&e.stream===!1?"capture":"stream";i.set(e.toolId,r),C(e.toolId,r),r==="stream"&&x(e.toolId)}else i.delete(e.toolId),E(e.toolId)},d=e=>{!g()||!w(e.targetDeviceId)||x(e.toolId)},Q=async e=>{if(!g()||!w(e.targetDeviceId))return;let r=p,{requestId:y,toolId:T,action:$,params:Y}=e,b={requestId:y,toolId:T,action:$,persistentDeviceId:c,timestamp:Date.now(),ok:!1},ne=n()[T],oe=ne?.actions?.[$];if(!oe){b.error=ne?`Unknown action "${$}" on tool "${T}"`:`Unknown tool "${T}"`,u("devtool-action-result",b);return}try{b.result=await oe(Y),b.ok=!0}catch(U){b.error=U instanceof Error?U.message:String(U)}if(!(r!==p||!g())){if(b.ok){let{over:U,approxBytes:Se}=j(b.result,8388608);U&&(b.ok=!1,b.result=void 0,b.error=`Result \u2248${(Se/1e6).toFixed(1)}MB exceeds the ${8388608/1e6}MB sync budget. Narrow the request (e.g. includeValues:false, a limit, or a more specific action).`)}b.timestamp=Date.now(),u("devtool-action-result",b)}},B=()=>{p+=1;for(let r of[...m.keys()])E(r);i.clear();for(let r of Object.values(v))clearTimeout(r);let e=t;e.sendBuffer&&(e.sendBuffer=e.sendBuffer.filter(r=>!["devtool-sync","devtool-capabilities","devtool-action-result","device-license"].includes(String(r.data?.[0]))))},Z=()=>{let e=I.getState();t.emit("device-license",{tier:e.tier,isPro:e.isPro?"1":"0"}),N()},ee=e=>{a=e?.protocol===1&&e.ready===!0,g()?Z():B()},ye=I.subscribe(()=>{I.hasAccountAccess()?g()&&Z():(t.connected&&t.emit("device-license",{tier:"locked",isPro:"0"}),B())}),L=()=>{a=!1,t.emit("buoy:access:request")},te=()=>{a=!1,B()};t.on("buoy:access",ee);for(let[e,r]of i)n()[e]&&C(e,r);return t.on("connect",L),t.on("disconnect",te),t.on("devtool-watch",f),t.on("request-devtool-state",d),t.on("devtool-action",Q),t.connected&&L(),()=>{s=!0,ye(),t.off("buoy:access",ee),B(),W(O),t.off("connect",L),t.off("disconnect",te),t.off("devtool-watch",f),t.off("request-devtool-state",d),t.off("devtool-action",Q);for(let e of[...m.keys()])E(e)}}function be({tools:t,socket:c,deviceId:n,enabled:i=!0,throttleMs:h=200,enableLogs:o=!1}){let a=me().status==="admitted",p=ae(t);p.current=t;let g=ae(new Map),m=Object.keys(t).sort().join(",");ve(()=>{if(!c||!n||!i||!a){g.current.clear();return}return ie({socket:c,deviceId:n,getTools:()=>p.current,watched:g.current,throttleMs:h,enableLogs:o})},[c,n,i,a,h,o,m])}function le(t,c){let n=()=>c(!1),i=()=>{n(),t.emit("buoy:access:request")},h=o=>{let s=o;c(s?.protocol===1&&s?.ready===!0)};return t.on("connect",i),t.on("disconnect",n),t.on("buoy:access",h),n(),t.connected&&i(),()=>{t.off("connect",i),t.off("disconnect",n),t.off("buoy:access",h)}}import{useEffect as G,useRef as fe,useState as J}from"react";import{io as De}from"socket.io-client";function ue(t,c){return t}var xe=42831;function Te(t){return`http://${t??"localhost"}:42831`}function ke(){return null}function H(){return Te(typeof location>"u"?null:location.hostname)}var z={targetURL:null,state:"idle",lastError:null,since:0},X=new Set;function R(t){z={...z,...t,since:Date.now()},X.forEach(c=>c())}function we(){return z}function Re(t){return X.add(t),()=>{X.delete(t)}}var Ee=typeof S<"u"?S:!1,_=null,F="",de="",pe="",ge=()=>`${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`;function _e(){let t=globalThis;return t.__buoyInstanceId||(t.__buoyInstanceId=ge()),t.__buoyInstanceId}function Me(t){let c=n=>n&&Object.fromEntries(Object.entries(n).filter(([i])=>!["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY","licenseKey"].includes(i)));return{deviceName:t.deviceName,deviceId:t.persistentDeviceId,platform:t.platform,extraDeviceInfo:JSON.stringify(c(t.extraDeviceInfo)),envVariables:JSON.stringify(c(t.envVariables)),connectionNonce:ge(),instanceId:_e()}}function Ae({deviceName:t,socketURL:c,persistentDeviceId:n,extraDeviceInfo:i,envVariables:h,platform:o,enableLogs:s=!1}){let a=fe(null),[p,g]=J(null),[m,k]=J(!1),v=c??H(),w=fe(!1),u=`[${t}]`,M=()=>{l(`${u} Socket connected successfully`,s),w.current=!1,k(!0),R({state:"connected",lastError:null})},x=f=>{l(`${u} Socket disconnected. Reason: ${f}`,s),k(!1),R({state:"retrying"})},A=f=>{if(R({state:"retrying",lastError:f.message}),!Ee||w.current)return;w.current=!0;let d=o==="android"&&/localhost|127\.0\.0\.1/.test(F)?"If this device is connected over USB, run: adb reverse tcp:42831 tcp:42831. ":"If it's running on another machine or port, pass socketURL via the externalSync prop. ";l(`[buoy] desktop sync: can't reach ${F} (is Buoy Desktop running?). Retrying in the background. ${d}(${f.message})`,s,"warn")},O=()=>{l(`${u} Socket connection timeout`,s,"error")};G(()=>{if(!n)return;let f=ue(v,o);R({targetURL:f,state:"connecting",lastError:null}),l(`${u} Platform: ${o}, using URL: ${f}`,s);try{_&&(F!==f||de!==n||pe!==o)&&(_.disconnect(),_=null),_?l(`${u} Reusing existing socket instance to ${f}`,s):(l(`${u} Creating new socket instance to ${f}`,s),_=De(f,{autoConnect:!0,query:Me({deviceName:t,persistentDeviceId:n,platform:o,extraDeviceInfo:i,envVariables:h}),reconnection:!0,reconnectionAttempts:1/0,reconnectionDelay:1e3,reconnectionDelayMax:5e3,transports:["websocket"]}),F=f,de=n,pe=o),a.current=_;let d=a.current;return g(d),d.on("connect_error",A),d.on("connect_timeout",O),k(d.connected),d.connected&&l(`${u} Socket already connected on init`,s),d.on("connect",M),d.on("disconnect",x),()=>{l(`${u} Cleaning up socket event listeners`,s),d.off("connect",M),d.off("disconnect",x),d.off("connect_error",A),d.off("connect_timeout",O)}}catch(d){l(`${u} Failed to initialize socket: ${d}`,s,"error")}},[n,v,o]),G(()=>{a.current&&a.current.io.opts.query&&n&&(l(`${u} Updating device name in socket connection`,s),a.current.io.opts.query={...a.current.io.opts.query,deviceName:t,deviceId:n,platform:o})},[t,u,n,o,s]);function D(){a.current&&!a.current.connected&&(l(`${u} Manually connecting to socket server`,s),a.current.connect())}function C(){a.current&&a.current.connected&&(l(`${u} Manually disconnecting from socket server`,s),a.current.disconnect())}let[E,N]=J(null);return G(()=>{if(p)return le(p,f=>{N(f?p:null),R(f?{state:"connected",lastError:null}:{state:"retrying",lastError:"Update Buoy Desktop and sign in to enable desktop sync."})})},[p]),{socket:E===p?p:null,connect:D,disconnect:C,isConnected:m&&E===p&&p!==null}}export{xe as DEFAULT_BROKER_PORT,re as SYNC_PROTOCOL_VERSION,he as flushToolSyncNow,H as getDefaultSocketURL,ke as getMetroHost,we as getSyncStatus,V as registerToolFlusher,R as setSyncStatus,Re as subscribeSyncStatus,W as unregisterToolFlusher,be as useExternalSync,Ae as useExternalSyncSocket};
1
+ "use client";var ne=process.env.NODE_ENV!=="production";typeof window<"u"&&typeof globalThis.global>"u"&&(globalThis.global=globalThis);import{useAccountAccess as he}from"@buoy-gg/license/web";import{useEffect as ve,useRef as oe}from"react";import{LicenseManager as P}from"@buoy-gg/license/web";function p(e,o,n="log"){if(!(!o&&n==="log"))switch(n){case"warn":console.warn(e);break;case"error":console.error(e);break;default:console.log(e)}}var j="__buoyFlushToolSync";function K(){let e=globalThis;return e[j]||(e[j]=new Set),e[j]}function re(e){K().add(e)}function ce(e){K().delete(e)}function be(e){try{for(let o of K())try{o(e)}catch{}}catch{}}function se(e,o){let n=0,i=[e],l=new Set;for(;i.length>0;){if(n>o)return{over:true,approxBytes:n};let r=i.pop();if(r==null)n+=4;else if(typeof r=="string")n+=r.length+2;else if(typeof r=="number")n+=8;else if(typeof r=="boolean")n+=5;else if(typeof r=="object"){if(l.has(r))continue;if(l.add(r),Array.isArray(r)){n+=2+r.length;for(let c=0;c<r.length;c++)i.push(r[c])}else{n+=2;for(let c in r)n+=c.length+4,i.push(r[c])}}else n+=4}return{over:n>o,approxBytes:n}}var $e=1;function U(){let e=globalThis.__buoyRealNow;return typeof e=="function"?e():Date.now()}function ie(){return globalThis.__buoyRealTimers??globalThis}function ae(e,o){return ie().setTimeout(e,o)}function le(e){ie().clearTimeout(e)}function _e(e){let o=globalThis.__buoyWithRealClock;return typeof o=="function"?o(e):e()}var Ie=["_resetPingTimeout","_hasPingExpired"];function ke(e){let o=e?.io;if(!o||o.__buoyRealTimers)return;let n=l=>{if(l){l.setTimeoutFn=ae,l.clearTimeoutFn=le;for(let r of Ie){let c=l[r];if(typeof c!="function"||c.__buoyReal)continue;let a=function(...y){return _e(()=>c.apply(this,y))};a.__buoyReal=true,l[r]=a}}};n(o),n(o.engine);let i=o.open;typeof i=="function"&&(o.open=function(...l){let r=i.apply(this,l);return n(this?.engine),r}),o.__buoyRealTimers=true}var N="[ExternalSync]";function Se({socket:e,deviceId:o,getTools:n,watched:i,throttleMs:l=200,enableLogs:r=false}){let c=false,a=false,y=0,m=()=>!c&&a&&e.connected&&P.hasAccountAccess(),b=new Map,k={},h={},S=t=>t==="All"||t===o,f=(t,s)=>{if(m())try{e.emit(t,s)}catch(g){p(`${N} Failed to emit "${t}": ${g}. Payload must be JSON-serializable.`,r,"error")}},R=new Set,_=t=>{if(!m())return;let s=n()[t];if(!s)return;k[t]=U();let g;try{g=s.getSnapshot()}catch{return}let{over:I,approxBytes:B}=se(g,2097152);if(I){R.has(t)||(R.add(t),console.warn(`${N} "${t}" snapshot \u2248${(B/1e6).toFixed(1)}MB exceeds the ${2097152/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}let V={toolId:t,persistentDeviceId:o,timestamp:k[t],version:s.version,kind:"snapshot",payload:g};f("devtool-sync",V)},D=t=>{if(h[t])return;let s=U()-(k[t]??0);if(s>=l){_(t);return}h[t]=setTimeout(()=>{delete h[t],_(t)},l-s)},O=t=>{e.connected&&b.has(t)&&(h[t]&&(clearTimeout(h[t]),delete h[t]),_(t))};re(O);let w=new Map,L=(t,s)=>{if(!m())return;let g=n()[t];if(g){if(b.has(t)){if(w.get(t)===s)return;let I=b.get(t);b.set(t,g.subscribe(s==="stream"?()=>D(t):()=>{})),w.set(t,s),I?.(),p(`${N} "${t}" watch mode \u2192 ${s}`,r);return}b.set(t,g.subscribe(s==="stream"?()=>D(t):()=>{})),w.set(t,s),p(`${N} Watching "${t}" (${s})`,r)}},E=t=>{b.get(t)?.(),b.delete(t),w.delete(t),h[t]&&(clearTimeout(h[t]),delete h[t]),p(`${N} Stopped watching "${t}"`,r)},F=()=>{let t={persistentDeviceId:o,protocolVersion:1,tools:Object.entries(n()).map(([s,g])=>({toolId:s,version:g.version,actions:Object.keys(g.actions??{})}))};f("devtool-capabilities",t)},d=t=>{if(!(!m()||!S(t.targetDeviceId)))if(t.watching){let s=t.consolidated===true&&t.stream===false?"capture":"stream";i.set(t.toolId,s),L(t.toolId,s),s==="stream"&&_(t.toolId)}else i.delete(t.toolId),E(t.toolId)},u=t=>{!m()||!S(t.targetDeviceId)||_(t.toolId)},X=async t=>{if(!m()||!S(t.targetDeviceId))return;let s=y,{requestId:g,toolId:I,action:B,params:V}=t,v={requestId:g,toolId:I,action:B,persistentDeviceId:o,timestamp:U(),ok:false},ee=n()[I],te=ee?.actions?.[B];if(!te){v.error=ee?`Unknown action "${B}" on tool "${I}"`:`Unknown tool "${I}"`,f("devtool-action-result",v);return}try{v.result=await te(V),v.ok=true}catch(x){v.error=x instanceof Error?x.message:String(x)}if(!(s!==y||!m())){if(v.ok){let{over:x,approxBytes:me}=se(v.result,8388608);x&&(v.ok=false,v.result=void 0,v.error=`Result \u2248${(me/1e6).toFixed(1)}MB exceeds the ${8388608/1e6}MB sync budget. Narrow the request (e.g. includeValues:false, a limit, or a more specific action).`)}v.timestamp=U(),f("devtool-action-result",v)}},A=()=>{y+=1;for(let s of[...b.keys()])E(s);i.clear();for(let s of Object.values(h))clearTimeout(s);let t=e;t.sendBuffer&&(t.sendBuffer=t.sendBuffer.filter(s=>!["devtool-sync","devtool-capabilities","devtool-action-result","device-license"].includes(String(s.data?.[0]))))},G=()=>{let t=P.getState();e.emit("device-license",{tier:t.tier,isPro:t.isPro?"1":"0"}),F()},Q=t=>{a=t?.protocol===1&&t.ready===true,m()?G():A()},ge=P.subscribe(()=>{P.hasAccountAccess()?m()&&G():(e.connected&&e.emit("device-license",{tier:"locked",isPro:"0"}),A())}),Y=()=>{a=false,e.emit("buoy:access:request")},Z=()=>{a=false,A()};e.on("buoy:access",Q);for(let[t,s]of i)n()[t]&&L(t,s);return e.on("connect",Y),e.on("disconnect",Z),e.on("devtool-watch",d),e.on("request-devtool-state",u),e.on("devtool-action",X),e.connected&&Y(),()=>{c=true,ge(),e.off("buoy:access",Q),A(),ce(O),e.off("connect",Y),e.off("disconnect",Z),e.off("devtool-watch",d),e.off("request-devtool-state",u),e.off("devtool-action",X);for(let t of[...b.keys()])E(t)}}function Te({tools:e,socket:o,deviceId:n,enabled:i=true,throttleMs:l=200,enableLogs:r=false}){let c=he().status==="admitted",a=oe(e);a.current=e;let y=oe(new Map),m=Object.keys(e).sort().join(",");ve(()=>{if(!o||!n||!i||!c){y.current.clear();return}return Se({socket:o,deviceId:n,getTools:()=>a.current,watched:y.current,throttleMs:l,enableLogs:r})},[o,n,i,c,l,r,m])}function we(e,o){let n=()=>o(false),i=()=>{n(),e.emit("buoy:access:request")},l=r=>{let c=r;o(c?.protocol===1&&c?.ready===true)};return e.on("connect",i),e.on("disconnect",n),e.on("buoy:access",l),n(),e.connected&&i(),()=>{e.off("connect",i),e.off("disconnect",n),e.off("buoy:access",l)}}import{useEffect as J,useRef as ue,useState as W}from"react";import{io as Ee}from"socket.io-client";function Re(e,o){return e}var De=42831;function Oe(e){return`http://${e??"localhost"}:42831`}function Be(){return null}function fe(){return Oe(typeof location>"u"?null:location.hostname)}var z={targetURL:null,state:"idle",lastError:null,since:0},H=new Set;function T(e){z={...z,...e,since:U()},H.forEach(o=>o())}function xe(){return z}function Ue(e){return H.add(e),()=>{H.delete(e)}}var Ne=typeof ne<"u"?ne:false,$=null,q="",de="",pe="",C=0,M=null,Me=2e3,ye=()=>`${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`;function Le(){let e=globalThis;return e.__buoyInstanceId||(e.__buoyInstanceId=ye()),e.__buoyInstanceId}function Ae(e){let o=n=>n&&Object.fromEntries(Object.entries(n).filter(([i])=>!["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY","licenseKey"].includes(i)));return{deviceName:e.deviceName,deviceId:e.persistentDeviceId,platform:e.platform,extraDeviceInfo:JSON.stringify(o(e.extraDeviceInfo)),envVariables:JSON.stringify(o(e.envVariables)),connectionNonce:ye(),instanceId:Le()}}function Pe({deviceName:e,socketURL:o,persistentDeviceId:n,extraDeviceInfo:i,envVariables:l,platform:r,enableLogs:c=false}){let a=ue(null),[y,m]=W(null),[b,k]=W(false),h=o??fe(),S=ue(false),f=`[${e}]`,R=()=>{p(`${f} Socket connected successfully`,c),S.current=false,k(true),T({state:"connected",lastError:null})},_=d=>{p(`${f} Socket disconnected. Reason: ${d}`,c),k(false),T({state:"retrying"})},D=d=>{if(T({state:"retrying",lastError:d.message}),!Ne||S.current)return;S.current=true;let u=r==="android"&&/localhost|127\.0\.0\.1/.test(q)?"If this device is connected over USB, run: adb reverse tcp:42831 tcp:42831. ":"If it's running on another machine or port, pass socketURL via the externalSync prop. ";p(`[buoy] desktop sync: can't reach ${q} (is Buoy Desktop running?). Retrying in the background. ${u}(${d.message})`,c,"warn")},O=()=>{p(`${f} Socket connection timeout`,c,"error")};J(()=>{if(!n)return;let d=Re(h,r);T({targetURL:d,state:"connecting",lastError:null}),p(`${f} Platform: ${r}, using URL: ${d}`,c);try{$&&(q!==d||de!==n||pe!==r)&&($.disconnect(),$=null),$?p(`${f} Reusing existing socket instance to ${d}`,c):(p(`${f} Creating new socket instance to ${d}`,c),$=Ee(d,{autoConnect:true,query:Ae({deviceName:e,persistentDeviceId:n,platform:r,extraDeviceInfo:i,envVariables:l}),reconnection:true,reconnectionAttempts:1/0,reconnectionDelay:1e3,reconnectionDelayMax:5e3,transports:["websocket"]}),ke($),q=d,de=n,pe=r),a.current=$;let u=a.current;return C++,M&&(le(M),M=null),m(u),u.on("connect_error",D),u.on("connect_timeout",O),k(u.connected),u.connected&&p(`${f} Socket already connected on init`,c),u.on("connect",R),u.on("disconnect",_),()=>{p(`${f} Cleaning up socket event listeners`,c),u.off("connect",R),u.off("disconnect",_),u.off("connect_error",D),u.off("connect_timeout",O),C--,C===0&&(M=ae(()=>{M=null,!(C>0||$!==u)&&(p(`${f} No longer used; disconnecting`,c),u.disconnect(),$=null,T({state:"idle",lastError:null}))},Me))}}catch(u){p(`${f} Failed to initialize socket: ${u}`,c,"error")}},[n,h,r]),J(()=>{a.current&&a.current.io.opts.query&&n&&(p(`${f} Updating device name in socket connection`,c),a.current.io.opts.query={...a.current.io.opts.query,deviceName:e,deviceId:n,platform:r})},[e,f,n,r,c]);function w(){a.current&&!a.current.connected&&(p(`${f} Manually connecting to socket server`,c),a.current.connect())}function L(){a.current&&a.current.connected&&(p(`${f} Manually disconnecting from socket server`,c),a.current.disconnect())}let[E,F]=W(null);return J(()=>{if(y)return we(y,d=>{F(d?y:null),T(d?{state:"connected",lastError:null}:{state:"retrying",lastError:"Update Buoy Desktop and sign in to enable desktop sync."})})},[y]),{socket:E===y?y:null,connect:w,disconnect:L,isConnected:b&&E===y&&y!==null}}export{De as DEFAULT_BROKER_PORT,$e as SYNC_PROTOCOL_VERSION,be as flushToolSyncNow,fe as getDefaultSocketURL,Be as getMetroHost,xe as getSyncStatus,re as registerToolFlusher,T as setSyncStatus,Ue as subscribeSyncStatus,ce as unregisterToolFlusher,Te as useExternalSync,Pe as useExternalSyncSocket};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/external-sync",
3
- "version": "7.0.41",
3
+ "version": "7.0.43",
4
4
  "description": "Generic device-to-desktop sync for Buoy dev tools",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -35,13 +35,10 @@
35
35
  "sideEffects": false,
36
36
  "dependencies": {
37
37
  "socket.io-client": "^4.8.1",
38
- "@buoy-gg/license": "^7.0.41"
38
+ "@buoy-gg/license": "^7.0.43"
39
39
  },
40
40
  "peerDependencies": {
41
- "react": "*",
42
- "react-native": "*",
43
- "react-native-web": "^0.21.0",
44
- "react-dom": ">=18"
41
+ "react": "*"
45
42
  },
46
43
  "devDependencies": {
47
44
  "@types/react": "^19.1.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"attachDeviceSync.d.ts","sourceRoot":"","sources":["../../src/attachDeviceSync.ts"],"names":[],"mappings":"AAQA,OAAO,EAQL,KAAK,eAAe,EACrB,MAAM,SAAS,CAAC;AAIjB,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IACrD,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC;IACnE,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC;CACrE,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE7C,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAChD;;;;OAIG;IACH,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,UAAgB,EAChB,UAAkB,GACnB,EAAE,uBAAuB,GAAG,MAAM,IAAI,CAsStC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"brokerAccess.d.ts","sourceRoot":"","sources":["../../src/brokerAccess.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC;IAC/D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC;IAChE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B;AACD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAC/B,MAAM,IAAI,CAoBZ"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"crashFlush.d.ts","sourceRoot":"","sources":["../../src/crashFlush.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,oEAAoE;AACpE,MAAM,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAiBnD,gEAAgE;AAChE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAE5D;AAED,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAE9D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAYrD"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAKhE,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,mBAAmB,GACpB,MAAM,6BAA6B,CAAC;AAKrC,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,OAAO,EACL,aAAa,EACb,aAAa,EACb,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAKpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAKhD,YAAY,EACV,eAAe,EACf,UAAU,EACV,cAAc,EACd,QAAQ,EACR,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACnB,qBAAqB,EACrB,UAAU,GACX,MAAM,SAAS,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"syncStatusStore.d.ts","sourceRoot":"","sources":["../../src/syncStatusStore.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,UAAU;IACzB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,UAAU,CAAC;IACxD,oEAAoE;IACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;CACf;AAWD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,CAG9D;AAED,wBAAgB,aAAa,IAAI,UAAU,CAE1C;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAKpE"}
@@ -1 +0,0 @@
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;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAMD,MAAM,WAAW,oBAAoB;IACnC,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC,8DAA8D;IAC9D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qBAAqB;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAMD;;GAEG;AACH,MAAM,MAAM,UAAU,GAClB,KAAK,GACL,SAAS,GACT,KAAK,GACL,SAAS,GACT,OAAO,GACP,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,UAAU,GACV,aAAa,CAAC;AAMlB,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,QAAQ,EAAE,UAAU,CAAC;IACrB,qBAAqB;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"useExternalSync.d.ts","sourceRoot":"","sources":["../../src/useExternalSync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,MAAM,EACN,QAAQ,EACR,OAAc,EACd,UAAgB,EAChB,UAAkB,GACnB,EAAE,oBAAoB,QA4BtB"}