@qping/plugin-bus 0.2.0 → 0.3.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/actions.d.ts +204 -0
- package/dist/bootstrap.d.ts +3 -1
- package/dist/bootstrap.mjs +5 -1
- package/dist/dev.d.ts +5 -0
- package/dist/dev.mjs +80 -0
- package/dist/developmentRefresh.d.ts +9 -0
- package/dist/hostEnv.d.ts +9 -0
- package/dist/hotkeyKeycaps.d.ts +2 -0
- package/dist/node.d.ts +35 -17
- package/dist/node.mjs +209 -26
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.mjs +1 -0
- package/dist/router.d.ts +3 -1
- package/dist/search.d.ts +7 -0
- package/dist/search.mjs +17 -0
- package/dist/webClient.d.ts +2 -1
- package/dist/webClient.mjs +26 -1
- package/dist/webTypes.d.ts +15 -0
- package/package.json +55 -45
- package/src/actions.ts +158 -0
- package/src/bootstrap.ts +159 -159
- package/src/dev.ts +14 -0
- package/src/developmentRefresh.ts +95 -0
- package/src/hostEnv.ts +21 -0
- package/src/hotkeyKeycaps.ts +28 -0
- package/src/node.ts +163 -44
- package/src/protocol.ts +1 -0
- package/src/router.ts +199 -196
- package/src/search.ts +22 -0
- package/src/webClient.ts +3 -0
- package/src/webTypes.ts +16 -0
package/src/router.ts
CHANGED
|
@@ -1,196 +1,199 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handler router for the Node SDK v3. Dispatches inbound plugin.call.* requests to registered
|
|
3
|
-
* handlers, auto-replies to bus.ping, and provides callHost() to invoke host.call.* capabilities
|
|
4
|
-
* and correlate their responses. Mirrors the C# MessageBus routing rules on the Node side.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
-
import { randomBytes } from "node:crypto";
|
|
9
|
-
import {
|
|
10
|
-
type Envelope,
|
|
11
|
-
type BusError,
|
|
12
|
-
EndpointIds,
|
|
13
|
-
ErrorCode,
|
|
14
|
-
MessageKind,
|
|
15
|
-
ProtocolVersion,
|
|
16
|
-
Routes,
|
|
17
|
-
} from "./protocol.ts";
|
|
18
|
-
|
|
19
|
-
type Handler = (payload: unknown) => Promise<unknown> | unknown;
|
|
20
|
-
type Sender = (env: Envelope) => void;
|
|
21
|
-
|
|
22
|
-
interface PendingHostCall {
|
|
23
|
-
resolve: (value: unknown) => void;
|
|
24
|
-
reject: (err: Error) => void;
|
|
25
|
-
route: string;
|
|
26
|
-
}
|
|
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
|
-
|
|
61
|
-
export class HandlerRouter {
|
|
62
|
-
private handlers = new Map<string, Handler>();
|
|
63
|
-
private pendingHostCalls = new Map<string, PendingHostCall>();
|
|
64
|
-
private pluginId = "p";
|
|
65
|
-
private entryId = "e";
|
|
66
|
-
private sessionId = "s";
|
|
67
|
-
private endpointId: string = EndpointIds.NodeMain;
|
|
68
|
-
|
|
69
|
-
/** Injected transport send fn; tests can override `router.send` directly. */
|
|
70
|
-
send: Sender;
|
|
71
|
-
|
|
72
|
-
constructor(deps: { send: Sender }) {
|
|
73
|
-
this.send = deps.send;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Sets the bound identity stamped on outbound messages (after handshake). */
|
|
77
|
-
setIdentity(ids: { pluginId: string; entryId: string; sessionId: string; endpointId: string }): void {
|
|
78
|
-
this.pluginId = ids.pluginId;
|
|
79
|
-
this.entryId = ids.entryId;
|
|
80
|
-
this.sessionId = ids.sessionId;
|
|
81
|
-
this.endpointId = ids.endpointId;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
handle(route: string, handler: Handler): void {
|
|
85
|
-
this.handlers.set(route, handler);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Dispatches an inbound request/response. Returns once handled. */
|
|
89
|
-
async dispatch(env: Envelope): Promise<void> {
|
|
90
|
-
if (env.kind === MessageKind.Response) {
|
|
91
|
-
this.handleHostResponse(env);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (env.kind !== MessageKind.Request) return;
|
|
95
|
-
|
|
96
|
-
// bus.ping is always auto-replied; it does not occupy a handler slot.
|
|
97
|
-
if (env.route === Routes.Bus.Ping) {
|
|
98
|
-
this.send(this.responseFor(env, { ok: true }));
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const handler = this.handlers.get(env.route);
|
|
103
|
-
if (!handler) {
|
|
104
|
-
this.send(this.errorResponseFor(env, ErrorCode.RouteNotFound, `route '${env.route}' has no handler`));
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const deadlineMs = deadlineFromTimeoutMs(env.timeoutMs);
|
|
109
|
-
try {
|
|
110
|
-
const result = await requestScope.run(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.send(this.
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Handler router for the Node SDK v3. Dispatches inbound plugin.call.* requests to registered
|
|
3
|
+
* handlers, auto-replies to bus.ping, and provides callHost() to invoke host.call.* capabilities
|
|
4
|
+
* and correlate their responses. Mirrors the C# MessageBus routing rules on the Node side.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import {
|
|
10
|
+
type Envelope,
|
|
11
|
+
type BusError,
|
|
12
|
+
EndpointIds,
|
|
13
|
+
ErrorCode,
|
|
14
|
+
MessageKind,
|
|
15
|
+
ProtocolVersion,
|
|
16
|
+
Routes,
|
|
17
|
+
} from "./protocol.ts";
|
|
18
|
+
|
|
19
|
+
type Handler = (payload: unknown, context: { sessionId: string }) => Promise<unknown> | unknown;
|
|
20
|
+
type Sender = (env: Envelope) => void;
|
|
21
|
+
|
|
22
|
+
interface PendingHostCall {
|
|
23
|
+
resolve: (value: unknown) => void;
|
|
24
|
+
reject: (err: Error) => void;
|
|
25
|
+
route: string;
|
|
26
|
+
}
|
|
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
|
+
|
|
61
|
+
export class HandlerRouter {
|
|
62
|
+
private handlers = new Map<string, Handler>();
|
|
63
|
+
private pendingHostCalls = new Map<string, PendingHostCall>();
|
|
64
|
+
private pluginId = "p";
|
|
65
|
+
private entryId = "e";
|
|
66
|
+
private sessionId = "s";
|
|
67
|
+
private endpointId: string = EndpointIds.NodeMain;
|
|
68
|
+
|
|
69
|
+
/** Injected transport send fn; tests can override `router.send` directly. */
|
|
70
|
+
send: Sender;
|
|
71
|
+
|
|
72
|
+
constructor(deps: { send: Sender }) {
|
|
73
|
+
this.send = deps.send;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Sets the bound identity stamped on outbound messages (after handshake). */
|
|
77
|
+
setIdentity(ids: { pluginId: string; entryId: string; sessionId: string; endpointId: string }): void {
|
|
78
|
+
this.pluginId = ids.pluginId;
|
|
79
|
+
this.entryId = ids.entryId;
|
|
80
|
+
this.sessionId = ids.sessionId;
|
|
81
|
+
this.endpointId = ids.endpointId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
handle(route: string, handler: Handler): void {
|
|
85
|
+
this.handlers.set(route, handler);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Dispatches an inbound request/response. Returns once handled. */
|
|
89
|
+
async dispatch(env: Envelope): Promise<void> {
|
|
90
|
+
if (env.kind === MessageKind.Response) {
|
|
91
|
+
this.handleHostResponse(env);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (env.kind !== MessageKind.Request) return;
|
|
95
|
+
|
|
96
|
+
// bus.ping is always auto-replied; it does not occupy a handler slot.
|
|
97
|
+
if (env.route === Routes.Bus.Ping) {
|
|
98
|
+
this.send(this.responseFor(env, { ok: true }));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handler = this.handlers.get(env.route);
|
|
103
|
+
if (!handler) {
|
|
104
|
+
this.send(this.errorResponseFor(env, ErrorCode.RouteNotFound, `route '${env.route}' has no handler`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const deadlineMs = deadlineFromTimeoutMs(env.timeoutMs);
|
|
109
|
+
try {
|
|
110
|
+
const result = await requestScope.run(
|
|
111
|
+
{ deadlineMs },
|
|
112
|
+
() => handler(env.payload, { sessionId: env.sessionId }),
|
|
113
|
+
);
|
|
114
|
+
this.send(this.responseFor(env, result ?? {}));
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
this.send(this.errorResponseFor(env, ErrorCode.InternalError, message));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Calls a host.call.* capability and resolves with the response payload. */
|
|
122
|
+
callHost(route: string, payload: unknown, timeoutMs?: number): Promise<unknown> {
|
|
123
|
+
const effectiveTimeoutMs = resolveHostCallTimeoutMs(timeoutMs);
|
|
124
|
+
if (effectiveTimeoutMs <= 0) {
|
|
125
|
+
return Promise.reject(new Error(`host call ${route} timed out (no time remaining)`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const id = randomBytesHex();
|
|
130
|
+
const req: Envelope = {
|
|
131
|
+
version: ProtocolVersion,
|
|
132
|
+
id,
|
|
133
|
+
traceId: id,
|
|
134
|
+
sessionId: this.sessionId,
|
|
135
|
+
pluginId: this.pluginId,
|
|
136
|
+
entryId: this.entryId,
|
|
137
|
+
endpointId: this.endpointId,
|
|
138
|
+
kind: MessageKind.Request,
|
|
139
|
+
route,
|
|
140
|
+
timeoutMs: effectiveTimeoutMs,
|
|
141
|
+
payload,
|
|
142
|
+
};
|
|
143
|
+
const pending: PendingHostCall = { resolve, reject, route };
|
|
144
|
+
this.pendingHostCalls.set(id, pending);
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
if (this.pendingHostCalls.has(id)) {
|
|
147
|
+
this.pendingHostCalls.delete(id);
|
|
148
|
+
reject(new Error(`host call ${route} timed out after ${effectiveTimeoutMs}ms`));
|
|
149
|
+
}
|
|
150
|
+
}, effectiveTimeoutMs);
|
|
151
|
+
const origResolve = pending.resolve;
|
|
152
|
+
const origReject = pending.reject;
|
|
153
|
+
pending.resolve = (v) => { clearTimeout(timer); origResolve(v); };
|
|
154
|
+
pending.reject = (e) => { clearTimeout(timer); origReject(e); };
|
|
155
|
+
this.send(req);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private handleHostResponse(env: Envelope): void {
|
|
160
|
+
if (!env.correlationId) return;
|
|
161
|
+
const pending = this.pendingHostCalls.get(env.correlationId);
|
|
162
|
+
if (!pending) return;
|
|
163
|
+
this.pendingHostCalls.delete(env.correlationId);
|
|
164
|
+
if (env.error) {
|
|
165
|
+
pending.reject(new Error(`${env.error.code}: ${env.error.message}`));
|
|
166
|
+
} else {
|
|
167
|
+
pending.resolve(env.payload);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private responseFor(req: Envelope, payload: unknown): Envelope {
|
|
172
|
+
return {
|
|
173
|
+
version: ProtocolVersion,
|
|
174
|
+
id: randomBytesHex(),
|
|
175
|
+
correlationId: req.id,
|
|
176
|
+
traceId: req.traceId,
|
|
177
|
+
sessionId: req.sessionId,
|
|
178
|
+
pluginId: req.pluginId,
|
|
179
|
+
entryId: req.entryId,
|
|
180
|
+
endpointId: this.endpointId,
|
|
181
|
+
kind: MessageKind.Response,
|
|
182
|
+
route: req.route,
|
|
183
|
+
payload,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private errorResponseFor(req: Envelope, code: BusError["code"], message: string): Envelope {
|
|
188
|
+
return {
|
|
189
|
+
...this.responseFor(req, null),
|
|
190
|
+
payload: undefined,
|
|
191
|
+
error: { code, message, retryable: false } as BusError,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function randomBytesHex(): string {
|
|
197
|
+
// 16 random bytes -> 32 hex chars, matching the C# GuidIdGenerator format.
|
|
198
|
+
return randomBytes(16).toString("hex");
|
|
199
|
+
}
|
package/src/search.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether every character in `pattern` occurs in `target` in the same order.
|
|
3
|
+
* Matching is case-insensitive and characters do not need to be adjacent.
|
|
4
|
+
*
|
|
5
|
+
* @example isSubsequence("gthb", "GitHub") // true
|
|
6
|
+
*/
|
|
7
|
+
export function isSubsequence(pattern: string, target: string): boolean {
|
|
8
|
+
if (!pattern) return true;
|
|
9
|
+
if (!target) return false;
|
|
10
|
+
|
|
11
|
+
const needle = pattern.toLowerCase();
|
|
12
|
+
const haystack = target.toLowerCase();
|
|
13
|
+
let patternIndex = 0;
|
|
14
|
+
|
|
15
|
+
for (let targetIndex = 0; targetIndex < haystack.length && patternIndex < needle.length; targetIndex += 1) {
|
|
16
|
+
if (haystack[targetIndex] === needle[patternIndex]) {
|
|
17
|
+
patternIndex += 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return patternIndex === needle.length;
|
|
22
|
+
}
|
package/src/webClient.ts
CHANGED
|
@@ -18,8 +18,11 @@ import {
|
|
|
18
18
|
import type { MyToolsThemePayload } from "./webTypes.ts";
|
|
19
19
|
|
|
20
20
|
export { mytoolsI18n } from "./i18n.ts";
|
|
21
|
+
export { renderHotkeyKeycaps } from "./hotkeyKeycaps.ts";
|
|
21
22
|
export { HostEvents } from "./webTypes.ts";
|
|
22
23
|
export type {
|
|
24
|
+
MyToolsHostActionDefinition,
|
|
25
|
+
MyToolsHostDetailActionPayload,
|
|
23
26
|
MyToolsHostInitializePayload,
|
|
24
27
|
MyToolsHostKeyPayload,
|
|
25
28
|
MyToolsHostSearchPayload,
|
package/src/webTypes.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Routes } from "./protocol.ts";
|
|
2
2
|
|
|
3
|
+
export interface MyToolsHostActionDefinition {
|
|
4
|
+
id: string;
|
|
5
|
+
/** Host-localized action display name. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Host-validated display form, for example `Ctrl+H`; absent means click-only. */
|
|
8
|
+
hotkey?: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export interface MyToolsHostInitializePayload {
|
|
4
12
|
protocolVersion: string;
|
|
5
13
|
pluginId: string;
|
|
@@ -12,6 +20,7 @@ export interface MyToolsHostInitializePayload {
|
|
|
12
20
|
fallbackLocale: string;
|
|
13
21
|
translationRevision: string;
|
|
14
22
|
messages: Record<string, string>;
|
|
23
|
+
actions: MyToolsHostActionDefinition[];
|
|
15
24
|
theme?: string;
|
|
16
25
|
themeTokens?: Record<string, string>;
|
|
17
26
|
}
|
|
@@ -36,6 +45,13 @@ export interface MyToolsHostKeyPayload {
|
|
|
36
45
|
key: string;
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
/** Payload explicitly returned in an action outcome's `web.payload`. */
|
|
49
|
+
export interface MyToolsHostDetailActionPayload {
|
|
50
|
+
actionId?: string;
|
|
51
|
+
action?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
export interface MyToolsInputActionCapturedPayload {
|
|
40
56
|
requestId: string;
|
|
41
57
|
cancelled?: boolean;
|