@unbound-app/debugger-protocol 1.0.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controller.d.mts +9 -5
- package/dist/controller.mjs +47 -66
- package/dist/index-DwVwBIbm.d.mts +234 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -73
- package/dist/pending-requests.d.mts +42 -0
- package/dist/pending-requests.mjs +54 -0
- package/dist/src-D0Oi4NXs.mjs +139 -0
- package/package.json +5 -1
- package/dist/index-DdSiZNoR.d.mts +0 -124
package/dist/controller.d.mts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { g as PushedAddonKind, h as PushResult, s as DeviceInfo, u as EvalResult } from "./index-DwVwBIbm.mjs";
|
|
2
2
|
import { PushedManifest } from "@unbound-app/types";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The controller half of the bridge: dials the `?mcp` endpoint and settles id-correlated evals
|
|
5
|
+
* against the results the bridge routes back. Reconnects so it outlives bridge restarts.
|
|
6
|
+
*/
|
|
7
|
+
type AddonPush = {
|
|
4
8
|
addonId: string;
|
|
5
9
|
kind?: PushedAddonKind;
|
|
6
10
|
bundle: string;
|
|
@@ -10,6 +14,7 @@ type PluginPush = {
|
|
|
10
14
|
};
|
|
11
15
|
export declare class ControllerClient {
|
|
12
16
|
private ws;
|
|
17
|
+
/** Each pending eval keeps the console output seen since its dispatch, so side effects surface too. */
|
|
13
18
|
private readonly pending;
|
|
14
19
|
private readonly pendingPushes;
|
|
15
20
|
private deviceConnected;
|
|
@@ -21,7 +26,6 @@ export declare class ControllerClient {
|
|
|
21
26
|
connect(): void;
|
|
22
27
|
/** @description Tears the client down and fails any in-flight evals. Twin of {@link connect}. */
|
|
23
28
|
close(): void;
|
|
24
|
-
/** @description Whether a device is currently connected to the bridge. */
|
|
25
29
|
get isDeviceConnected(): boolean;
|
|
26
30
|
/** @description What the connected device reported about itself in its hello, once it has said hello. */
|
|
27
31
|
get device(): DeviceInfo | undefined;
|
|
@@ -37,9 +41,9 @@ export declare class ControllerClient {
|
|
|
37
41
|
* @description Pushes a freshly built plugin or theme to the device via the bridge and resolves with the
|
|
38
42
|
* device's reload outcome. Fails fast if the socket isn't open or the bridge doesn't answer in time.
|
|
39
43
|
* @param push The addon id plus the built bundle and manifest to swap in.
|
|
40
|
-
* @returns The device's {@link
|
|
44
|
+
* @returns The device's {@link PushResult}.
|
|
41
45
|
*/
|
|
42
|
-
|
|
46
|
+
pushAddon(push: AddonPush): Promise<PushResult>;
|
|
43
47
|
private onMessage;
|
|
44
48
|
private onClose;
|
|
45
49
|
}
|
package/dist/controller.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as resolveEvalTimeout, l as serializeMessage, o as PUSH_TIMEOUT_MS, s as parseMessage, t as BRIDGE_REPLY_GRACE_MS } from "./src-D0Oi4NXs.mjs";
|
|
2
|
+
import { PendingRequests } from "./pending-requests.mjs";
|
|
2
3
|
// src/controller.ts
|
|
3
|
-
const EVAL_RESULT_GRACE_MS = 5e3;
|
|
4
|
-
const PLUGIN_PUSH_TIMEOUT_MS = 75e3;
|
|
5
|
-
/** How long to wait between reconnection attempts when the bridge is unreachable. */
|
|
6
4
|
const RECONNECT_DELAY_MS = 1e3;
|
|
7
5
|
var ControllerClient = class {
|
|
8
6
|
ws = null;
|
|
9
|
-
pending
|
|
10
|
-
|
|
7
|
+
/** Each pending eval keeps the console output seen since its dispatch, so side effects surface too. */
|
|
8
|
+
pending = new PendingRequests();
|
|
9
|
+
pendingPushes = new PendingRequests();
|
|
11
10
|
deviceConnected = false;
|
|
12
11
|
deviceInfo;
|
|
13
12
|
closed = false;
|
|
@@ -27,29 +26,21 @@ var ControllerClient = class {
|
|
|
27
26
|
close() {
|
|
28
27
|
this.closed = true;
|
|
29
28
|
this.ws?.close();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
id,
|
|
45
|
-
ok: false,
|
|
46
|
-
error: "Debugger client shut down."
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
this.pending.clear();
|
|
50
|
-
this.pendingPushes.clear();
|
|
29
|
+
this.pending.failAll((id) => ({
|
|
30
|
+
type: "eval-result",
|
|
31
|
+
id,
|
|
32
|
+
ok: false,
|
|
33
|
+
error: "Debugger client shut down.",
|
|
34
|
+
code: "shut-down"
|
|
35
|
+
}));
|
|
36
|
+
this.pendingPushes.failAll((id) => ({
|
|
37
|
+
type: "plugin-push-result",
|
|
38
|
+
id,
|
|
39
|
+
ok: false,
|
|
40
|
+
error: "Debugger client shut down.",
|
|
41
|
+
code: "shut-down"
|
|
42
|
+
}));
|
|
51
43
|
}
|
|
52
|
-
/** @description Whether a device is currently connected to the bridge. */
|
|
53
44
|
get isDeviceConnected() {
|
|
54
45
|
return this.deviceConnected;
|
|
55
46
|
}
|
|
@@ -72,26 +63,27 @@ var ControllerClient = class {
|
|
|
72
63
|
type: "eval-result",
|
|
73
64
|
id,
|
|
74
65
|
ok: false,
|
|
75
|
-
error: "Not connected to the debugger bridge."
|
|
66
|
+
error: "Not connected to the debugger bridge.",
|
|
67
|
+
code: "bridge-unreachable"
|
|
76
68
|
});
|
|
77
69
|
return;
|
|
78
70
|
}
|
|
79
71
|
const logs = [];
|
|
80
|
-
const limit =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
const limit = resolveEvalTimeout(timeout);
|
|
73
|
+
this.pending.add(id, {
|
|
74
|
+
settle: (result) => resolve({
|
|
75
|
+
...result,
|
|
76
|
+
logs
|
|
77
|
+
}),
|
|
78
|
+
timeoutMs: limit + BRIDGE_REPLY_GRACE_MS,
|
|
79
|
+
onTimeout: () => ({
|
|
84
80
|
type: "eval-result",
|
|
85
81
|
id,
|
|
86
82
|
ok: false,
|
|
87
83
|
error: `Evaluation timed out after ${limit}ms.`,
|
|
88
|
-
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
this.pending.set(id, {
|
|
92
|
-
resolve,
|
|
93
|
-
timer,
|
|
94
|
-
logs
|
|
84
|
+
code: "timeout"
|
|
85
|
+
}),
|
|
86
|
+
meta: logs
|
|
95
87
|
});
|
|
96
88
|
this.ws.send(serializeMessage(timeout === void 0 ? {
|
|
97
89
|
type: "eval",
|
|
@@ -109,9 +101,9 @@ var ControllerClient = class {
|
|
|
109
101
|
* @description Pushes a freshly built plugin or theme to the device via the bridge and resolves with the
|
|
110
102
|
* device's reload outcome. Fails fast if the socket isn't open or the bridge doesn't answer in time.
|
|
111
103
|
* @param push The addon id plus the built bundle and manifest to swap in.
|
|
112
|
-
* @returns The device's {@link
|
|
104
|
+
* @returns The device's {@link PushResult}.
|
|
113
105
|
*/
|
|
114
|
-
|
|
106
|
+
pushAddon(push) {
|
|
115
107
|
const id = crypto.randomUUID();
|
|
116
108
|
return new Promise((resolve) => {
|
|
117
109
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
@@ -119,22 +111,22 @@ var ControllerClient = class {
|
|
|
119
111
|
type: "plugin-push-result",
|
|
120
112
|
id,
|
|
121
113
|
ok: false,
|
|
122
|
-
error: "Not connected to the debugger bridge."
|
|
114
|
+
error: "Not connected to the debugger bridge.",
|
|
115
|
+
code: "bridge-unreachable"
|
|
123
116
|
});
|
|
124
117
|
return;
|
|
125
118
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
119
|
+
this.pendingPushes.add(id, {
|
|
120
|
+
settle: resolve,
|
|
121
|
+
timeoutMs: PUSH_TIMEOUT_MS + BRIDGE_REPLY_GRACE_MS,
|
|
122
|
+
onTimeout: () => ({
|
|
129
123
|
type: "plugin-push-result",
|
|
130
124
|
id,
|
|
131
125
|
ok: false,
|
|
132
|
-
error: `
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
resolve,
|
|
137
|
-
timer
|
|
126
|
+
error: `Push timed out after ${PUSH_TIMEOUT_MS}ms.`,
|
|
127
|
+
code: "timeout"
|
|
128
|
+
}),
|
|
129
|
+
meta: void 0
|
|
138
130
|
});
|
|
139
131
|
this.ws.send(serializeMessage({
|
|
140
132
|
type: "plugin-push",
|
|
@@ -152,26 +144,15 @@ var ControllerClient = class {
|
|
|
152
144
|
return;
|
|
153
145
|
}
|
|
154
146
|
if (message.type === "log") {
|
|
155
|
-
for (const
|
|
147
|
+
for (const logs of this.pending.metas()) logs.push(message);
|
|
156
148
|
return;
|
|
157
149
|
}
|
|
158
150
|
if (message.type === "eval-result") {
|
|
159
|
-
|
|
160
|
-
if (!entry) return;
|
|
161
|
-
clearTimeout(entry.timer);
|
|
162
|
-
this.pending.delete(message.id);
|
|
163
|
-
entry.resolve({
|
|
164
|
-
...message,
|
|
165
|
-
logs: entry.logs
|
|
166
|
-
});
|
|
151
|
+
this.pending.resolve(message.id, message);
|
|
167
152
|
return;
|
|
168
153
|
}
|
|
169
154
|
if (message.type !== "plugin-push-result") return;
|
|
170
|
-
|
|
171
|
-
if (!entry) return;
|
|
172
|
-
clearTimeout(entry.timer);
|
|
173
|
-
this.pendingPushes.delete(message.id);
|
|
174
|
-
entry.resolve(message);
|
|
155
|
+
this.pendingPushes.resolve(message.id, message);
|
|
175
156
|
}
|
|
176
157
|
onClose() {
|
|
177
158
|
this.ws = null;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { PushedManifest } from "@unbound-app/types";
|
|
2
|
+
/**
|
|
3
|
+
* React DevTools over the debugger socket: the device's backend and the frontend (the DevTools app's
|
|
4
|
+
* controller) swap React DevTools' own `{ event, payload }` messages, each wrapped in one frame.
|
|
5
|
+
*/
|
|
6
|
+
/** Device ↔ bridge ↔ the frontend controller: one React DevTools message. */
|
|
7
|
+
type ReactDevtoolsFrame = {
|
|
8
|
+
type: 'react-devtools';
|
|
9
|
+
event: string;
|
|
10
|
+
payload?: unknown;
|
|
11
|
+
};
|
|
12
|
+
/** The payload of {@link REACT_DEVTOOLS_STATUS_EVENT}. */
|
|
13
|
+
type ReactDevtoolsStatus = {
|
|
14
|
+
connected: boolean;
|
|
15
|
+
/** Why the backend is unavailable, such as the loader not having run it. */
|
|
16
|
+
error?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The event the device sends the frontend, with a {@link ReactDevtoolsStatus}, when its backend
|
|
20
|
+
* starts a session, cannot start one, or ends one.
|
|
21
|
+
*/
|
|
22
|
+
declare const REACT_DEVTOOLS_STATUS_EVENT = "unbound:backend-status";
|
|
23
|
+
/** React DevTools' own event that ends a backend session; the bridge sends it when the frontend leaves. */
|
|
24
|
+
declare const REACT_DEVTOOLS_SHUTDOWN_EVENT = "shutdown";
|
|
25
|
+
/**
|
|
26
|
+
* The view finder's frames: a controller asks the device for one level of its native view tree at a
|
|
27
|
+
* time, and names the view the device should outline on screen.
|
|
28
|
+
*/
|
|
29
|
+
/** A rectangle in screen coordinates, in dp on Android and points on iOS. */
|
|
30
|
+
type ViewFrame = {
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
};
|
|
36
|
+
/** One native view, with only its top-level properties. */
|
|
37
|
+
type ViewNode = {
|
|
38
|
+
/** The device's id for the view, valid until the next request for the roots. */
|
|
39
|
+
id: string;
|
|
40
|
+
/** The fully qualified Java class name, or the Objective-C class name. */
|
|
41
|
+
className: string;
|
|
42
|
+
/** The Android resource entry name of the view's id, or the iOS accessibility identifier. */
|
|
43
|
+
resourceName: string | null;
|
|
44
|
+
frame: ViewFrame;
|
|
45
|
+
visibility: 'visible' | 'invisible' | 'gone';
|
|
46
|
+
/** The view's opacity from 0 to 1, or `null` when the platform did not report it. */
|
|
47
|
+
alpha: number | null;
|
|
48
|
+
/** The text a text view shows, cut to a few hundred characters. */
|
|
49
|
+
text: string | null;
|
|
50
|
+
/** How many direct children the view has. */
|
|
51
|
+
childCount: number;
|
|
52
|
+
};
|
|
53
|
+
/** Controller → bridge → device: list the children of `parent`, or the root views when it is omitted. */
|
|
54
|
+
type ViewsRequest = {
|
|
55
|
+
type: 'views-request';
|
|
56
|
+
/** Correlation id echoed back on the matching {@link ViewsResult}. */
|
|
57
|
+
id: string;
|
|
58
|
+
parent?: string;
|
|
59
|
+
};
|
|
60
|
+
/** The answer to a {@link ViewsRequest}, sent device → bridge → the controller that asked. */
|
|
61
|
+
type ViewsResult = {
|
|
62
|
+
type: 'views-result';
|
|
63
|
+
/** The `id` of the {@link ViewsRequest} this answers. */
|
|
64
|
+
id: string;
|
|
65
|
+
} & ({
|
|
66
|
+
ok: true;
|
|
67
|
+
nodes: ViewNode[];
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
error: string;
|
|
71
|
+
});
|
|
72
|
+
/** Controller → bridge → device: outline the view with this id on screen, or clear the outline when `null`. */
|
|
73
|
+
type ViewHighlight = {
|
|
74
|
+
type: 'view-highlight';
|
|
75
|
+
node: string | null;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The wire protocol shared by the bridge, the device and controllers: every frame is a JSON-encoded
|
|
79
|
+
* {@link BridgeMessage}. Dependency-free at runtime so the React Native client can bundle it.
|
|
80
|
+
*/
|
|
81
|
+
/** A request to evaluate code on the device, sent controller → bridge → device. */
|
|
82
|
+
type EvalRequest = {
|
|
83
|
+
type: 'eval';
|
|
84
|
+
/** Correlation id echoed back on the matching {@link EvalResult}. */
|
|
85
|
+
id: string;
|
|
86
|
+
code: string;
|
|
87
|
+
timeout?: number;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Why a request failed before the device answered it, so callers branch on a code instead of the error text.
|
|
91
|
+
*/
|
|
92
|
+
type RequestFailureCode =
|
|
93
|
+
/** The bridge has no device. */
|
|
94
|
+
'no-device' |
|
|
95
|
+
/** The device dropped mid-request. */
|
|
96
|
+
'device-disconnected' | 'timeout' |
|
|
97
|
+
/** The controller has no socket to the bridge. */
|
|
98
|
+
'bridge-unreachable' |
|
|
99
|
+
/** The controller closed. */
|
|
100
|
+
'shut-down';
|
|
101
|
+
/** The outcome of an {@link EvalRequest}, sent device → bridge → the one waiting controller. */
|
|
102
|
+
type EvalResult = {
|
|
103
|
+
type: 'eval-result';
|
|
104
|
+
/** The `id` of the {@link EvalRequest} this answers. */
|
|
105
|
+
id: string;
|
|
106
|
+
/**
|
|
107
|
+
* Console output captured during this eval, so callers see its side effects too. Populated by
|
|
108
|
+
* the controller that owns the request, not sent by the device.
|
|
109
|
+
*/
|
|
110
|
+
logs?: LogMessage[];
|
|
111
|
+
} & ({
|
|
112
|
+
ok: true;
|
|
113
|
+
/** The inspected return value. */
|
|
114
|
+
value: string;
|
|
115
|
+
} | {
|
|
116
|
+
ok: false;
|
|
117
|
+
/** The inspected error. */
|
|
118
|
+
error: string;
|
|
119
|
+
/** Set when the bridge or controller failed the eval; absent when the code itself threw on the device. */
|
|
120
|
+
code?: RequestFailureCode;
|
|
121
|
+
});
|
|
122
|
+
/** The addon managers a push can reload into. */
|
|
123
|
+
type PushedAddonKind = 'plugin' | 'theme';
|
|
124
|
+
/**
|
|
125
|
+
* A request to hot-reload a plugin or theme on the device, sent controller → bridge → device. Carries
|
|
126
|
+
* the freshly built bundle and manifest so the device swaps them in without a re-fetch.
|
|
127
|
+
*/
|
|
128
|
+
type PushRequest = {
|
|
129
|
+
type: 'plugin-push';
|
|
130
|
+
/** Correlation id echoed back on the matching {@link PushResult}. */
|
|
131
|
+
id: string;
|
|
132
|
+
addonId: string;
|
|
133
|
+
/** Which manager reloads the addon; a plugin when omitted, as pushes from older CLIs are. */
|
|
134
|
+
kind?: PushedAddonKind;
|
|
135
|
+
bundle: string;
|
|
136
|
+
/** The manifest as authored; the device validates it before swapping anything in. */
|
|
137
|
+
manifest: PushedManifest;
|
|
138
|
+
/** The bundle compiled to Hermes bytecode, base64-encoded, by bytecode version; its digests are in `manifest.bytecode`. */
|
|
139
|
+
bytecode?: Record<string, string>;
|
|
140
|
+
/** Each extra file the manifest lists beside the bundle, such as an image, base64-encoded, by path. */
|
|
141
|
+
files?: Record<string, string>;
|
|
142
|
+
};
|
|
143
|
+
/** The outcome of a {@link PushRequest}, sent device → bridge → the one waiting controller. */
|
|
144
|
+
type PushResult = {
|
|
145
|
+
type: 'plugin-push-result';
|
|
146
|
+
/** The `id` of the {@link PushRequest} this answers. */
|
|
147
|
+
id: string;
|
|
148
|
+
} & ({
|
|
149
|
+
ok: true;
|
|
150
|
+
} | {
|
|
151
|
+
ok: false;
|
|
152
|
+
/** The reload failure, inspected. */
|
|
153
|
+
error: string;
|
|
154
|
+
/** Set when the bridge or controller failed the push; absent when the device's reload failed. */
|
|
155
|
+
code?: RequestFailureCode;
|
|
156
|
+
});
|
|
157
|
+
/** A `console.*` / `nativeLoggingHook` line, streamed device → bridge → all controllers. */
|
|
158
|
+
type LogMessage = {
|
|
159
|
+
type: 'log';
|
|
160
|
+
/** 1 = log/gray, 2 = warn/yellow, 3 = error/red. */
|
|
161
|
+
level: 1 | 2 | 3;
|
|
162
|
+
message: string;
|
|
163
|
+
};
|
|
164
|
+
/** What a device reports about itself when it connects. */
|
|
165
|
+
type DeviceInfo = {
|
|
166
|
+
platform: string;
|
|
167
|
+
discordVersion: string;
|
|
168
|
+
loaderVersion: string;
|
|
169
|
+
clientVersion: string;
|
|
170
|
+
};
|
|
171
|
+
/** Device → bridge, once its socket opens, so the bridge can say which device is attached. */
|
|
172
|
+
type DeviceHello = {
|
|
173
|
+
type: 'hello';
|
|
174
|
+
device: DeviceInfo;
|
|
175
|
+
};
|
|
176
|
+
/** Bridge → controller, once on attach and whenever the device connects, drops or says hello. */
|
|
177
|
+
type DeviceStatus = {
|
|
178
|
+
type: 'device-status';
|
|
179
|
+
} & ({
|
|
180
|
+
connected: false;
|
|
181
|
+
} | {
|
|
182
|
+
connected: true;
|
|
183
|
+
/** Present once the connected device has said hello. */
|
|
184
|
+
device?: DeviceInfo;
|
|
185
|
+
});
|
|
186
|
+
/** Every framed message on the wire. */
|
|
187
|
+
type BridgeMessage = EvalRequest | EvalResult | PushRequest | PushResult | LogMessage | DeviceHello | DeviceStatus | ViewsRequest | ViewsResult | ViewHighlight | ReactDevtoolsFrame;
|
|
188
|
+
/** The default port the bridge listens on and controllers dial. */
|
|
189
|
+
declare const DEFAULT_BRIDGE_PORT = 9090;
|
|
190
|
+
declare const DEFAULT_EVAL_TIMEOUT_MS = 10000;
|
|
191
|
+
declare const MAX_EVAL_TIMEOUT_MS = 86400000;
|
|
192
|
+
/** How long the bridge waits for the device to reload a pushed addon. */
|
|
193
|
+
declare const PUSH_TIMEOUT_MS = 60000;
|
|
194
|
+
/**
|
|
195
|
+
* How much longer a controller waits than the bridge, so the bridge's own timeout reply always
|
|
196
|
+
* arrives before the controller gives up on its own.
|
|
197
|
+
*/
|
|
198
|
+
declare const BRIDGE_REPLY_GRACE_MS = 5000;
|
|
199
|
+
/**
|
|
200
|
+
* @description Picks how long an eval may run: the requested timeout when it is a positive number of
|
|
201
|
+
* milliseconds, capped at {@link MAX_EVAL_TIMEOUT_MS}, otherwise {@link DEFAULT_EVAL_TIMEOUT_MS}.
|
|
202
|
+
* @param requested The timeout the controller asked for, if any.
|
|
203
|
+
* @returns The timeout to apply, in milliseconds.
|
|
204
|
+
*/
|
|
205
|
+
declare function resolveEvalTimeout(requested: number | undefined): number;
|
|
206
|
+
/** The query marker a controller appends to its URL so the bridge routes it as a controller, not a device. */
|
|
207
|
+
declare const CONTROLLER_ENDPOINT_MARKER = "mcp";
|
|
208
|
+
/**
|
|
209
|
+
* @description Safely parses a raw wire frame into a {@link BridgeMessage}, guarding against invalid
|
|
210
|
+
* JSON and unknown shapes so both ends can trust what they route.
|
|
211
|
+
* @param raw The JSON string received off the socket.
|
|
212
|
+
* @returns The parsed message, or `undefined` if it isn't a recognised frame.
|
|
213
|
+
*/
|
|
214
|
+
declare function parseMessage(raw: string): BridgeMessage | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* @description Serialises a {@link BridgeMessage} into its wire form. Inverse of {@link parseMessage}.
|
|
217
|
+
* @param message The message to frame.
|
|
218
|
+
* @returns The JSON string to send over the socket.
|
|
219
|
+
*/
|
|
220
|
+
declare function serializeMessage(message: BridgeMessage): string;
|
|
221
|
+
declare const _default: {
|
|
222
|
+
DEFAULT_BRIDGE_PORT: number;
|
|
223
|
+
DEFAULT_EVAL_TIMEOUT_MS: number;
|
|
224
|
+
MAX_EVAL_TIMEOUT_MS: number;
|
|
225
|
+
PUSH_TIMEOUT_MS: number;
|
|
226
|
+
BRIDGE_REPLY_GRACE_MS: number;
|
|
227
|
+
CONTROLLER_ENDPOINT_MARKER: string;
|
|
228
|
+
REACT_DEVTOOLS_SHUTDOWN_EVENT: string;
|
|
229
|
+
REACT_DEVTOOLS_STATUS_EVENT: string;
|
|
230
|
+
resolveEvalTimeout: typeof resolveEvalTimeout;
|
|
231
|
+
parseMessage: typeof parseMessage;
|
|
232
|
+
serializeMessage: typeof serializeMessage;
|
|
233
|
+
};
|
|
234
|
+
export { ReactDevtoolsStatus as A, ViewHighlight as C, REACT_DEVTOOLS_SHUTDOWN_EVENT as D, ViewsResult as E, REACT_DEVTOOLS_STATUS_EVENT as O, ViewFrame as S, ViewsRequest as T, RequestFailureCode as _, DEFAULT_EVAL_TIMEOUT_MS as a, resolveEvalTimeout as b, DeviceStatus as c, LogMessage as d, MAX_EVAL_TIMEOUT_MS as f, PushedAddonKind as g, PushResult as h, DEFAULT_BRIDGE_PORT as i, ReactDevtoolsFrame as k, EvalRequest as l, PushRequest as m, BridgeMessage as n, DeviceHello as o, PUSH_TIMEOUT_MS as p, CONTROLLER_ENDPOINT_MARKER as r, DeviceInfo as s, BRIDGE_REPLY_GRACE_MS as t, EvalResult as u, _default as v, ViewNode as w, serializeMessage as x, parseMessage as y };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
export { BridgeMessage, CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, DEFAULT_EVAL_TIMEOUT_MS, DeviceHello, DeviceInfo, DeviceStatus, EvalRequest, EvalResult, LogMessage, MAX_EVAL_TIMEOUT_MS,
|
|
1
|
+
import { A as ReactDevtoolsStatus, C as ViewHighlight, D as REACT_DEVTOOLS_SHUTDOWN_EVENT, E as ViewsResult, O as REACT_DEVTOOLS_STATUS_EVENT, S as ViewFrame, T as ViewsRequest, _ as RequestFailureCode, a as DEFAULT_EVAL_TIMEOUT_MS, b as resolveEvalTimeout, c as DeviceStatus, d as LogMessage, f as MAX_EVAL_TIMEOUT_MS, g as PushedAddonKind, h as PushResult, i as DEFAULT_BRIDGE_PORT, k as ReactDevtoolsFrame, l as EvalRequest, m as PushRequest, n as BridgeMessage, o as DeviceHello, p as PUSH_TIMEOUT_MS, r as CONTROLLER_ENDPOINT_MARKER, s as DeviceInfo, t as BRIDGE_REPLY_GRACE_MS, u as EvalResult, v as _default, w as ViewNode, x as serializeMessage, y as parseMessage } from "./index-DwVwBIbm.mjs";
|
|
2
|
+
export { BRIDGE_REPLY_GRACE_MS, BridgeMessage, CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, DEFAULT_EVAL_TIMEOUT_MS, DeviceHello, DeviceInfo, DeviceStatus, EvalRequest, EvalResult, LogMessage, MAX_EVAL_TIMEOUT_MS, PUSH_TIMEOUT_MS, PushRequest, PushResult, PushedAddonKind, REACT_DEVTOOLS_SHUTDOWN_EVENT, REACT_DEVTOOLS_STATUS_EVENT, type ReactDevtoolsFrame, type ReactDevtoolsStatus, RequestFailureCode, type ViewFrame, type ViewHighlight, type ViewNode, type ViewsRequest, type ViewsResult, _default as default, parseMessage, resolveEvalTimeout, serializeMessage };
|
package/dist/index.mjs
CHANGED
|
@@ -1,73 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/** The default port the bridge listens on and controllers dial. */
|
|
3
|
-
const DEFAULT_BRIDGE_PORT = 9090;
|
|
4
|
-
const DEFAULT_EVAL_TIMEOUT_MS = 1e4;
|
|
5
|
-
const MAX_EVAL_TIMEOUT_MS = 864e5;
|
|
6
|
-
/** The query marker a controller appends to its URL so the bridge routes it as a controller, not a device. */
|
|
7
|
-
const CONTROLLER_ENDPOINT_MARKER = "mcp";
|
|
8
|
-
const MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
9
|
-
"eval",
|
|
10
|
-
"eval-result",
|
|
11
|
-
"plugin-push",
|
|
12
|
-
"plugin-push-result",
|
|
13
|
-
"log",
|
|
14
|
-
"hello",
|
|
15
|
-
"device-status"
|
|
16
|
-
]);
|
|
17
|
-
/**
|
|
18
|
-
* @description Safely parses a raw wire frame into a {@link BridgeMessage}, guarding against invalid
|
|
19
|
-
* JSON and unknown shapes so both ends can trust what they route.
|
|
20
|
-
* @param raw The JSON string received off the socket.
|
|
21
|
-
* @returns The parsed message, or `undefined` if it isn't a recognised frame.
|
|
22
|
-
*/
|
|
23
|
-
function parseMessage(raw) {
|
|
24
|
-
let value;
|
|
25
|
-
try {
|
|
26
|
-
value = JSON.parse(raw);
|
|
27
|
-
} catch {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
return isBridgeMessage(value) ? value : void 0;
|
|
31
|
-
}
|
|
32
|
-
function hasRequiredFields(frame) {
|
|
33
|
-
if (typeof frame !== "object" || frame === null || !("type" in frame)) return false;
|
|
34
|
-
switch (frame.type) {
|
|
35
|
-
case "log": return "message" in frame && typeof frame.message === "string" && "level" in frame && (frame.level === 1 || frame.level === 2 || frame.level === 3);
|
|
36
|
-
case "eval": return "id" in frame && typeof frame.id === "string" && "code" in frame && typeof frame.code === "string";
|
|
37
|
-
case "eval-result": return "id" in frame && typeof frame.id === "string" && "ok" in frame && (frame.ok === true ? "value" in frame && typeof frame.value === "string" : frame.ok === false && hasError(frame));
|
|
38
|
-
case "plugin-push-result": return "id" in frame && typeof frame.id === "string" && "ok" in frame && (frame.ok === true || frame.ok === false && hasError(frame));
|
|
39
|
-
case "plugin-push": return "id" in frame && typeof frame.id === "string" && "addonId" in frame && typeof frame.addonId === "string" && (!("kind" in frame) || frame.kind === void 0 || frame.kind === "plugin" || frame.kind === "theme") && "bundle" in frame && typeof frame.bundle === "string" && "manifest" in frame && typeof frame.manifest === "object" && frame.manifest !== null && (!("bytecode" in frame) || frame.bytecode === void 0 || isStringRecord(frame.bytecode)) && (!("files" in frame) || frame.files === void 0 || isStringRecord(frame.files));
|
|
40
|
-
case "hello": return "device" in frame && isDeviceInfo(frame.device);
|
|
41
|
-
case "device-status": return "connected" in frame && (frame.connected === false || frame.connected === true && (!("device" in frame) || frame.device === void 0 || isDeviceInfo(frame.device)));
|
|
42
|
-
default: return false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function hasError(frame) {
|
|
46
|
-
return typeof frame === "object" && frame !== null && "error" in frame && typeof frame.error === "string";
|
|
47
|
-
}
|
|
48
|
-
function isDeviceInfo(value) {
|
|
49
|
-
return typeof value === "object" && value !== null && "platform" in value && typeof value.platform === "string" && "discordVersion" in value && typeof value.discordVersion === "string" && "loaderVersion" in value && typeof value.loaderVersion === "string" && "clientVersion" in value && typeof value.clientVersion === "string";
|
|
50
|
-
}
|
|
51
|
-
function isStringRecord(value) {
|
|
52
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
53
|
-
}
|
|
54
|
-
function isBridgeMessage(value) {
|
|
55
|
-
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string" && MESSAGE_TYPES.has(value.type) && hasRequiredFields(value);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* @description Serialises a {@link BridgeMessage} into its wire form. Inverse of {@link parseMessage}.
|
|
59
|
-
* @param message The message to frame.
|
|
60
|
-
* @returns The JSON string to send over the socket.
|
|
61
|
-
*/
|
|
62
|
-
function serializeMessage(message) {
|
|
63
|
-
return JSON.stringify(message);
|
|
64
|
-
}
|
|
65
|
-
var src_default = {
|
|
66
|
-
DEFAULT_BRIDGE_PORT,
|
|
67
|
-
DEFAULT_EVAL_TIMEOUT_MS,
|
|
68
|
-
MAX_EVAL_TIMEOUT_MS,
|
|
69
|
-
CONTROLLER_ENDPOINT_MARKER: "mcp",
|
|
70
|
-
parseMessage,
|
|
71
|
-
serializeMessage
|
|
72
|
-
};
|
|
73
|
-
export { CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, DEFAULT_EVAL_TIMEOUT_MS, MAX_EVAL_TIMEOUT_MS, src_default as default, parseMessage, serializeMessage };
|
|
1
|
+
export { t as BRIDGE_REPLY_GRACE_MS, n as CONTROLLER_ENDPOINT_MARKER, r as DEFAULT_BRIDGE_PORT, i as DEFAULT_EVAL_TIMEOUT_MS, a as MAX_EVAL_TIMEOUT_MS, o as PUSH_TIMEOUT_MS, d as REACT_DEVTOOLS_SHUTDOWN_EVENT, f as REACT_DEVTOOLS_STATUS_EVENT, u as default, s as parseMessage, c as resolveEvalTimeout, l as serializeMessage } from "./src-D0Oi4NXs.mjs";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Id-correlated requests awaiting a reply. Each settles exactly once: by its reply, its timeout, or
|
|
3
|
+
* a fail-all when its peer goes away. Used by both the bridge and the controller.
|
|
4
|
+
*/
|
|
5
|
+
type PendingRequest<R, M> = {
|
|
6
|
+
/** Receives the reply, the timeout result or the fail-all result. */
|
|
7
|
+
settle: (result: R) => void;
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
/** Builds the result delivered when the reply doesn't arrive in time. */
|
|
10
|
+
onTimeout: () => R;
|
|
11
|
+
/** Caller data kept with the request, such as the socket that issued it. */
|
|
12
|
+
meta: M;
|
|
13
|
+
};
|
|
14
|
+
export declare class PendingRequests<R, M = undefined> {
|
|
15
|
+
private readonly entries;
|
|
16
|
+
/**
|
|
17
|
+
* @description Tracks a request until its reply arrives or `timeoutMs` elapses.
|
|
18
|
+
* @param id The correlation id its reply will carry.
|
|
19
|
+
* @param request How to settle it, when to give up and what to keep with it.
|
|
20
|
+
*/
|
|
21
|
+
add(id: string, { settle, timeoutMs, onTimeout, meta }: PendingRequest<R, M>): void;
|
|
22
|
+
/**
|
|
23
|
+
* @description Settles the request with this id, if one is still pending.
|
|
24
|
+
* @param id The reply's correlation id.
|
|
25
|
+
* @param result The result to deliver.
|
|
26
|
+
* @returns Whether a pending request took the result.
|
|
27
|
+
*/
|
|
28
|
+
resolve(id: string, result: R): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* @description Settles every pending request with a failure, so no caller waits forever.
|
|
31
|
+
* @param fail Builds each request's failure result from its id.
|
|
32
|
+
*/
|
|
33
|
+
failAll(fail: (id: string) => R): void;
|
|
34
|
+
/**
|
|
35
|
+
* @description Drops matching requests without settling them, for when nobody is left to answer.
|
|
36
|
+
* @param matches Picks the requests to drop by their meta.
|
|
37
|
+
*/
|
|
38
|
+
discard(matches: (meta: M) => boolean): void;
|
|
39
|
+
/** @description The meta of every pending request, in the order they were added. */
|
|
40
|
+
metas(): M[];
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/pending-requests.ts
|
|
2
|
+
var PendingRequests = class {
|
|
3
|
+
entries = /* @__PURE__ */ new Map();
|
|
4
|
+
/**
|
|
5
|
+
* @description Tracks a request until its reply arrives or `timeoutMs` elapses.
|
|
6
|
+
* @param id The correlation id its reply will carry.
|
|
7
|
+
* @param request How to settle it, when to give up and what to keep with it.
|
|
8
|
+
*/
|
|
9
|
+
add(id, { settle, timeoutMs, onTimeout, meta }) {
|
|
10
|
+
const timer = setTimeout(() => this.resolve(id, onTimeout()), timeoutMs);
|
|
11
|
+
this.entries.set(id, {
|
|
12
|
+
settle,
|
|
13
|
+
timer,
|
|
14
|
+
meta
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @description Settles the request with this id, if one is still pending.
|
|
19
|
+
* @param id The reply's correlation id.
|
|
20
|
+
* @param result The result to deliver.
|
|
21
|
+
* @returns Whether a pending request took the result.
|
|
22
|
+
*/
|
|
23
|
+
resolve(id, result) {
|
|
24
|
+
const entry = this.entries.get(id);
|
|
25
|
+
if (!entry) return false;
|
|
26
|
+
clearTimeout(entry.timer);
|
|
27
|
+
this.entries.delete(id);
|
|
28
|
+
entry.settle(result);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @description Settles every pending request with a failure, so no caller waits forever.
|
|
33
|
+
* @param fail Builds each request's failure result from its id.
|
|
34
|
+
*/
|
|
35
|
+
failAll(fail) {
|
|
36
|
+
for (const id of this.entries.keys()) this.resolve(id, fail(id));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @description Drops matching requests without settling them, for when nobody is left to answer.
|
|
40
|
+
* @param matches Picks the requests to drop by their meta.
|
|
41
|
+
*/
|
|
42
|
+
discard(matches) {
|
|
43
|
+
for (const [id, entry] of this.entries) {
|
|
44
|
+
if (!matches(entry.meta)) continue;
|
|
45
|
+
clearTimeout(entry.timer);
|
|
46
|
+
this.entries.delete(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** @description The meta of every pending request, in the order they were added. */
|
|
50
|
+
metas() {
|
|
51
|
+
return [...this.entries.values()].map((entry) => entry.meta);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
export { PendingRequests };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// src/react-devtools.ts
|
|
2
|
+
/**
|
|
3
|
+
* The event the device sends the frontend, with a {@link ReactDevtoolsStatus}, when its backend
|
|
4
|
+
* starts a session, cannot start one, or ends one.
|
|
5
|
+
*/
|
|
6
|
+
const REACT_DEVTOOLS_STATUS_EVENT = "unbound:backend-status";
|
|
7
|
+
/** React DevTools' own event that ends a backend session; the bridge sends it when the frontend leaves. */
|
|
8
|
+
const REACT_DEVTOOLS_SHUTDOWN_EVENT = "shutdown";
|
|
9
|
+
/**
|
|
10
|
+
* @description Checks the fields of a React DevTools frame whose `type` is already known.
|
|
11
|
+
* @param frame The parsed frame.
|
|
12
|
+
* @returns Whether the frame names an event.
|
|
13
|
+
*/
|
|
14
|
+
function isReactDevtoolsFrame(frame) {
|
|
15
|
+
return typeof frame === "object" && frame !== null && "event" in frame && typeof frame.event === "string";
|
|
16
|
+
}
|
|
17
|
+
// src/views.ts
|
|
18
|
+
const VISIBILITIES = /* @__PURE__ */ new Set([
|
|
19
|
+
"visible",
|
|
20
|
+
"invisible",
|
|
21
|
+
"gone"
|
|
22
|
+
]);
|
|
23
|
+
function isObject(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function isNullableString(value) {
|
|
27
|
+
return value === null || typeof value === "string";
|
|
28
|
+
}
|
|
29
|
+
function isViewFrame(value) {
|
|
30
|
+
return isObject(value) && "x" in value && typeof value.x === "number" && "y" in value && typeof value.y === "number" && "width" in value && typeof value.width === "number" && "height" in value && typeof value.height === "number";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @description Checks that a value has every field a {@link ViewNode} needs.
|
|
34
|
+
* @param value The parsed value.
|
|
35
|
+
* @returns Whether it is a view node.
|
|
36
|
+
*/
|
|
37
|
+
function isViewNode(value) {
|
|
38
|
+
return isObject(value) && "id" in value && typeof value.id === "string" && "className" in value && typeof value.className === "string" && "resourceName" in value && isNullableString(value.resourceName) && "frame" in value && isViewFrame(value.frame) && "visibility" in value && VISIBILITIES.has(value.visibility) && "alpha" in value && (value.alpha === null || typeof value.alpha === "number") && "text" in value && isNullableString(value.text) && "childCount" in value && typeof value.childCount === "number";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* @description Checks the fields of a view finder frame whose `type` is already known.
|
|
42
|
+
* @param frame The parsed frame.
|
|
43
|
+
* @returns Whether the frame is a valid view request, result or highlight.
|
|
44
|
+
*/
|
|
45
|
+
function isViewFrameMessage(frame) {
|
|
46
|
+
if (!isObject(frame) || !("type" in frame)) return false;
|
|
47
|
+
switch (frame.type) {
|
|
48
|
+
case "views-request": return "id" in frame && typeof frame.id === "string" && (!("parent" in frame) || frame.parent === void 0 || typeof frame.parent === "string");
|
|
49
|
+
case "views-result": return "id" in frame && typeof frame.id === "string" && "ok" in frame && (frame.ok === true ? "nodes" in frame && Array.isArray(frame.nodes) && frame.nodes.every(isViewNode) : frame.ok === false && "error" in frame && typeof frame.error === "string");
|
|
50
|
+
case "view-highlight": return "node" in frame && isNullableString(frame.node);
|
|
51
|
+
default: return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// src/index.ts
|
|
55
|
+
/** The default port the bridge listens on and controllers dial. */
|
|
56
|
+
const DEFAULT_BRIDGE_PORT = 9090;
|
|
57
|
+
const DEFAULT_EVAL_TIMEOUT_MS = 1e4;
|
|
58
|
+
const MAX_EVAL_TIMEOUT_MS = 864e5;
|
|
59
|
+
/** How long the bridge waits for the device to reload a pushed addon. */
|
|
60
|
+
const PUSH_TIMEOUT_MS = 6e4;
|
|
61
|
+
/**
|
|
62
|
+
* How much longer a controller waits than the bridge, so the bridge's own timeout reply always
|
|
63
|
+
* arrives before the controller gives up on its own.
|
|
64
|
+
*/
|
|
65
|
+
const BRIDGE_REPLY_GRACE_MS = 5e3;
|
|
66
|
+
/**
|
|
67
|
+
* @description Picks how long an eval may run: the requested timeout when it is a positive number of
|
|
68
|
+
* milliseconds, capped at {@link MAX_EVAL_TIMEOUT_MS}, otherwise {@link DEFAULT_EVAL_TIMEOUT_MS}.
|
|
69
|
+
* @param requested The timeout the controller asked for, if any.
|
|
70
|
+
* @returns The timeout to apply, in milliseconds.
|
|
71
|
+
*/
|
|
72
|
+
function resolveEvalTimeout(requested) {
|
|
73
|
+
return typeof requested === "number" && requested > 0 ? Math.min(requested, MAX_EVAL_TIMEOUT_MS) : DEFAULT_EVAL_TIMEOUT_MS;
|
|
74
|
+
}
|
|
75
|
+
/** The query marker a controller appends to its URL so the bridge routes it as a controller, not a device. */
|
|
76
|
+
const CONTROLLER_ENDPOINT_MARKER = "mcp";
|
|
77
|
+
/**
|
|
78
|
+
* @description Safely parses a raw wire frame into a {@link BridgeMessage}, guarding against invalid
|
|
79
|
+
* JSON and unknown shapes so both ends can trust what they route.
|
|
80
|
+
* @param raw The JSON string received off the socket.
|
|
81
|
+
* @returns The parsed message, or `undefined` if it isn't a recognised frame.
|
|
82
|
+
*/
|
|
83
|
+
function parseMessage(raw) {
|
|
84
|
+
let value;
|
|
85
|
+
try {
|
|
86
|
+
value = JSON.parse(raw);
|
|
87
|
+
} catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
return hasRequiredFields(value) ? value : void 0;
|
|
91
|
+
}
|
|
92
|
+
function hasRequiredFields(frame) {
|
|
93
|
+
if (typeof frame !== "object" || frame === null || !("type" in frame)) return false;
|
|
94
|
+
switch (frame.type) {
|
|
95
|
+
case "log": return "message" in frame && typeof frame.message === "string" && "level" in frame && (frame.level === 1 || frame.level === 2 || frame.level === 3);
|
|
96
|
+
case "eval": return "id" in frame && typeof frame.id === "string" && "code" in frame && typeof frame.code === "string";
|
|
97
|
+
case "eval-result": return "id" in frame && typeof frame.id === "string" && "ok" in frame && (frame.ok === true ? "value" in frame && typeof frame.value === "string" : frame.ok === false && hasError(frame));
|
|
98
|
+
case "plugin-push-result": return "id" in frame && typeof frame.id === "string" && "ok" in frame && (frame.ok === true || frame.ok === false && hasError(frame));
|
|
99
|
+
case "plugin-push": return "id" in frame && typeof frame.id === "string" && "addonId" in frame && typeof frame.addonId === "string" && (!("kind" in frame) || frame.kind === void 0 || frame.kind === "plugin" || frame.kind === "theme") && "bundle" in frame && typeof frame.bundle === "string" && "manifest" in frame && typeof frame.manifest === "object" && frame.manifest !== null && (!("bytecode" in frame) || frame.bytecode === void 0 || isStringRecord(frame.bytecode)) && (!("files" in frame) || frame.files === void 0 || isStringRecord(frame.files));
|
|
100
|
+
case "hello": return "device" in frame && isDeviceInfo(frame.device);
|
|
101
|
+
case "device-status": return "connected" in frame && (frame.connected === false || frame.connected === true && (!("device" in frame) || frame.device === void 0 || isDeviceInfo(frame.device)));
|
|
102
|
+
case "views-request":
|
|
103
|
+
case "views-result":
|
|
104
|
+
case "view-highlight": return isViewFrameMessage(frame);
|
|
105
|
+
case "react-devtools": return isReactDevtoolsFrame(frame);
|
|
106
|
+
default: return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function hasError(frame) {
|
|
110
|
+
return typeof frame === "object" && frame !== null && "error" in frame && typeof frame.error === "string";
|
|
111
|
+
}
|
|
112
|
+
function isDeviceInfo(value) {
|
|
113
|
+
return typeof value === "object" && value !== null && "platform" in value && typeof value.platform === "string" && "discordVersion" in value && typeof value.discordVersion === "string" && "loaderVersion" in value && typeof value.loaderVersion === "string" && "clientVersion" in value && typeof value.clientVersion === "string";
|
|
114
|
+
}
|
|
115
|
+
function isStringRecord(value) {
|
|
116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* @description Serialises a {@link BridgeMessage} into its wire form. Inverse of {@link parseMessage}.
|
|
120
|
+
* @param message The message to frame.
|
|
121
|
+
* @returns The JSON string to send over the socket.
|
|
122
|
+
*/
|
|
123
|
+
function serializeMessage(message) {
|
|
124
|
+
return JSON.stringify(message);
|
|
125
|
+
}
|
|
126
|
+
var src_default = {
|
|
127
|
+
DEFAULT_BRIDGE_PORT,
|
|
128
|
+
DEFAULT_EVAL_TIMEOUT_MS,
|
|
129
|
+
MAX_EVAL_TIMEOUT_MS,
|
|
130
|
+
PUSH_TIMEOUT_MS,
|
|
131
|
+
BRIDGE_REPLY_GRACE_MS,
|
|
132
|
+
CONTROLLER_ENDPOINT_MARKER: "mcp",
|
|
133
|
+
REACT_DEVTOOLS_SHUTDOWN_EVENT,
|
|
134
|
+
REACT_DEVTOOLS_STATUS_EVENT,
|
|
135
|
+
resolveEvalTimeout,
|
|
136
|
+
parseMessage,
|
|
137
|
+
serializeMessage
|
|
138
|
+
};
|
|
139
|
+
export { MAX_EVAL_TIMEOUT_MS as a, resolveEvalTimeout as c, REACT_DEVTOOLS_SHUTDOWN_EVENT as d, REACT_DEVTOOLS_STATUS_EVENT as f, DEFAULT_EVAL_TIMEOUT_MS as i, serializeMessage as l, CONTROLLER_ENDPOINT_MARKER as n, PUSH_TIMEOUT_MS as o, DEFAULT_BRIDGE_PORT as r, parseMessage as s, BRIDGE_REPLY_GRACE_MS as t, src_default as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unbound-app/debugger-protocol",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "The wire protocol and controller client shared by the debugger bridge, the on-device client, and the CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mario P.",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"types": "./dist/controller.d.mts",
|
|
27
27
|
"import": "./dist/controller.mjs"
|
|
28
28
|
},
|
|
29
|
+
"./pending-requests": {
|
|
30
|
+
"types": "./dist/pending-requests.d.mts",
|
|
31
|
+
"import": "./dist/pending-requests.mjs"
|
|
32
|
+
},
|
|
29
33
|
"./registry": {
|
|
30
34
|
"types": "./dist/registry.d.mts",
|
|
31
35
|
"import": "./dist/registry.mjs"
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { PushedManifest } from "@unbound-app/types";
|
|
2
|
-
/**
|
|
3
|
-
* The wire protocol shared by the bridge, the device and controllers: every frame is a JSON-encoded
|
|
4
|
-
* {@link BridgeMessage}. Dependency-free at runtime so the React Native client can bundle it.
|
|
5
|
-
*/
|
|
6
|
-
/** A request to evaluate code on the device, sent controller → bridge → device. */
|
|
7
|
-
type EvalRequest = {
|
|
8
|
-
type: 'eval';
|
|
9
|
-
/** Correlation id echoed back on the matching {@link EvalResult}. */
|
|
10
|
-
id: string;
|
|
11
|
-
code: string;
|
|
12
|
-
timeout?: number;
|
|
13
|
-
};
|
|
14
|
-
/** The outcome of an {@link EvalRequest}, sent device → bridge → the one waiting controller. */
|
|
15
|
-
type EvalResult = {
|
|
16
|
-
type: 'eval-result';
|
|
17
|
-
/** The `id` of the {@link EvalRequest} this answers. */
|
|
18
|
-
id: string;
|
|
19
|
-
/**
|
|
20
|
-
* Console output captured during this eval, so callers see its side effects too. Populated by
|
|
21
|
-
* the controller that owns the request, not sent by the device.
|
|
22
|
-
*/
|
|
23
|
-
logs?: LogMessage[];
|
|
24
|
-
} & ({
|
|
25
|
-
ok: true;
|
|
26
|
-
/** The inspected return value. */
|
|
27
|
-
value: string;
|
|
28
|
-
} | {
|
|
29
|
-
ok: false;
|
|
30
|
-
/** The inspected error. */
|
|
31
|
-
error: string;
|
|
32
|
-
});
|
|
33
|
-
/** The addon managers a push can reload into. */
|
|
34
|
-
type PushedAddonKind = 'plugin' | 'theme';
|
|
35
|
-
/**
|
|
36
|
-
* A request to hot-reload a plugin or theme on the device, sent controller → bridge → device. Carries
|
|
37
|
-
* the freshly built bundle and manifest so the device swaps them in without a re-fetch.
|
|
38
|
-
*/
|
|
39
|
-
type PluginPushRequest = {
|
|
40
|
-
type: 'plugin-push';
|
|
41
|
-
/** Correlation id echoed back on the matching {@link PluginPushResult}. */
|
|
42
|
-
id: string;
|
|
43
|
-
addonId: string;
|
|
44
|
-
/** Which manager reloads the addon; a plugin when omitted, as pushes from older CLIs are. */
|
|
45
|
-
kind?: PushedAddonKind;
|
|
46
|
-
bundle: string;
|
|
47
|
-
/** The manifest as authored; the device validates it before swapping anything in. */
|
|
48
|
-
manifest: PushedManifest;
|
|
49
|
-
/** The bundle compiled to Hermes bytecode, base64-encoded, by bytecode version; its digests are in `manifest.bytecode`. */
|
|
50
|
-
bytecode?: Record<string, string>;
|
|
51
|
-
/** Each extra file the manifest lists beside the bundle, such as an image, base64-encoded, by path. */
|
|
52
|
-
files?: Record<string, string>;
|
|
53
|
-
};
|
|
54
|
-
/** The outcome of a {@link PluginPushRequest}, sent device → bridge → the one waiting controller. */
|
|
55
|
-
type PluginPushResult = {
|
|
56
|
-
type: 'plugin-push-result';
|
|
57
|
-
/** The `id` of the {@link PluginPushRequest} this answers. */
|
|
58
|
-
id: string;
|
|
59
|
-
} & ({
|
|
60
|
-
ok: true;
|
|
61
|
-
} | {
|
|
62
|
-
ok: false;
|
|
63
|
-
/** The reload failure, inspected. */
|
|
64
|
-
error: string;
|
|
65
|
-
});
|
|
66
|
-
/** A `console.*` / `nativeLoggingHook` line, streamed device → bridge → all controllers. */
|
|
67
|
-
type LogMessage = {
|
|
68
|
-
type: 'log';
|
|
69
|
-
/** 1 = log/gray, 2 = warn/yellow, 3 = error/red. */
|
|
70
|
-
level: 1 | 2 | 3;
|
|
71
|
-
message: string;
|
|
72
|
-
};
|
|
73
|
-
/** What a device reports about itself when it connects. */
|
|
74
|
-
type DeviceInfo = {
|
|
75
|
-
platform: string;
|
|
76
|
-
discordVersion: string;
|
|
77
|
-
loaderVersion: string;
|
|
78
|
-
clientVersion: string;
|
|
79
|
-
};
|
|
80
|
-
/** Device → bridge, once its socket opens, so the bridge can say which device is attached. */
|
|
81
|
-
type DeviceHello = {
|
|
82
|
-
type: 'hello';
|
|
83
|
-
device: DeviceInfo;
|
|
84
|
-
};
|
|
85
|
-
/** Bridge → controller, once on attach and whenever the device connects, drops or says hello. */
|
|
86
|
-
type DeviceStatus = {
|
|
87
|
-
type: 'device-status';
|
|
88
|
-
} & ({
|
|
89
|
-
connected: false;
|
|
90
|
-
} | {
|
|
91
|
-
connected: true;
|
|
92
|
-
/** Present once the connected device has said hello. */
|
|
93
|
-
device?: DeviceInfo;
|
|
94
|
-
});
|
|
95
|
-
/** Every framed message on the wire. */
|
|
96
|
-
type BridgeMessage = EvalRequest | EvalResult | PluginPushRequest | PluginPushResult | LogMessage | DeviceHello | DeviceStatus;
|
|
97
|
-
/** The default port the bridge listens on and controllers dial. */
|
|
98
|
-
declare const DEFAULT_BRIDGE_PORT = 9090;
|
|
99
|
-
declare const DEFAULT_EVAL_TIMEOUT_MS = 10000;
|
|
100
|
-
declare const MAX_EVAL_TIMEOUT_MS = 86400000;
|
|
101
|
-
/** The query marker a controller appends to its URL so the bridge routes it as a controller, not a device. */
|
|
102
|
-
declare const CONTROLLER_ENDPOINT_MARKER = "mcp";
|
|
103
|
-
/**
|
|
104
|
-
* @description Safely parses a raw wire frame into a {@link BridgeMessage}, guarding against invalid
|
|
105
|
-
* JSON and unknown shapes so both ends can trust what they route.
|
|
106
|
-
* @param raw The JSON string received off the socket.
|
|
107
|
-
* @returns The parsed message, or `undefined` if it isn't a recognised frame.
|
|
108
|
-
*/
|
|
109
|
-
declare function parseMessage(raw: string): BridgeMessage | undefined;
|
|
110
|
-
/**
|
|
111
|
-
* @description Serialises a {@link BridgeMessage} into its wire form. Inverse of {@link parseMessage}.
|
|
112
|
-
* @param message The message to frame.
|
|
113
|
-
* @returns The JSON string to send over the socket.
|
|
114
|
-
*/
|
|
115
|
-
declare function serializeMessage(message: BridgeMessage): string;
|
|
116
|
-
declare const _default: {
|
|
117
|
-
DEFAULT_BRIDGE_PORT: number;
|
|
118
|
-
DEFAULT_EVAL_TIMEOUT_MS: number;
|
|
119
|
-
MAX_EVAL_TIMEOUT_MS: number;
|
|
120
|
-
CONTROLLER_ENDPOINT_MARKER: string;
|
|
121
|
-
parseMessage: typeof parseMessage;
|
|
122
|
-
serializeMessage: typeof serializeMessage;
|
|
123
|
-
};
|
|
124
|
-
export { serializeMessage as _, DeviceHello as a, EvalRequest as c, MAX_EVAL_TIMEOUT_MS as d, PluginPushRequest as f, parseMessage as g, _default as h, DEFAULT_EVAL_TIMEOUT_MS as i, EvalResult as l, PushedAddonKind as m, CONTROLLER_ENDPOINT_MARKER as n, DeviceInfo as o, PluginPushResult as p, DEFAULT_BRIDGE_PORT as r, DeviceStatus as s, BridgeMessage as t, LogMessage as u };
|