@unbound-app/debugger-protocol 0.1.0 → 0.4.0

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.
@@ -1,8 +1,15 @@
1
- import { o as EvalResult } from "./index-DA_F_n3N.mjs";
1
+ import { l as PluginPushResult, o as EvalResult } from "./index-CMW4XGZB.mjs";
2
+ import { AddonManifest } from "@unbound-app/types";
2
3
  //#region src/controller.d.ts
4
+ type PluginPush = {
5
+ addonId: string;
6
+ bundle: string;
7
+ manifest: AddonManifest;
8
+ };
3
9
  declare class ControllerClient {
4
10
  private ws;
5
11
  private readonly pending;
12
+ private readonly pendingPushes;
6
13
  private deviceConnected;
7
14
  private closed;
8
15
  private readonly url;
@@ -20,6 +27,13 @@ declare class ControllerClient {
20
27
  * @returns The device's {@link EvalResult}.
21
28
  */
22
29
  evaluate(code: string): Promise<EvalResult>;
30
+ /**
31
+ * @description Pushes a freshly built plugin to the device via the bridge and resolves with the
32
+ * device's reload outcome. Fails fast if the socket isn't open or the bridge doesn't answer in time.
33
+ * @param push The addon id plus the built bundle and manifest to swap in.
34
+ * @returns The device's {@link PluginPushResult}.
35
+ */
36
+ pushPlugin(push: PluginPush): Promise<PluginPushResult>;
23
37
  private onMessage;
24
38
  private onClose;
25
39
  }
@@ -7,6 +7,7 @@ const RECONNECT_DELAY_MS = 1e3;
7
7
  var ControllerClient = class {
8
8
  ws = null;
9
9
  pending = /* @__PURE__ */ new Map();
10
+ pendingPushes = /* @__PURE__ */ new Map();
10
11
  deviceConnected = false;
11
12
  closed = false;
12
13
  url;
@@ -35,7 +36,17 @@ var ControllerClient = class {
35
36
  logs: entry.logs
36
37
  });
37
38
  }
39
+ for (const [id, entry] of this.pendingPushes) {
40
+ clearTimeout(entry.timer);
41
+ entry.resolve({
42
+ type: "plugin-push-result",
43
+ id,
44
+ ok: false,
45
+ error: "Debugger client shut down."
46
+ });
47
+ }
38
48
  this.pending.clear();
49
+ this.pendingPushes.clear();
39
50
  }
40
51
  /** @description Whether a device is currently connected to the bridge. */
41
52
  get isDeviceConnected() {
@@ -82,6 +93,44 @@ var ControllerClient = class {
82
93
  }));
83
94
  });
84
95
  }
96
+ /**
97
+ * @description Pushes a freshly built plugin to the device via the bridge and resolves with the
98
+ * device's reload outcome. Fails fast if the socket isn't open or the bridge doesn't answer in time.
99
+ * @param push The addon id plus the built bundle and manifest to swap in.
100
+ * @returns The device's {@link PluginPushResult}.
101
+ */
102
+ pushPlugin(push) {
103
+ const id = crypto.randomUUID();
104
+ return new Promise((resolve) => {
105
+ if (this.ws?.readyState !== WebSocket.OPEN) {
106
+ resolve({
107
+ type: "plugin-push-result",
108
+ id,
109
+ ok: false,
110
+ error: "Not connected to the debugger bridge."
111
+ });
112
+ return;
113
+ }
114
+ const timer = setTimeout(() => {
115
+ this.pendingPushes.delete(id);
116
+ resolve({
117
+ type: "plugin-push-result",
118
+ id,
119
+ ok: false,
120
+ error: `Plugin push timed out after ${EVAL_TIMEOUT_MS}ms.`
121
+ });
122
+ }, EVAL_TIMEOUT_MS);
123
+ this.pendingPushes.set(id, {
124
+ resolve,
125
+ timer
126
+ });
127
+ this.ws.send(serializeMessage({
128
+ type: "plugin-push",
129
+ id,
130
+ ...push
131
+ }));
132
+ });
133
+ }
85
134
  onMessage(raw) {
86
135
  const message = parseMessage(raw);
87
136
  if (!message) return;
@@ -102,6 +151,14 @@ var ControllerClient = class {
102
151
  ...message,
103
152
  logs: entry.logs
104
153
  });
154
+ return;
155
+ }
156
+ if (message.type === "plugin-push-result") {
157
+ const entry = this.pendingPushes.get(message.id);
158
+ if (!entry) return;
159
+ clearTimeout(entry.timer);
160
+ this.pendingPushes.delete(message.id);
161
+ entry.resolve(message);
105
162
  }
106
163
  }
