@qping/plugin-bus 0.1.0 → 0.2.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.
- package/dist/bootstrap.d.ts +34 -0
- package/dist/bootstrap.mjs +170 -16
- package/dist/framing.d.ts +36 -0
- package/dist/i18n.d.ts +19 -0
- package/dist/i18n.mjs +2343 -0
- package/dist/node.d.ts +65 -0
- package/dist/{server.mjs → node.mjs} +244 -60
- package/dist/protocol.d.ts +101 -0
- package/dist/protocol.mjs +73 -1
- package/dist/router.d.ts +41 -0
- package/dist/transport.d.ts +24 -0
- package/dist/webClient.d.ts +34 -0
- package/dist/webClient.mjs +2599 -0
- package/dist/webTypes.d.ts +50 -0
- package/package.json +22 -8
- package/src/bootstrap.ts +98 -9
- package/src/i18n.ts +120 -0
- package/src/node.ts +213 -0
- package/src/protocol.ts +82 -17
- package/src/router.ts +65 -18
- package/src/transport.ts +2 -1
- package/src/webClient.ts +285 -0
- package/src/webTypes.ts +52 -0
- package/dist/bootstrap.d.mts +0 -1
- package/dist/protocol.d.mts +0 -1
- package/dist/server.d.mts +0 -1
- package/src/server.ts +0 -156
package/src/protocol.ts
CHANGED
|
@@ -6,25 +6,90 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Field names are camelCase on the wire (System.Text.Json camelCase policy on the C# side).
|
|
8
8
|
* Null fields are omitted on the wire (WhenWritingNull).
|
|
9
|
+
*
|
|
10
|
+
* Runtime constants mirror MyTools.Protocol (MessageKindWire, Routes, EndpointIds,
|
|
11
|
+
* ProtocolVersion.CurrentWire). Do not re-hardcode those strings in SDK source.
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
|
-
export
|
|
14
|
+
export const MessageKind = {
|
|
15
|
+
Request: "request",
|
|
16
|
+
Response: "response",
|
|
17
|
+
Event: "event",
|
|
18
|
+
} as const;
|
|
19
|
+
export type MessageKind = (typeof MessageKind)[keyof typeof MessageKind];
|
|
20
|
+
|
|
21
|
+
export const ErrorCode = {
|
|
22
|
+
ProtocolMismatch: "ProtocolMismatch",
|
|
23
|
+
HandshakeFailed: "HandshakeFailed",
|
|
24
|
+
CapabilityNotDeclared: "CapabilityNotDeclared",
|
|
25
|
+
CapabilityDenied: "CapabilityDenied",
|
|
26
|
+
InvalidPayload: "InvalidPayload",
|
|
27
|
+
MessageTooLarge: "MessageTooLarge",
|
|
28
|
+
RouteNotFound: "RouteNotFound",
|
|
29
|
+
RequestTimeout: "RequestTimeout",
|
|
30
|
+
TooManyRequests: "TooManyRequests",
|
|
31
|
+
TransportDisconnected: "TransportDisconnected",
|
|
32
|
+
PluginUnavailable: "PluginUnavailable",
|
|
33
|
+
InternalError: "InternalError",
|
|
34
|
+
Cancelled: "Cancelled",
|
|
35
|
+
RateLimited: "RateLimited",
|
|
36
|
+
} as const;
|
|
37
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
38
|
+
|
|
39
|
+
export const ProtocolVersion = "3.0";
|
|
40
|
+
|
|
41
|
+
export const EndpointIds = {
|
|
42
|
+
NodeMain: "node-main",
|
|
43
|
+
Host: "host",
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
export const Routes = {
|
|
47
|
+
Bus: {
|
|
48
|
+
Handshake: "bus.handshake",
|
|
49
|
+
Ping: "bus.ping",
|
|
50
|
+
Cancel: "bus.cancel",
|
|
51
|
+
Subscribe: "bus.subscribe",
|
|
52
|
+
Unsubscribe: "bus.unsubscribe",
|
|
53
|
+
},
|
|
54
|
+
Prefix: {
|
|
55
|
+
PluginCall: "plugin.call.",
|
|
56
|
+
HostCall: "host.call.",
|
|
57
|
+
PluginEvent: "plugin.event.",
|
|
58
|
+
HostEvent: "host.event.",
|
|
59
|
+
Diagnostics: "diagnostics.",
|
|
60
|
+
},
|
|
61
|
+
PluginCall: {
|
|
62
|
+
Initialize: "plugin.call.initialize",
|
|
63
|
+
Search: "plugin.call.search",
|
|
64
|
+
InvokeAction: "plugin.call.invokeAction",
|
|
65
|
+
},
|
|
66
|
+
HostEvent: {
|
|
67
|
+
Initialize: "host.event.initialize",
|
|
68
|
+
Search: "host.event.search",
|
|
69
|
+
Key: "host.event.key",
|
|
70
|
+
LanguageChanged: "host.event.languageChanged",
|
|
71
|
+
ThemeChanged: "host.event.themeChanged",
|
|
72
|
+
InputActionCaptured: "host.event.inputActionCaptured",
|
|
73
|
+
},
|
|
74
|
+
} as const;
|
|
12
75
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
76
|
+
export function pluginCallRoute(method: string): string {
|
|
77
|
+
return method.startsWith(Routes.Prefix.PluginCall)
|
|
78
|
+
? method
|
|
79
|
+
: `${Routes.Prefix.PluginCall}${method}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function hostCallRoute(method: string): string {
|
|
83
|
+
return method.startsWith(Routes.Prefix.HostCall)
|
|
84
|
+
? method
|
|
85
|
+
: `${Routes.Prefix.HostCall}${method}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function pluginEventRoute(subjectId: string): string {
|
|
89
|
+
return subjectId.startsWith(Routes.Prefix.PluginEvent)
|
|
90
|
+
? subjectId
|
|
91
|
+
: `${Routes.Prefix.PluginEvent}${subjectId}`;
|
|
92
|
+
}
|
|
28
93
|
|
|
29
94
|
export interface BusError {
|
|
30
95
|
code: ErrorCode;
|
|
@@ -38,7 +103,7 @@ export interface BusError {
|
|
|
38
103
|
* required; the optional ones are omitted on the wire when null.
|
|
39
104
|
*/
|
|
40
105
|
export interface Envelope {
|
|
41
|
-
version: string; // e.g.
|
|
106
|
+
version: string; // e.g. ProtocolVersion
|
|
42
107
|
id: string;
|
|
43
108
|
correlationId?: string | null;
|
|
44
109
|
traceId: string;
|
package/src/router.ts
CHANGED
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
* and correlate their responses. Mirrors the C# MessageBus routing rules on the Node side.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
8
|
import { randomBytes } from "node:crypto";
|
|
8
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
type Envelope,
|
|
11
|
+
type BusError,
|
|
12
|
+
EndpointIds,
|
|
13
|
+
ErrorCode,
|
|
14
|
+
MessageKind,
|
|
15
|
+
ProtocolVersion,
|
|
16
|
+
Routes,
|
|
17
|
+
} from "./protocol.ts";
|
|
9
18
|
|
|
10
19
|
type Handler = (payload: unknown) => Promise<unknown> | unknown;
|
|
11
20
|
type Sender = (env: Envelope) => void;
|
|
@@ -16,13 +25,46 @@ interface PendingHostCall {
|
|
|
16
25
|
route: string;
|
|
17
26
|
}
|
|
18
27
|
|
|
28
|
+
type RequestScope = { deadlineMs: number | null };
|
|
29
|
+
|
|
30
|
+
const requestScope = new AsyncLocalStorage<RequestScope>();
|
|
31
|
+
|
|
32
|
+
export const DefaultHostCallTimeoutMs = 30_000;
|
|
33
|
+
|
|
34
|
+
/** Remaining ms of the inbound plugin.call timeout, if currently inside a request. */
|
|
35
|
+
export function remainingTimeoutMs(): number | undefined {
|
|
36
|
+
const scope = requestScope.getStore();
|
|
37
|
+
if (!scope || scope.deadlineMs == null) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return scope.deadlineMs - Date.now();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveHostCallTimeoutMs(explicit?: number): number {
|
|
45
|
+
const remaining = remainingTimeoutMs();
|
|
46
|
+
if (explicit != null) {
|
|
47
|
+
return remaining == null ? explicit : Math.min(explicit, remaining);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return remaining ?? DefaultHostCallTimeoutMs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function deadlineFromTimeoutMs(timeoutMs: number | null | undefined): number | null {
|
|
54
|
+
if (typeof timeoutMs !== "number" || timeoutMs <= 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Date.now() + timeoutMs;
|
|
59
|
+
}
|
|
60
|
+
|
|
19
61
|
export class HandlerRouter {
|
|
20
62
|
private handlers = new Map<string, Handler>();
|
|
21
63
|
private pendingHostCalls = new Map<string, PendingHostCall>();
|
|
22
64
|
private pluginId = "p";
|
|
23
65
|
private entryId = "e";
|
|
24
66
|
private sessionId = "s";
|
|
25
|
-
private endpointId =
|
|
67
|
+
private endpointId: string = EndpointIds.NodeMain;
|
|
26
68
|
|
|
27
69
|
/** Injected transport send fn; tests can override `router.send` directly. */
|
|
28
70
|
send: Sender;
|
|
@@ -45,48 +87,54 @@ export class HandlerRouter {
|
|
|
45
87
|
|
|
46
88
|
/** Dispatches an inbound request/response. Returns once handled. */
|
|
47
89
|
async dispatch(env: Envelope): Promise<void> {
|
|
48
|
-
if (env.kind ===
|
|
90
|
+
if (env.kind === MessageKind.Response) {
|
|
49
91
|
this.handleHostResponse(env);
|
|
50
92
|
return;
|
|
51
93
|
}
|
|
52
|
-
if (env.kind !==
|
|
94
|
+
if (env.kind !== MessageKind.Request) return;
|
|
53
95
|
|
|
54
96
|
// bus.ping is always auto-replied; it does not occupy a handler slot.
|
|
55
|
-
if (env.route ===
|
|
97
|
+
if (env.route === Routes.Bus.Ping) {
|
|
56
98
|
this.send(this.responseFor(env, { ok: true }));
|
|
57
99
|
return;
|
|
58
100
|
}
|
|
59
101
|
|
|
60
102
|
const handler = this.handlers.get(env.route);
|
|
61
103
|
if (!handler) {
|
|
62
|
-
this.send(this.errorResponseFor(env,
|
|
104
|
+
this.send(this.errorResponseFor(env, ErrorCode.RouteNotFound, `route '${env.route}' has no handler`));
|
|
63
105
|
return;
|
|
64
106
|
}
|
|
65
107
|
|
|
108
|
+
const deadlineMs = deadlineFromTimeoutMs(env.timeoutMs);
|
|
66
109
|
try {
|
|
67
|
-
const result = await handler(env.payload);
|
|
110
|
+
const result = await requestScope.run({ deadlineMs }, () => handler(env.payload));
|
|
68
111
|
this.send(this.responseFor(env, result ?? {}));
|
|
69
112
|
} catch (err) {
|
|
70
113
|
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
-
this.send(this.errorResponseFor(env,
|
|
114
|
+
this.send(this.errorResponseFor(env, ErrorCode.InternalError, message));
|
|
72
115
|
}
|
|
73
116
|
}
|
|
74
117
|
|
|
75
118
|
/** Calls a host.call.* capability and resolves with the response payload. */
|
|
76
|
-
callHost(route: string, payload: unknown, timeoutMs
|
|
119
|
+
callHost(route: string, payload: unknown, timeoutMs?: number): Promise<unknown> {
|
|
120
|
+
const effectiveTimeoutMs = resolveHostCallTimeoutMs(timeoutMs);
|
|
121
|
+
if (effectiveTimeoutMs <= 0) {
|
|
122
|
+
return Promise.reject(new Error(`host call ${route} timed out (no time remaining)`));
|
|
123
|
+
}
|
|
124
|
+
|
|
77
125
|
return new Promise((resolve, reject) => {
|
|
78
126
|
const id = randomBytesHex();
|
|
79
127
|
const req: Envelope = {
|
|
80
|
-
version:
|
|
128
|
+
version: ProtocolVersion,
|
|
81
129
|
id,
|
|
82
130
|
traceId: id,
|
|
83
131
|
sessionId: this.sessionId,
|
|
84
132
|
pluginId: this.pluginId,
|
|
85
133
|
entryId: this.entryId,
|
|
86
134
|
endpointId: this.endpointId,
|
|
87
|
-
kind:
|
|
135
|
+
kind: MessageKind.Request,
|
|
88
136
|
route,
|
|
89
|
-
timeoutMs,
|
|
137
|
+
timeoutMs: effectiveTimeoutMs,
|
|
90
138
|
payload,
|
|
91
139
|
};
|
|
92
140
|
const pending: PendingHostCall = { resolve, reject, route };
|
|
@@ -94,10 +142,9 @@ export class HandlerRouter {
|
|
|
94
142
|
const timer = setTimeout(() => {
|
|
95
143
|
if (this.pendingHostCalls.has(id)) {
|
|
96
144
|
this.pendingHostCalls.delete(id);
|
|
97
|
-
reject(new Error(`host call ${route} timed out after ${
|
|
145
|
+
reject(new Error(`host call ${route} timed out after ${effectiveTimeoutMs}ms`));
|
|
98
146
|
}
|
|
99
|
-
},
|
|
100
|
-
// Clear the timer when settled.
|
|
147
|
+
}, effectiveTimeoutMs);
|
|
101
148
|
const origResolve = pending.resolve;
|
|
102
149
|
const origReject = pending.reject;
|
|
103
150
|
pending.resolve = (v) => { clearTimeout(timer); origResolve(v); };
|
|
@@ -120,7 +167,7 @@ export class HandlerRouter {
|
|
|
120
167
|
|
|
121
168
|
private responseFor(req: Envelope, payload: unknown): Envelope {
|
|
122
169
|
return {
|
|
123
|
-
version:
|
|
170
|
+
version: ProtocolVersion,
|
|
124
171
|
id: randomBytesHex(),
|
|
125
172
|
correlationId: req.id,
|
|
126
173
|
traceId: req.traceId,
|
|
@@ -128,13 +175,13 @@ export class HandlerRouter {
|
|
|
128
175
|
pluginId: req.pluginId,
|
|
129
176
|
entryId: req.entryId,
|
|
130
177
|
endpointId: this.endpointId,
|
|
131
|
-
kind:
|
|
178
|
+
kind: MessageKind.Response,
|
|
132
179
|
route: req.route,
|
|
133
180
|
payload,
|
|
134
181
|
};
|
|
135
182
|
}
|
|
136
183
|
|
|
137
|
-
private errorResponseFor(req: Envelope, code:
|
|
184
|
+
private errorResponseFor(req: Envelope, code: BusError["code"], message: string): Envelope {
|
|
138
185
|
return {
|
|
139
186
|
...this.responseFor(req, null),
|
|
140
187
|
payload: undefined,
|
package/src/transport.ts
CHANGED
|
@@ -17,8 +17,9 @@ export class NodeTransport {
|
|
|
17
17
|
private disconnectHandlers = new Set<DisconnectHandler>();
|
|
18
18
|
private closed = false;
|
|
19
19
|
|
|
20
|
-
onMessage(handler: MessageHandler): void {
|
|
20
|
+
onMessage(handler: MessageHandler): () => void {
|
|
21
21
|
this.messageHandlers.add(handler);
|
|
22
|
+
return () => { this.messageHandlers.delete(handler); };
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
onDisconnect(handler: DisconnectHandler): void {
|
package/src/webClient.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3 Web SDK: speaks protocol envelopes over chrome.webview.postMessage.
|
|
3
|
+
* Host stamps identity; the page does not supply plugin/entry/session ids.
|
|
4
|
+
*
|
|
5
|
+
* Connections start with bus.handshake. Subsequent plugin.call.* envelopes use the
|
|
6
|
+
* negotiated version. call("refresh") is sent as plugin.call.refresh.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mytoolsI18n } from "./i18n.ts";
|
|
10
|
+
import {
|
|
11
|
+
type Envelope,
|
|
12
|
+
type BusError,
|
|
13
|
+
MessageKind,
|
|
14
|
+
ProtocolVersion,
|
|
15
|
+
Routes,
|
|
16
|
+
pluginCallRoute,
|
|
17
|
+
} from "./protocol.ts";
|
|
18
|
+
import type { MyToolsThemePayload } from "./webTypes.ts";
|
|
19
|
+
|
|
20
|
+
export { mytoolsI18n } from "./i18n.ts";
|
|
21
|
+
export { HostEvents } from "./webTypes.ts";
|
|
22
|
+
export type {
|
|
23
|
+
MyToolsHostInitializePayload,
|
|
24
|
+
MyToolsHostKeyPayload,
|
|
25
|
+
MyToolsHostSearchPayload,
|
|
26
|
+
MyToolsInputActionCapturedPayload,
|
|
27
|
+
MyToolsLanguageChangedPayload,
|
|
28
|
+
MyToolsThemeChangedPayload,
|
|
29
|
+
MyToolsThemePayload,
|
|
30
|
+
} from "./webTypes.ts";
|
|
31
|
+
|
|
32
|
+
type Pending = {
|
|
33
|
+
resolve: (value: unknown) => void;
|
|
34
|
+
reject: (err: Error) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface WebBusClient {
|
|
38
|
+
/** Sends plugin.call.<method>. Bare names are prefixed; full routes are left as-is. */
|
|
39
|
+
call<T = unknown>(method: string, payload?: unknown, timeoutMs?: number): Promise<T>;
|
|
40
|
+
on<T = unknown>(route: string, handler: (payload: T) => void): () => void;
|
|
41
|
+
i18n: typeof mytoolsI18n;
|
|
42
|
+
theme: typeof mytoolsTheme;
|
|
43
|
+
close(): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const HandshakeTimeoutMs = 8_000;
|
|
47
|
+
|
|
48
|
+
const mytoolsTheme = {
|
|
49
|
+
current: "dark",
|
|
50
|
+
apply(payload: MyToolsThemePayload): void {
|
|
51
|
+
if (typeof payload.theme === "string") {
|
|
52
|
+
this.current = payload.theme;
|
|
53
|
+
}
|
|
54
|
+
const root = typeof document !== "undefined" ? document.documentElement : null;
|
|
55
|
+
if (!root) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (typeof payload.theme === "string") {
|
|
59
|
+
root.setAttribute("data-theme", payload.theme);
|
|
60
|
+
root.style.colorScheme = payload.theme;
|
|
61
|
+
}
|
|
62
|
+
if (payload.themeTokens) {
|
|
63
|
+
for (const [key, value] of Object.entries(payload.themeTokens)) {
|
|
64
|
+
if (typeof value === "string") {
|
|
65
|
+
root.style.setProperty(key, value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function hasWebView(): boolean {
|
|
73
|
+
return !!(typeof window !== "undefined" && (window as any).chrome?.webview);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function post(env: Envelope): void {
|
|
77
|
+
if (!hasWebView()) throw new Error("chrome.webview is not available");
|
|
78
|
+
(window as any).chrome.webview.postMessage(env);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function randomId(): string {
|
|
82
|
+
const bytes = new Uint8Array(16);
|
|
83
|
+
crypto.getRandomValues(bytes);
|
|
84
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseMessage(data: unknown): Envelope | null {
|
|
88
|
+
if (typeof data === "string") {
|
|
89
|
+
try {
|
|
90
|
+
data = JSON.parse(data);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!data || typeof data !== "object") return null;
|
|
96
|
+
const env = data as Envelope;
|
|
97
|
+
if (typeof env.kind !== "string" || typeof env.route !== "string") return null;
|
|
98
|
+
return env;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function applyHostSideEffects(env: Envelope): void {
|
|
102
|
+
const payload = env.payload;
|
|
103
|
+
if (!payload || typeof payload !== "object") return;
|
|
104
|
+
if (env.route === Routes.HostEvent.Initialize) {
|
|
105
|
+
mytoolsI18n.configure(payload);
|
|
106
|
+
mytoolsI18n.apply();
|
|
107
|
+
mytoolsTheme.apply(payload as MyToolsThemePayload);
|
|
108
|
+
} else if (env.route === Routes.HostEvent.LanguageChanged) {
|
|
109
|
+
mytoolsI18n.configure(payload);
|
|
110
|
+
mytoolsI18n.apply();
|
|
111
|
+
} else if (env.route === Routes.HostEvent.ThemeChanged) {
|
|
112
|
+
mytoolsTheme.apply(payload as MyToolsThemePayload);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function negotiatedVersionFrom(payload: unknown): string {
|
|
117
|
+
if (payload && typeof payload === "object" && "negotiatedVersion" in payload) {
|
|
118
|
+
const value = (payload as { negotiatedVersion?: unknown }).negotiatedVersion;
|
|
119
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
120
|
+
}
|
|
121
|
+
return ProtocolVersion;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates a Web bus client. Registers the message listener immediately so host
|
|
126
|
+
* events that arrive before `on()` are buffered and replayed.
|
|
127
|
+
*
|
|
128
|
+
* Handshake is required before call(). Page scripts are bundled as IIFE, so this
|
|
129
|
+
* function is synchronous; handshake runs in the background and gates call().
|
|
130
|
+
*/
|
|
131
|
+
export function createWebBusClient(options?: {
|
|
132
|
+
timeoutMs?: number;
|
|
133
|
+
}): WebBusClient {
|
|
134
|
+
const pending = new Map<string, Pending>();
|
|
135
|
+
const eventHandlers = new Set<(env: Envelope) => void>();
|
|
136
|
+
const lastByRoute = new Map<string, Envelope>();
|
|
137
|
+
const defaultTimeout = options?.timeoutMs ?? 30_000;
|
|
138
|
+
let wireVersion = ProtocolVersion;
|
|
139
|
+
let handshakeError: Error | null = null;
|
|
140
|
+
|
|
141
|
+
const onMessage = (event: MessageEvent) => {
|
|
142
|
+
const env = parseMessage(event.data);
|
|
143
|
+
if (!env) return;
|
|
144
|
+
if (env.kind === MessageKind.Response && env.correlationId) {
|
|
145
|
+
const p = pending.get(env.correlationId);
|
|
146
|
+
if (!p) return;
|
|
147
|
+
pending.delete(env.correlationId);
|
|
148
|
+
if (env.error) {
|
|
149
|
+
p.reject(new Error(`${(env.error as BusError).code}: ${(env.error as BusError).message}`));
|
|
150
|
+
} else {
|
|
151
|
+
p.resolve(env.payload);
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (env.kind === MessageKind.Event) {
|
|
156
|
+
lastByRoute.set(env.route, env);
|
|
157
|
+
applyHostSideEffects(env);
|
|
158
|
+
for (const h of eventHandlers) h(env);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
let handshakeDone: Promise<void> = Promise.resolve();
|
|
163
|
+
if (hasWebView()) {
|
|
164
|
+
(window as any).chrome.webview.addEventListener("message", onMessage);
|
|
165
|
+
handshakeDone = handshake(HandshakeTimeoutMs)
|
|
166
|
+
.then((version) => {
|
|
167
|
+
wireVersion = version;
|
|
168
|
+
})
|
|
169
|
+
.catch((err: unknown) => {
|
|
170
|
+
handshakeError = err instanceof Error ? err : new Error(String(err));
|
|
171
|
+
throw handshakeError;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ensureHandshaken(): Promise<void> {
|
|
176
|
+
if (handshakeError) return Promise.reject(handshakeError);
|
|
177
|
+
return handshakeDone.then(() => {
|
|
178
|
+
if (handshakeError) throw handshakeError;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function call<T = unknown>(method: string, payload?: unknown, timeoutMs = defaultTimeout): Promise<T> {
|
|
183
|
+
const route = pluginCallRoute(method);
|
|
184
|
+
return ensureHandshaken().then(
|
|
185
|
+
() =>
|
|
186
|
+
new Promise<T>((resolve, reject) => {
|
|
187
|
+
const id = randomId();
|
|
188
|
+
const timer = window.setTimeout(() => {
|
|
189
|
+
pending.delete(id);
|
|
190
|
+
reject(new Error(`request timed out: ${route}`));
|
|
191
|
+
}, timeoutMs);
|
|
192
|
+
pending.set(id, {
|
|
193
|
+
resolve: (v) => {
|
|
194
|
+
window.clearTimeout(timer);
|
|
195
|
+
resolve(v as T);
|
|
196
|
+
},
|
|
197
|
+
reject: (e) => {
|
|
198
|
+
window.clearTimeout(timer);
|
|
199
|
+
reject(e);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
post({
|
|
203
|
+
version: wireVersion,
|
|
204
|
+
id,
|
|
205
|
+
traceId: id,
|
|
206
|
+
sessionId: "",
|
|
207
|
+
pluginId: "",
|
|
208
|
+
entryId: "",
|
|
209
|
+
endpointId: "",
|
|
210
|
+
kind: MessageKind.Request,
|
|
211
|
+
route,
|
|
212
|
+
timeoutMs,
|
|
213
|
+
payload: payload ?? {},
|
|
214
|
+
});
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function subscribe(handler: (env: Envelope) => void): () => void {
|
|
220
|
+
eventHandlers.add(handler);
|
|
221
|
+
for (const env of lastByRoute.values()) handler(env);
|
|
222
|
+
return () => {
|
|
223
|
+
eventHandlers.delete(handler);
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
call,
|
|
229
|
+
on: (route, handler) =>
|
|
230
|
+
subscribe((env) => {
|
|
231
|
+
if (env.route === route) handler((env.payload ?? {}) as never);
|
|
232
|
+
}),
|
|
233
|
+
i18n: mytoolsI18n,
|
|
234
|
+
theme: mytoolsTheme,
|
|
235
|
+
close: () => {
|
|
236
|
+
if (hasWebView()) {
|
|
237
|
+
(window as any).chrome.webview.removeEventListener("message", onMessage);
|
|
238
|
+
}
|
|
239
|
+
pending.clear();
|
|
240
|
+
eventHandlers.clear();
|
|
241
|
+
lastByRoute.clear();
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function handshake(timeoutMs: number): Promise<string> {
|
|
247
|
+
const id = randomId();
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
const timer = window.setTimeout(() => {
|
|
250
|
+
cleanup();
|
|
251
|
+
reject(new Error("bus.handshake timed out"));
|
|
252
|
+
}, timeoutMs);
|
|
253
|
+
|
|
254
|
+
const onMessage = (event: MessageEvent) => {
|
|
255
|
+
const env = parseMessage(event.data);
|
|
256
|
+
if (!env || env.kind !== MessageKind.Response || env.correlationId !== id) return;
|
|
257
|
+
cleanup();
|
|
258
|
+
if (env.error) {
|
|
259
|
+
reject(new Error(`${env.error.code}: ${env.error.message}`));
|
|
260
|
+
} else {
|
|
261
|
+
resolve(negotiatedVersionFrom(env.payload));
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const cleanup = () => {
|
|
266
|
+
window.clearTimeout(timer);
|
|
267
|
+
(window as any).chrome.webview.removeEventListener("message", onMessage);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
(window as any).chrome.webview.addEventListener("message", onMessage);
|
|
271
|
+
post({
|
|
272
|
+
version: ProtocolVersion,
|
|
273
|
+
id,
|
|
274
|
+
traceId: id,
|
|
275
|
+
sessionId: "",
|
|
276
|
+
pluginId: "",
|
|
277
|
+
entryId: "",
|
|
278
|
+
endpointId: "web",
|
|
279
|
+
kind: MessageKind.Request,
|
|
280
|
+
route: Routes.Bus.Handshake,
|
|
281
|
+
timeoutMs,
|
|
282
|
+
payload: { version: ProtocolVersion, supportedVersions: [ProtocolVersion] },
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
package/src/webTypes.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Routes } from "./protocol.ts";
|
|
2
|
+
|
|
3
|
+
export interface MyToolsHostInitializePayload {
|
|
4
|
+
protocolVersion: string;
|
|
5
|
+
pluginId: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
itemId: string;
|
|
8
|
+
query: string;
|
|
9
|
+
keyword: string;
|
|
10
|
+
initialState: unknown;
|
|
11
|
+
locale: string;
|
|
12
|
+
fallbackLocale: string;
|
|
13
|
+
translationRevision: string;
|
|
14
|
+
messages: Record<string, string>;
|
|
15
|
+
theme?: string;
|
|
16
|
+
themeTokens?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MyToolsLanguageChangedPayload {
|
|
20
|
+
locale: string;
|
|
21
|
+
fallbackLocale: string;
|
|
22
|
+
translationRevision: string;
|
|
23
|
+
messages: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MyToolsThemeChangedPayload {
|
|
27
|
+
theme: string;
|
|
28
|
+
themeTokens: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MyToolsHostSearchPayload {
|
|
32
|
+
query: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MyToolsHostKeyPayload {
|
|
36
|
+
key: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MyToolsInputActionCapturedPayload {
|
|
40
|
+
requestId: string;
|
|
41
|
+
cancelled?: boolean;
|
|
42
|
+
kind?: "hotkey" | "mouse";
|
|
43
|
+
hotKey?: string | null;
|
|
44
|
+
mouseButton?: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MyToolsThemePayload {
|
|
48
|
+
theme?: string;
|
|
49
|
+
themeTokens?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const HostEvents = Routes.HostEvent;
|
package/dist/bootstrap.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../src/bootstrap.ts";
|
package/dist/protocol.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../src/protocol.ts";
|
package/dist/server.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../src/server.ts";
|