@rainbow-robotics/plugin-ui 0.1.2-dev.5 → 0.1.2-dev.6
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/build-browser.mjs +39 -1
- package/dist/connect.d.ts +2 -3
- package/dist/connect.d.ts.map +1 -1
- package/dist/connect.js +8 -3
- package/dist/connect.js.map +2 -2
- package/dist/sdk.d.ts +18 -4
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +17 -8
- package/dist/sdk.js.map +2 -2
- package/dist/zenoh.d.ts +29 -0
- package/dist/zenoh.d.ts.map +1 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/{chunk-JRCDA3EG.js → chunk-3DOWEHFU.js} +1 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-HJVATDC6.js +1 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-Q6XGCIPE.js +1 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/connect.js +1 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/elements.js +1 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/react.js +1 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/sdk.js +1 -1
- package/package.json +2 -1
- package/src/connect.ts +17 -21
- package/src/sdk.ts +50 -17
- package/src/zenoh.ts +122 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-AMCIKVG7.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-GJWPOWHG.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rainbow-robotics/plugin-ui",
|
|
3
|
-
"version": "0.1.2-dev.
|
|
3
|
+
"version": "0.1.2-dev.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20"
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"typescript": "^5.8.0"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"@eclipse-zenoh/zenoh-ts": "^1.9.0",
|
|
59
60
|
"@quilted/threads": "^4.0.3",
|
|
60
61
|
"@remote-dom/core": "^1.11.1",
|
|
61
62
|
"@remote-dom/react": "^1.2.2"
|
package/src/connect.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sandbox bootstrap — runs *inside* the plugin iframe.
|
|
3
3
|
*
|
|
4
|
-
* The sandbox initiates the handshake
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* @quilted/threads) plus the host SDK. We then render the plugin's React tree
|
|
8
|
-
* into a detached root and let `RemoteMutationObserver` stream every DOM
|
|
9
|
-
* mutation to the host, where the real rb-components live.
|
|
10
|
-
*
|
|
11
|
-
* The child-initiates-handshake pattern is required because @quilted/threads
|
|
12
|
-
* does not buffer a call made before the other side is listening; the host has
|
|
13
|
-
* been listening since it mounted the iframe, so a call *from* the sandbox is
|
|
14
|
-
* always delivered.
|
|
4
|
+
* The sandbox initiates the handshake: imports the host's `connect()` and calls
|
|
5
|
+
* it. The host responds with the RemoteConnection, toast(), and Zenoh config
|
|
6
|
+
* (url + pluginId). We open a Zenoh session and wire up the rb object.
|
|
15
7
|
*
|
|
16
8
|
* Verified against @remote-dom/core@1.11.x + @quilted/threads@4.x.
|
|
17
9
|
*/
|
|
@@ -22,23 +14,23 @@ import { createElement, type ComponentType } from "react";
|
|
|
22
14
|
import { createRoot } from "react-dom/client";
|
|
23
15
|
import { defineElements } from "./elements";
|
|
24
16
|
import { RbContext, type RbSdk } from "./sdk";
|
|
17
|
+
import { RbZenoh } from "./zenoh";
|
|
25
18
|
|
|
26
|
-
/** What the host exposes to the sandbox. */
|
|
27
19
|
interface HostExports {
|
|
28
20
|
connect: () => Promise<{
|
|
29
21
|
connection: RemoteConnection;
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
toast: (message: string, type?: string) => void;
|
|
23
|
+
zenohUrl: string;
|
|
24
|
+
pluginId: string;
|
|
32
25
|
}>;
|
|
33
26
|
}
|
|
34
27
|
|
|
35
28
|
/**
|
|
36
|
-
* Mount a plugin. Call this once from the sandbox entry (
|
|
37
|
-
* the plugin's root component.
|
|
29
|
+
* Mount a plugin. Call this once from the sandbox entry (content.tsx).
|
|
38
30
|
*
|
|
39
31
|
* @example
|
|
40
32
|
* import { connect } from "@rainbow-robotics/plugin-ui/connect";
|
|
41
|
-
* import App from "./
|
|
33
|
+
* import App from "./App";
|
|
42
34
|
* connect(App);
|
|
43
35
|
*/
|
|
44
36
|
export function connect(App: ComponentType): void {
|
|
@@ -48,14 +40,18 @@ export function connect(App: ComponentType): void {
|
|
|
48
40
|
targetOrigin: "*",
|
|
49
41
|
});
|
|
50
42
|
|
|
51
|
-
void host.connect().then(({ connection,
|
|
43
|
+
void host.connect().then(async ({ connection, toast, zenohUrl, pluginId }) => {
|
|
44
|
+
const zenoh = await RbZenoh.open(zenohUrl, pluginId);
|
|
45
|
+
|
|
52
46
|
const rb: RbSdk = {
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
pub: (topic, payload) => zenoh.pub(topic, payload),
|
|
48
|
+
sub: (topic, handler) => zenoh.sub(topic, handler),
|
|
49
|
+
action: (topic, payload) => zenoh.action(topic, payload),
|
|
50
|
+
serve: (topic, handler) => zenoh.serve(topic, handler),
|
|
51
|
+
toast: (message, type) => toast(message, type),
|
|
55
52
|
};
|
|
56
53
|
|
|
57
54
|
const root = document.createElement("remote-root");
|
|
58
|
-
|
|
59
55
|
document.body.append(root);
|
|
60
56
|
|
|
61
57
|
const observer = new RemoteMutationObserver(connection);
|
package/src/sdk.ts
CHANGED
|
@@ -1,35 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin runtime SDK — the
|
|
3
|
-
* inside the sandbox to talk to the host (Muscat / nexus).
|
|
2
|
+
* Plugin runtime SDK — the surface a plugin author calls from inside the sandbox.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Zenoh methods (pub/sub/action/serve) talk directly to the Zenoh router via
|
|
5
|
+
* WebSocket. Topics are auto-namespaced to plugin/{pluginId}/... to prevent
|
|
6
|
+
* cross-plugin collisions. A leading "/" bypasses namespacing (absolute path).
|
|
7
|
+
*
|
|
8
|
+
* notify() is still host-mediated (goes through the Muscat shell).
|
|
7
9
|
*/
|
|
8
10
|
import { createContext, useContext } from "react";
|
|
11
|
+
import type { Subscriber, Queryable } from "./zenoh";
|
|
9
12
|
|
|
10
13
|
export type RbNotifyType = "info" | "success" | "warning" | "error";
|
|
11
14
|
|
|
12
15
|
export interface RbSdk {
|
|
13
16
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* Publish JSON payload to a topic.
|
|
18
|
+
* "sensor/data" → plugin/{pluginId}/sensor/data
|
|
19
|
+
* "/broadcast/x" → broadcast/x (absolute)
|
|
20
|
+
*/
|
|
21
|
+
pub: <P = any>(topic: string, payload: P) => Promise<void>;
|
|
22
|
+
|
|
23
|
+
/** Subscribe to a topic. Returns a handle with undeclare(). */
|
|
24
|
+
sub: <P = any>(
|
|
25
|
+
topic: string,
|
|
26
|
+
handler: (payload: P) => void,
|
|
27
|
+
) => Promise<Subscriber>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Send a request and await the first reply (Zenoh query).
|
|
31
|
+
* Use this to call a backend handler registered with rb.serve() / ZenohRouter.
|
|
32
|
+
*/
|
|
33
|
+
action: <R = any, P = any>(topic: string, payload?: P) => Promise<R>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Register a queryable — the frontend responds to backend queries.
|
|
37
|
+
* Mirror of rb.action() on the other side.
|
|
16
38
|
*/
|
|
17
|
-
|
|
39
|
+
serve: <P = any, R = any>(
|
|
40
|
+
topic: string,
|
|
41
|
+
handler: (payload?: P) => R | Promise<R>,
|
|
42
|
+
) => Promise<Queryable>;
|
|
43
|
+
|
|
18
44
|
/** Show a transient notification through the host's toast system. */
|
|
19
|
-
|
|
45
|
+
toast: (message: string, type?: RbNotifyType) => void;
|
|
20
46
|
}
|
|
21
47
|
|
|
22
48
|
const noopSdk: RbSdk = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
49
|
+
pub: async <P = any>(_topic?: string, _payload?: P) => {
|
|
50
|
+
console.warn("[plugin-ui] rb.pub called outside of a host context");
|
|
51
|
+
},
|
|
52
|
+
sub: async <P = any>(_topic?: string, _handler?: (payload: P) => void) => {
|
|
53
|
+
console.warn("[plugin-ui] rb.sub called outside of a host context");
|
|
54
|
+
return { undeclare: async () => {} } as unknown as Subscriber;
|
|
55
|
+
},
|
|
56
|
+
action: async <R = any, P = any>(_topic?: string, _payload?: P) => {
|
|
57
|
+
console.warn("[plugin-ui] rb.action called outside of a host context");
|
|
58
|
+
return undefined as R;
|
|
59
|
+
},
|
|
60
|
+
serve: async <P = any, R = any>(_topic?: string, _handler?: (payload?: P) => R) => {
|
|
61
|
+
console.warn("[plugin-ui] rb.serve called outside of a host context");
|
|
62
|
+
return { undeclare: async () => {} } as unknown as Queryable;
|
|
28
63
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.warn("[plugin-ui] rb.notify (no host):", message);
|
|
32
|
-
}
|
|
64
|
+
toast: (message) => {
|
|
65
|
+
console.warn("[plugin-ui] rb.toast (no host):", message);
|
|
33
66
|
},
|
|
34
67
|
};
|
|
35
68
|
|
package/src/zenoh.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenoh communication client for plugin frontends.
|
|
3
|
+
* All topics are automatically namespaced to prevent cross-plugin collisions:
|
|
4
|
+
* "sensor/data" → "plugin/{pluginId}/sensor/data"
|
|
5
|
+
* "/broadcast/x" → "broadcast/x" (leading slash = absolute)
|
|
6
|
+
*/
|
|
7
|
+
import { Session, Config } from "@eclipse-zenoh/zenoh-ts";
|
|
8
|
+
import type { Publisher, Subscriber, Queryable } from "@eclipse-zenoh/zenoh-ts";
|
|
9
|
+
|
|
10
|
+
export type { Subscriber, Queryable };
|
|
11
|
+
|
|
12
|
+
export interface ZenohHandle {
|
|
13
|
+
undeclare(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveTopic(pluginId: string, topic: string): string {
|
|
17
|
+
return topic.startsWith("/") ? topic.slice(1) : `plugin/${pluginId}/${topic}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function encode(payload: unknown): string {
|
|
21
|
+
return JSON.stringify(payload);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function decode(raw: string | Uint8Array): unknown {
|
|
25
|
+
const str = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(str);
|
|
28
|
+
} catch {
|
|
29
|
+
return str;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class RbZenoh {
|
|
34
|
+
private readonly _session: Session;
|
|
35
|
+
private readonly _pluginId: string;
|
|
36
|
+
private readonly _pubs = new Map<string, Publisher>();
|
|
37
|
+
|
|
38
|
+
private constructor(session: Session, pluginId: string) {
|
|
39
|
+
this._session = session;
|
|
40
|
+
this._pluginId = pluginId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static async open(url: string, pluginId: string): Promise<RbZenoh> {
|
|
44
|
+
const session = await Session.open(new Config(url));
|
|
45
|
+
return new RbZenoh(session, pluginId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private _key(topic: string): string {
|
|
49
|
+
return resolveTopic(this._pluginId, topic);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Publish JSON payload to a topic. Publisher is cached per topic. */
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
async pub<P = any>(topic: string, payload: P): Promise<void> {
|
|
55
|
+
const key = this._key(topic);
|
|
56
|
+
let publisher = this._pubs.get(key);
|
|
57
|
+
if (!publisher) {
|
|
58
|
+
publisher = await this._session.declarePublisher(key);
|
|
59
|
+
this._pubs.set(key, publisher);
|
|
60
|
+
}
|
|
61
|
+
await publisher.put(encode(payload));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Subscribe to a topic. Returns a handle with undeclare(). */
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
async sub<P = any>(
|
|
67
|
+
topic: string,
|
|
68
|
+
handler: (payload: P) => void,
|
|
69
|
+
): Promise<Subscriber> {
|
|
70
|
+
return this._session.declareSubscriber(this._key(topic), {
|
|
71
|
+
handler: (sample) => {
|
|
72
|
+
handler(decode(sample.payload().toBytes()) as P);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Send a request and await the first reply (Zenoh query).
|
|
79
|
+
* Equivalent to a remote procedure call.
|
|
80
|
+
*/
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
82
|
+
async action<R = any, P = any>(topic: string, payload?: P): Promise<R> {
|
|
83
|
+
const opts =
|
|
84
|
+
payload !== undefined ? { payload: encode(payload) } : undefined;
|
|
85
|
+
const receiver = await this._session.get(this._key(topic), opts);
|
|
86
|
+
if (!receiver) return undefined;
|
|
87
|
+
for await (const reply of receiver) {
|
|
88
|
+
const result = reply.result();
|
|
89
|
+
if (result && "payload" in result) {
|
|
90
|
+
return decode(result.payload().toBytes()) as R;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Register a queryable handler (Zenoh queryable).
|
|
98
|
+
* The backend calls action(), this side responds.
|
|
99
|
+
*/
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
async serve<P = any, R = any>(
|
|
102
|
+
topic: string,
|
|
103
|
+
handler: (payload?: P) => R | Promise<R>,
|
|
104
|
+
): Promise<Queryable> {
|
|
105
|
+
const key = this._key(topic);
|
|
106
|
+
return this._session.declareQueryable(key, {
|
|
107
|
+
handler: async (query) => {
|
|
108
|
+
const reqPayload = query.payload()
|
|
109
|
+
? (decode(query.payload()!.toBytes()) as P)
|
|
110
|
+
: undefined;
|
|
111
|
+
const result = await handler(reqPayload);
|
|
112
|
+
await query.reply(key, encode(result));
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async close(): Promise<void> {
|
|
118
|
+
for (const pub of this._pubs.values()) await pub.undeclare();
|
|
119
|
+
this._pubs.clear();
|
|
120
|
+
await this._session.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var g=Object.defineProperty;var f=a=>{throw TypeError(a)};var h=(a,b,c)=>b in a?g(a,b,{enumerable:!0,configurable:!0,writable:!0,value:c}):a[b]=c;var i=(a,b,c)=>h(a,typeof b!="symbol"?b+"":b,c),d=(a,b,c)=>b.has(a)||f("Cannot "+c);var j=(a,b,c)=>(d(a,b,"read from private field"),c?c.call(a):b.get(a)),k=(a,b,c)=>b.has(a)?f("Cannot add the same private member more than once"):b instanceof WeakSet?b.add(a):b.set(a,c),l=(a,b,c,e)=>(d(a,b,"write to private field"),e?e.call(a,c):b.set(a,c),c),m=(a,b,c)=>(d(a,b,"access private method"),c);export{i as a,j as b,k as c,l as d,m as e};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{createContext as o,useContext as e}from"react";var t={action:async()=>{typeof console<"u"&&console.warn("[plugin-ui] rb.action called outside of a host context")},notify:n=>{typeof console<"u"&&console.warn("[plugin-ui] rb.notify (no host):",n)}},i=o(t);function s(){return e(i)}export{i as a,s as b};
|