107
164
  onClose() {
@@ -1,14 +1,16 @@
1
+ import { AddonManifest } from "@unbound-app/types";
1
2
  //#region src/index.d.ts
2
3
  /**
3
4
  * The wire protocol shared by the bridge, the on-device debugger, and any controller (the terminal
4
5
  * REPL or an MCP client). Everything crossing a socket is a JSON-encoded {@link BridgeMessage}.
5
6
  *
6
7
  * Two planes travel over the single device socket:
7
- * - control: a controller issues an {@link EvalRequest}; the device answers with the matching
8
- * {@link EvalResult} (correlated by `id`).
8
+ * - control: a controller issues an {@link EvalRequest} or {@link PluginPushRequest}; the device
9
+ * answers with the matching {@link EvalResult} / {@link PluginPushResult} (correlated by `id`).
9
10
  * - telemetry: the device streams {@link LogMessage}s, broadcast to every controller.
10
11
  *
11
- * Device-safe and dependency-free so it can be bundled into the React Native client.
12
+ * Device-safe and dependency-free at runtime (the manifest type is import-erased) so it can be
13
+ * bundled into the React Native client.
12
14
  */
13
15
  /** A request to evaluate code on the device, sent controller → bridge → device. */
14
16
  interface EvalRequest {
@@ -34,6 +36,27 @@ interface EvalResult {
34
36
  */
35
37
  logs?: LogMessage[];
36
38
  }
39
+ /**
40
+ * A request to hot-reload a plugin on the device, sent controller → bridge → device. Carries the
41
+ * freshly built bundle and manifest so the device swaps them in without a re-fetch.
42
+ */
43
+ interface PluginPushRequest {
44
+ type: 'plugin-push';
45
+ /** Correlation id echoed back on the matching {@link PluginPushResult}. */
46
+ id: string;
47
+ addonId: string;
48
+ bundle: string;
49
+ manifest: AddonManifest;
50
+ }
51
+ /** The outcome of a {@link PluginPushRequest}, sent device → bridge → the one waiting controller. */
52
+ interface PluginPushResult {
53
+ type: 'plugin-push-result';
54
+ /** The `id` of the {@link PluginPushRequest} this answers. */
55
+ id: string;
56
+ ok: boolean;
57
+ /** Present when `!ok`; the reload failure, inspected. */
58
+ error?: string;
59
+ }
37
60
  /** A `console.*` / `nativeLoggingHook` line, streamed device → bridge → all controllers. */
38
61
  interface LogMessage {
39
62
  type: 'log';
@@ -47,7 +70,7 @@ interface DeviceStatus {
47
70
  connected: boolean;
48
71
  }
49
72
  /** Every framed message on the wire. */
50
- type BridgeMessage = EvalRequest | EvalResult | LogMessage | DeviceStatus;
73
+ type BridgeMessage = EvalRequest | EvalResult | PluginPushRequest | PluginPushResult | LogMessage | DeviceStatus;
51
74
  /** The default port the bridge listens on and controllers dial. */
52
75
  declare const DEFAULT_BRIDGE_PORT = 9090;
53
76
  /** The query marker a controller appends to its URL so the bridge routes it as a controller, not a device. */
@@ -72,4 +95,4 @@ declare const _default: {
72
95
  serializeMessage: typeof serializeMessage;
73
96
  };
74
97
  //#endregion
75
- export { EvalRequest as a, _default as c, DeviceStatus as i, parseMessage as l, CONTROLLER_ENDPOINT_MARKER as n, EvalResult as o, DEFAULT_BRIDGE_PORT as r, LogMessage as s, BridgeMessage as t, serializeMessage as u };
98
+ export { EvalRequest as a, PluginPushRequest as c, parseMessage as d, serializeMessage as f, DeviceStatus as i, PluginPushResult as l, CONTROLLER_ENDPOINT_MARKER as n, EvalResult as o, DEFAULT_BRIDGE_PORT as r, LogMessage as s, BridgeMessage as t, _default as u };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as EvalRequest, c as _default, i as DeviceStatus, l as parseMessage, n as CONTROLLER_ENDPOINT_MARKER, o as EvalResult, r as DEFAULT_BRIDGE_PORT, s as LogMessage, t as BridgeMessage, u as serializeMessage } from "./index-DA_F_n3N.mjs";
2
- export { BridgeMessage, CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, DeviceStatus, EvalRequest, EvalResult, LogMessage, _default as default, parseMessage, serializeMessage };
1
+ import { a as EvalRequest, c as PluginPushRequest, d as parseMessage, f as serializeMessage, i as DeviceStatus, l as PluginPushResult, n as CONTROLLER_ENDPOINT_MARKER, o as EvalResult, r as DEFAULT_BRIDGE_PORT, s as LogMessage, t as BridgeMessage, u as _default } from "./index-CMW4XGZB.mjs";
2
+ export { BridgeMessage, CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, DeviceStatus, EvalRequest, EvalResult, LogMessage, PluginPushRequest, PluginPushResult, _default as default, parseMessage, serializeMessage };
package/dist/index.mjs CHANGED
@@ -6,6 +6,8 @@ const CONTROLLER_ENDPOINT_MARKER = "mcp";
6
6
  const MESSAGE_TYPES = /* @__PURE__ */ new Set([
7
7
  "eval",
8
8
  "eval-result",
9
+ "plugin-push",
10
+ "plugin-push-result",
9
11
  "log",
10
12
  "device-status"
11
13
  ]);
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@unbound-app/debugger-protocol",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
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.",
7
7
  "email": "contact@marioparaschiv.com",
8
8
  "url": "https://github.com/marioparaschiv"
9
9
  },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/marioparaschiv/unbound.git",
13
+ "directory": "packages/debugger-protocol"
14
+ },
10
15
  "files": [
11
16
  "dist"
12
17
  ],
@@ -30,11 +35,9 @@
30
35
  "build": "tsdown"
31
36
  },
32
37
  "dependencies": {
38
+ "@unbound-app/types": "0.4.0",
33
39
  "zod": "^4.0.0"
34
40
  },
35
- "devDependencies": {
36
- "tsdown": "^0.22.9"
37
- },
38
41
  "peerDependencies": {
39
42
  "typescript": "^5.9.3"
40
43
  }