@rainbow-robotics/plugin-ui 1.10.1-dev.1 → 1.10.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/README.md +246 -246
- package/build-dist.mjs +80 -80
- package/dist/host-ui.js.map +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/react.d.ts +12 -12
- package/dist/react.js.map +1 -1
- package/dist/sdk.js.map +1 -1
- package/dist/viteHost.js.map +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/host-ui.ts +95 -95
- package/src/host.tsx +277 -277
- package/src/index.d.ts +1 -1
- package/src/manifest.ts +135 -135
- package/src/pluginData.tsx +240 -240
- package/src/react.tsx +13 -13
- package/src/sdk.ts +175 -175
- package/src/viteHost.ts +309 -309
- package/src/ws.ts +256 -256
package/src/ws.ts
CHANGED
|
@@ -1,256 +1,256 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Socket.IO communication client for plugin frontends.
|
|
3
|
-
*
|
|
4
|
-
* Topics are automatically namespaced to prevent cross-plugin collisions:
|
|
5
|
-
* "sensor/data" → "{pluginId}/sensor/data"
|
|
6
|
-
* "/broadcast/x" → "broadcast/x" (leading slash = absolute)
|
|
7
|
-
*
|
|
8
|
-
* Browser ←→ Python backend protocol:
|
|
9
|
-
* browser emits : sub, unsub, pub, action (with ack), serve_reply
|
|
10
|
-
* server emits : msg, serve_call
|
|
11
|
-
*/
|
|
12
|
-
import { io, type Socket } from "socket.io-client";
|
|
13
|
-
// Type-only — no runtime coupling to host-ui.ts.
|
|
14
|
-
import type { PluginUIServices } from "./host-ui";
|
|
15
|
-
|
|
16
|
-
export interface WsHandle {
|
|
17
|
-
undeclare(): void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type WsSubscriber = WsHandle;
|
|
21
|
-
export type WsQueryable = WsHandle;
|
|
22
|
-
|
|
23
|
-
function resolveTopic(pluginId: string, topic: string): string {
|
|
24
|
-
return topic.startsWith("/") ? topic.slice(1) : `${pluginId}/${topic}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class RbWs {
|
|
28
|
-
private readonly _socket: Socket;
|
|
29
|
-
private readonly _pluginId: string;
|
|
30
|
-
private readonly _subHandlers = new Map<
|
|
31
|
-
string,
|
|
32
|
-
Set<(payload: unknown) => void>
|
|
33
|
-
>();
|
|
34
|
-
private readonly _serveHandlers = new Map<
|
|
35
|
-
string,
|
|
36
|
-
(payload?: unknown) => unknown | Promise<unknown>
|
|
37
|
-
>();
|
|
38
|
-
|
|
39
|
-
private constructor(socket: Socket, pluginId: string) {
|
|
40
|
-
this._socket = socket;
|
|
41
|
-
this._pluginId = pluginId;
|
|
42
|
-
|
|
43
|
-
// The gateway keeps subscriptions/serve registrations in memory — re-send them after
|
|
44
|
-
// a reconnect (e.g. the gateway restarted), or they silently stop working.
|
|
45
|
-
socket.io.on("reconnect", () => {
|
|
46
|
-
for (const topic of this._subHandlers.keys()) socket.emit("sub", { topic });
|
|
47
|
-
for (const topic of this._serveHandlers.keys()) socket.emit("serve_register", { topic });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Deliver incoming pub/sub messages to registered handlers
|
|
51
|
-
socket.on(
|
|
52
|
-
"msg",
|
|
53
|
-
({ topic, payload }: { topic: string; payload: unknown }) => {
|
|
54
|
-
this._subHandlers.get(topic)?.forEach((h) => h(payload));
|
|
55
|
-
},
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
// Python calls a browser-side serve handler
|
|
59
|
-
socket.on(
|
|
60
|
-
"serve_call",
|
|
61
|
-
async ({
|
|
62
|
-
id,
|
|
63
|
-
topic,
|
|
64
|
-
payload,
|
|
65
|
-
}: {
|
|
66
|
-
id: string;
|
|
67
|
-
topic: string;
|
|
68
|
-
payload: unknown;
|
|
69
|
-
}) => {
|
|
70
|
-
const handler = this._serveHandlers.get(topic);
|
|
71
|
-
const result = handler ? await handler(payload) : undefined;
|
|
72
|
-
socket.emit("serve_reply", { id, payload: result });
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
get pluginId(): string {
|
|
78
|
-
return this._pluginId;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
static connect(url: string, pluginId: string): Promise<RbWs> {
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
const socket = io(url, { transports: ["websocket"] });
|
|
84
|
-
const onConnect = () => {
|
|
85
|
-
cleanup();
|
|
86
|
-
resolve(new RbWs(socket, pluginId));
|
|
87
|
-
};
|
|
88
|
-
const onError = (err: Error) => {
|
|
89
|
-
cleanup();
|
|
90
|
-
reject(err);
|
|
91
|
-
};
|
|
92
|
-
const cleanup = () => {
|
|
93
|
-
socket.off("connect", onConnect);
|
|
94
|
-
socket.off("connect_error", onError);
|
|
95
|
-
};
|
|
96
|
-
socket.once("connect", onConnect);
|
|
97
|
-
socket.once("connect_error", onError);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private _key(topic: string): string {
|
|
102
|
-
return resolveTopic(this._pluginId, topic);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
-
async pub<P = any>(topic: string, payload: P): Promise<void> {
|
|
107
|
-
this._socket.emit("pub", { topic: this._key(topic), payload });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
-
async sub<P = any>(
|
|
112
|
-
topic: string,
|
|
113
|
-
handler: (payload: P) => void,
|
|
114
|
-
): Promise<WsSubscriber> {
|
|
115
|
-
const key = this._key(topic);
|
|
116
|
-
if (!this._subHandlers.has(key)) {
|
|
117
|
-
this._subHandlers.set(key, new Set());
|
|
118
|
-
this._socket.emit("sub", { topic: key });
|
|
119
|
-
}
|
|
120
|
-
const h = handler as (payload: unknown) => void;
|
|
121
|
-
this._subHandlers.get(key)!.add(h);
|
|
122
|
-
return {
|
|
123
|
-
undeclare: () => {
|
|
124
|
-
const set = this._subHandlers.get(key);
|
|
125
|
-
if (!set) return;
|
|
126
|
-
set.delete(h);
|
|
127
|
-
if (set.size === 0) {
|
|
128
|
-
this._subHandlers.delete(key);
|
|
129
|
-
this._socket.emit("unsub", { topic: key });
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
-
async action<R = any, P = any>(topic: string, payload?: P): Promise<R> {
|
|
137
|
-
const key = this._key(topic);
|
|
138
|
-
return new Promise((resolve, reject) => {
|
|
139
|
-
this._socket
|
|
140
|
-
.timeout(10_000)
|
|
141
|
-
.emit(
|
|
142
|
-
"action",
|
|
143
|
-
{ topic: key, payload },
|
|
144
|
-
(err: Error | null, result: R) => {
|
|
145
|
-
if (err) {
|
|
146
|
-
// socket.io ack timeout — the bridge itself never answered.
|
|
147
|
-
reject(new Error(`action "${key}": no ack from bridge (${err.message})`));
|
|
148
|
-
} else if (
|
|
149
|
-
result &&
|
|
150
|
-
typeof result === "object" &&
|
|
151
|
-
"error" in result &&
|
|
152
|
-
typeof result.error === "string"
|
|
153
|
-
) {
|
|
154
|
-
// Backend handler returned an error payload — surface it verbatim.
|
|
155
|
-
reject(new Error(result.error));
|
|
156
|
-
} else if (result === undefined || result === null) {
|
|
157
|
-
// The bridge acked but with no payload: the backend handler is
|
|
158
|
-
// registered yet didn't reply in time (hung inside the handler,
|
|
159
|
-
// e.g. a blocked SDK call) or returned nothing. Distinct from a
|
|
160
|
-
// "No handler registered" error, which comes back as result.error.
|
|
161
|
-
reject(
|
|
162
|
-
new Error(
|
|
163
|
-
`action "${key}": no reply from plugin backend — the handler was reached but didn't return in time (check the backend's stdout on the robot).`,
|
|
164
|
-
),
|
|
165
|
-
);
|
|
166
|
-
} else {
|
|
167
|
-
resolve(result);
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
175
|
-
async serve<P = any, R = any>(
|
|
176
|
-
topic: string,
|
|
177
|
-
handler: (payload?: P) => R | Promise<R>,
|
|
178
|
-
): Promise<WsQueryable> {
|
|
179
|
-
const key = this._key(topic);
|
|
180
|
-
this._serveHandlers.set(key, handler as (payload?: unknown) => unknown);
|
|
181
|
-
this._socket.emit("serve_register", { topic: key });
|
|
182
|
-
return {
|
|
183
|
-
undeclare: () => {
|
|
184
|
-
this._serveHandlers.delete(key);
|
|
185
|
-
this._socket.emit("serve_unregister", { topic: key });
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
close(): void {
|
|
191
|
-
this._socket.disconnect();
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Builds an rb-sdk-shaped object (pub/sub/action/serve + the passed-through
|
|
197
|
-
* toast/alert/confirm/prompt) that lazily awaits a not-yet-connected `RbWs` —
|
|
198
|
-
* so callers don't block their first render on the WebSocket handshake (there
|
|
199
|
-
* may be no backend reachable yet). Shared by both host mount paths (see
|
|
200
|
-
* ./host.tsx and muscat-plugin-kit's dev bootstrap), which otherwise
|
|
201
|
-
* duplicate this wiring.
|
|
202
|
-
*/
|
|
203
|
-
export function createLazyRbSdk(
|
|
204
|
-
wsPromise: Promise<RbWs>,
|
|
205
|
-
services: PluginUIServices,
|
|
206
|
-
// Per-mount, host-provided (unlike `services`, which is a global singleton):
|
|
207
|
-
// initial `data` and the `onSave` registration the host reads back. Plain
|
|
208
|
-
// values/closures, so they cross the dev React-instance boundary safely.
|
|
209
|
-
mount?: {
|
|
210
|
-
data?: unknown;
|
|
211
|
-
onSave?: (getPayload: () => unknown) => () => void;
|
|
212
|
-
},
|
|
213
|
-
) {
|
|
214
|
-
return {
|
|
215
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
-
pub: async (topic: string, payload: any) =>
|
|
217
|
-
(await wsPromise).pub(topic, payload),
|
|
218
|
-
sub: async (
|
|
219
|
-
topic: string,
|
|
220
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
221
|
-
handler: (payload: any) => void,
|
|
222
|
-
) => (await wsPromise).sub(topic, handler),
|
|
223
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
224
|
-
action: async (topic: string, payload?: any) =>
|
|
225
|
-
(await wsPromise).action(topic, payload),
|
|
226
|
-
serve: async (
|
|
227
|
-
topic: string,
|
|
228
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
229
|
-
handler: (payload?: any) => any,
|
|
230
|
-
) => (await wsPromise).serve(topic, handler),
|
|
231
|
-
toast: services.toast,
|
|
232
|
-
alert: services.alert,
|
|
233
|
-
confirm: services.confirm,
|
|
234
|
-
prompt: services.prompt,
|
|
235
|
-
program: services.program,
|
|
236
|
-
data: mount?.data,
|
|
237
|
-
onSave: mount?.onSave ?? (() => () => {}),
|
|
238
|
-
rosPeers: rosPeersApi(async (op, payload) => {
|
|
239
|
-
const ws = await wsPromise;
|
|
240
|
-
return ws.action(`/muscat/plugin/${ws.pluginId}/ros_peers/${op}`, payload);
|
|
241
|
-
}),
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Answered by Muscat (common) on the robot and by `muscat-pk dev` locally.
|
|
246
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
|
-
function rosPeersApi(call: (op: "get" | "set", payload?: any) => Promise<any>) {
|
|
248
|
-
return {
|
|
249
|
-
get: async (): Promise<string[]> => (await call("get"))?.peers ?? [],
|
|
250
|
-
set: async (peers: string[]): Promise<string[]> => {
|
|
251
|
-
const r = await call("set", { peers });
|
|
252
|
-
if (!r?.ok) throw new Error(r?.error ?? "rb.rosPeers.set failed");
|
|
253
|
-
return r.peers;
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Socket.IO communication client for plugin frontends.
|
|
3
|
+
*
|
|
4
|
+
* Topics are automatically namespaced to prevent cross-plugin collisions:
|
|
5
|
+
* "sensor/data" → "{pluginId}/sensor/data"
|
|
6
|
+
* "/broadcast/x" → "broadcast/x" (leading slash = absolute)
|
|
7
|
+
*
|
|
8
|
+
* Browser ←→ Python backend protocol:
|
|
9
|
+
* browser emits : sub, unsub, pub, action (with ack), serve_reply
|
|
10
|
+
* server emits : msg, serve_call
|
|
11
|
+
*/
|
|
12
|
+
import { io, type Socket } from "socket.io-client";
|
|
13
|
+
// Type-only — no runtime coupling to host-ui.ts.
|
|
14
|
+
import type { PluginUIServices } from "./host-ui";
|
|
15
|
+
|
|
16
|
+
export interface WsHandle {
|
|
17
|
+
undeclare(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type WsSubscriber = WsHandle;
|
|
21
|
+
export type WsQueryable = WsHandle;
|
|
22
|
+
|
|
23
|
+
function resolveTopic(pluginId: string, topic: string): string {
|
|
24
|
+
return topic.startsWith("/") ? topic.slice(1) : `${pluginId}/${topic}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class RbWs {
|
|
28
|
+
private readonly _socket: Socket;
|
|
29
|
+
private readonly _pluginId: string;
|
|
30
|
+
private readonly _subHandlers = new Map<
|
|
31
|
+
string,
|
|
32
|
+
Set<(payload: unknown) => void>
|
|
33
|
+
>();
|
|
34
|
+
private readonly _serveHandlers = new Map<
|
|
35
|
+
string,
|
|
36
|
+
(payload?: unknown) => unknown | Promise<unknown>
|
|
37
|
+
>();
|
|
38
|
+
|
|
39
|
+
private constructor(socket: Socket, pluginId: string) {
|
|
40
|
+
this._socket = socket;
|
|
41
|
+
this._pluginId = pluginId;
|
|
42
|
+
|
|
43
|
+
// The gateway keeps subscriptions/serve registrations in memory — re-send them after
|
|
44
|
+
// a reconnect (e.g. the gateway restarted), or they silently stop working.
|
|
45
|
+
socket.io.on("reconnect", () => {
|
|
46
|
+
for (const topic of this._subHandlers.keys()) socket.emit("sub", { topic });
|
|
47
|
+
for (const topic of this._serveHandlers.keys()) socket.emit("serve_register", { topic });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Deliver incoming pub/sub messages to registered handlers
|
|
51
|
+
socket.on(
|
|
52
|
+
"msg",
|
|
53
|
+
({ topic, payload }: { topic: string; payload: unknown }) => {
|
|
54
|
+
this._subHandlers.get(topic)?.forEach((h) => h(payload));
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Python calls a browser-side serve handler
|
|
59
|
+
socket.on(
|
|
60
|
+
"serve_call",
|
|
61
|
+
async ({
|
|
62
|
+
id,
|
|
63
|
+
topic,
|
|
64
|
+
payload,
|
|
65
|
+
}: {
|
|
66
|
+
id: string;
|
|
67
|
+
topic: string;
|
|
68
|
+
payload: unknown;
|
|
69
|
+
}) => {
|
|
70
|
+
const handler = this._serveHandlers.get(topic);
|
|
71
|
+
const result = handler ? await handler(payload) : undefined;
|
|
72
|
+
socket.emit("serve_reply", { id, payload: result });
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get pluginId(): string {
|
|
78
|
+
return this._pluginId;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static connect(url: string, pluginId: string): Promise<RbWs> {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const socket = io(url, { transports: ["websocket"] });
|
|
84
|
+
const onConnect = () => {
|
|
85
|
+
cleanup();
|
|
86
|
+
resolve(new RbWs(socket, pluginId));
|
|
87
|
+
};
|
|
88
|
+
const onError = (err: Error) => {
|
|
89
|
+
cleanup();
|
|
90
|
+
reject(err);
|
|
91
|
+
};
|
|
92
|
+
const cleanup = () => {
|
|
93
|
+
socket.off("connect", onConnect);
|
|
94
|
+
socket.off("connect_error", onError);
|
|
95
|
+
};
|
|
96
|
+
socket.once("connect", onConnect);
|
|
97
|
+
socket.once("connect_error", onError);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private _key(topic: string): string {
|
|
102
|
+
return resolveTopic(this._pluginId, topic);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
async pub<P = any>(topic: string, payload: P): Promise<void> {
|
|
107
|
+
this._socket.emit("pub", { topic: this._key(topic), payload });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
async sub<P = any>(
|
|
112
|
+
topic: string,
|
|
113
|
+
handler: (payload: P) => void,
|
|
114
|
+
): Promise<WsSubscriber> {
|
|
115
|
+
const key = this._key(topic);
|
|
116
|
+
if (!this._subHandlers.has(key)) {
|
|
117
|
+
this._subHandlers.set(key, new Set());
|
|
118
|
+
this._socket.emit("sub", { topic: key });
|
|
119
|
+
}
|
|
120
|
+
const h = handler as (payload: unknown) => void;
|
|
121
|
+
this._subHandlers.get(key)!.add(h);
|
|
122
|
+
return {
|
|
123
|
+
undeclare: () => {
|
|
124
|
+
const set = this._subHandlers.get(key);
|
|
125
|
+
if (!set) return;
|
|
126
|
+
set.delete(h);
|
|
127
|
+
if (set.size === 0) {
|
|
128
|
+
this._subHandlers.delete(key);
|
|
129
|
+
this._socket.emit("unsub", { topic: key });
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
async action<R = any, P = any>(topic: string, payload?: P): Promise<R> {
|
|
137
|
+
const key = this._key(topic);
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
this._socket
|
|
140
|
+
.timeout(10_000)
|
|
141
|
+
.emit(
|
|
142
|
+
"action",
|
|
143
|
+
{ topic: key, payload },
|
|
144
|
+
(err: Error | null, result: R) => {
|
|
145
|
+
if (err) {
|
|
146
|
+
// socket.io ack timeout — the bridge itself never answered.
|
|
147
|
+
reject(new Error(`action "${key}": no ack from bridge (${err.message})`));
|
|
148
|
+
} else if (
|
|
149
|
+
result &&
|
|
150
|
+
typeof result === "object" &&
|
|
151
|
+
"error" in result &&
|
|
152
|
+
typeof result.error === "string"
|
|
153
|
+
) {
|
|
154
|
+
// Backend handler returned an error payload — surface it verbatim.
|
|
155
|
+
reject(new Error(result.error));
|
|
156
|
+
} else if (result === undefined || result === null) {
|
|
157
|
+
// The bridge acked but with no payload: the backend handler is
|
|
158
|
+
// registered yet didn't reply in time (hung inside the handler,
|
|
159
|
+
// e.g. a blocked SDK call) or returned nothing. Distinct from a
|
|
160
|
+
// "No handler registered" error, which comes back as result.error.
|
|
161
|
+
reject(
|
|
162
|
+
new Error(
|
|
163
|
+
`action "${key}": no reply from plugin backend — the handler was reached but didn't return in time (check the backend's stdout on the robot).`,
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
resolve(result);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
175
|
+
async serve<P = any, R = any>(
|
|
176
|
+
topic: string,
|
|
177
|
+
handler: (payload?: P) => R | Promise<R>,
|
|
178
|
+
): Promise<WsQueryable> {
|
|
179
|
+
const key = this._key(topic);
|
|
180
|
+
this._serveHandlers.set(key, handler as (payload?: unknown) => unknown);
|
|
181
|
+
this._socket.emit("serve_register", { topic: key });
|
|
182
|
+
return {
|
|
183
|
+
undeclare: () => {
|
|
184
|
+
this._serveHandlers.delete(key);
|
|
185
|
+
this._socket.emit("serve_unregister", { topic: key });
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
close(): void {
|
|
191
|
+
this._socket.disconnect();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Builds an rb-sdk-shaped object (pub/sub/action/serve + the passed-through
|
|
197
|
+
* toast/alert/confirm/prompt) that lazily awaits a not-yet-connected `RbWs` —
|
|
198
|
+
* so callers don't block their first render on the WebSocket handshake (there
|
|
199
|
+
* may be no backend reachable yet). Shared by both host mount paths (see
|
|
200
|
+
* ./host.tsx and muscat-plugin-kit's dev bootstrap), which otherwise
|
|
201
|
+
* duplicate this wiring.
|
|
202
|
+
*/
|
|
203
|
+
export function createLazyRbSdk(
|
|
204
|
+
wsPromise: Promise<RbWs>,
|
|
205
|
+
services: PluginUIServices,
|
|
206
|
+
// Per-mount, host-provided (unlike `services`, which is a global singleton):
|
|
207
|
+
// initial `data` and the `onSave` registration the host reads back. Plain
|
|
208
|
+
// values/closures, so they cross the dev React-instance boundary safely.
|
|
209
|
+
mount?: {
|
|
210
|
+
data?: unknown;
|
|
211
|
+
onSave?: (getPayload: () => unknown) => () => void;
|
|
212
|
+
},
|
|
213
|
+
) {
|
|
214
|
+
return {
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
+
pub: async (topic: string, payload: any) =>
|
|
217
|
+
(await wsPromise).pub(topic, payload),
|
|
218
|
+
sub: async (
|
|
219
|
+
topic: string,
|
|
220
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
221
|
+
handler: (payload: any) => void,
|
|
222
|
+
) => (await wsPromise).sub(topic, handler),
|
|
223
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
224
|
+
action: async (topic: string, payload?: any) =>
|
|
225
|
+
(await wsPromise).action(topic, payload),
|
|
226
|
+
serve: async (
|
|
227
|
+
topic: string,
|
|
228
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
229
|
+
handler: (payload?: any) => any,
|
|
230
|
+
) => (await wsPromise).serve(topic, handler),
|
|
231
|
+
toast: services.toast,
|
|
232
|
+
alert: services.alert,
|
|
233
|
+
confirm: services.confirm,
|
|
234
|
+
prompt: services.prompt,
|
|
235
|
+
program: services.program,
|
|
236
|
+
data: mount?.data,
|
|
237
|
+
onSave: mount?.onSave ?? (() => () => {}),
|
|
238
|
+
rosPeers: rosPeersApi(async (op, payload) => {
|
|
239
|
+
const ws = await wsPromise;
|
|
240
|
+
return ws.action(`/muscat/plugin/${ws.pluginId}/ros_peers/${op}`, payload);
|
|
241
|
+
}),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Answered by Muscat (common) on the robot and by `muscat-pk dev` locally.
|
|
246
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
|
+
function rosPeersApi(call: (op: "get" | "set", payload?: any) => Promise<any>) {
|
|
248
|
+
return {
|
|
249
|
+
get: async (): Promise<string[]> => (await call("get"))?.peers ?? [],
|
|
250
|
+
set: async (peers: string[]): Promise<string[]> => {
|
|
251
|
+
const r = await call("set", { peers });
|
|
252
|
+
if (!r?.ok) throw new Error(r?.error ?? "rb.rosPeers.set failed");
|
|
253
|
+
return r.peers;
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|