@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/dist/node.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-side plugin SDK: fluent `createPlugin()` over the v3 named-pipe message bus.
|
|
3
|
+
*
|
|
4
|
+
* Method names map to v3 routes:
|
|
5
|
+
* initialize -> plugin.call.initialize
|
|
6
|
+
* search -> plugin.call.search
|
|
7
|
+
* action -> plugin.call.invokeAction
|
|
8
|
+
* handle(name) -> plugin.call.<name>
|
|
9
|
+
* publish -> plugin.event.<subjectId>
|
|
10
|
+
* hostCall -> host.call.<method>
|
|
11
|
+
*/
|
|
12
|
+
export type PluginTheme = "light" | "dark";
|
|
13
|
+
export type PluginHostEnv = {
|
|
14
|
+
locale: string;
|
|
15
|
+
fallbackLocale: string;
|
|
16
|
+
theme: PluginTheme;
|
|
17
|
+
};
|
|
18
|
+
export type PluginContext = PluginHostEnv & {
|
|
19
|
+
action: string;
|
|
20
|
+
itemId: string;
|
|
21
|
+
query: string;
|
|
22
|
+
};
|
|
23
|
+
/** Payload of plugin.call.initialize. Host sends locale, theme, and the resolved message bag. */
|
|
24
|
+
export type PluginInitializeParams = PluginHostEnv & {
|
|
25
|
+
messages: Record<string, string>;
|
|
26
|
+
};
|
|
27
|
+
/** Payload of plugin.call.search. */
|
|
28
|
+
export type PluginSearchParams = PluginHostEnv & {
|
|
29
|
+
query: string;
|
|
30
|
+
mode: "global" | "plugin";
|
|
31
|
+
};
|
|
32
|
+
/** Payload of plugin.call.invokeAction. */
|
|
33
|
+
export type PluginActionParams = PluginHostEnv & {
|
|
34
|
+
itemId: string;
|
|
35
|
+
actionId: string;
|
|
36
|
+
query: string;
|
|
37
|
+
};
|
|
38
|
+
type PluginInitializeHandler = (params: PluginInitializeParams) => unknown | Promise<unknown>;
|
|
39
|
+
type PluginSearchHandler = (params: PluginSearchParams) => unknown | Promise<unknown>;
|
|
40
|
+
type PluginActionHandler = (params: PluginActionParams) => unknown | Promise<unknown>;
|
|
41
|
+
type PluginHandler = (payload: any, context: PluginContext) => unknown | Promise<unknown>;
|
|
42
|
+
export declare class Plugin {
|
|
43
|
+
#private;
|
|
44
|
+
initialize(handler: PluginInitializeHandler): this;
|
|
45
|
+
search(handler: PluginSearchHandler): this;
|
|
46
|
+
action(handler: PluginActionHandler): this;
|
|
47
|
+
handle(action: string, handler: PluginHandler): this;
|
|
48
|
+
/** Publishes a plugin.event.<subjectId> event to all webviews in the session. */
|
|
49
|
+
publish(subjectId: string, payload?: unknown): void;
|
|
50
|
+
/** Calls a host.call.<method> capability and awaits the response.
|
|
51
|
+
* `timeoutMs` defaults to the remaining timeout of the inbound plugin.call
|
|
52
|
+
* (from a page `bus.call`) when inside a handler; otherwise 30s.
|
|
53
|
+
*/
|
|
54
|
+
hostCall(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
|
55
|
+
/** Connects to the host pipe and begins dispatching. Must be called last. */
|
|
56
|
+
start(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Builds the v3 route map from the fluent registrations. Exposed for unit testing the mapping
|
|
59
|
+
* without connecting a pipe.
|
|
60
|
+
*/
|
|
61
|
+
buildRoutes(): Record<string, (payload: any) => unknown | Promise<unknown>>;
|
|
62
|
+
stop(): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
export declare function createPlugin(): Plugin;
|
|
65
|
+
export {};
|
|
@@ -110,12 +110,77 @@ var init_framing = __esm({
|
|
|
110
110
|
|
|
111
111
|
// src/bootstrap.ts
|
|
112
112
|
import readline from "node:readline/promises";
|
|
113
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
113
114
|
|
|
114
115
|
// src/transport.ts
|
|
115
116
|
init_framing();
|
|
116
117
|
import { connect as netConnect } from "node:net";
|
|
117
118
|
|
|
118
119
|
// src/protocol.ts
|
|
120
|
+
var MessageKind = {
|
|
121
|
+
Request: "request",
|
|
122
|
+
Response: "response",
|
|
123
|
+
Event: "event"
|
|
124
|
+
};
|
|
125
|
+
var ErrorCode = {
|
|
126
|
+
ProtocolMismatch: "ProtocolMismatch",
|
|
127
|
+
HandshakeFailed: "HandshakeFailed",
|
|
128
|
+
CapabilityNotDeclared: "CapabilityNotDeclared",
|
|
129
|
+
CapabilityDenied: "CapabilityDenied",
|
|
130
|
+
InvalidPayload: "InvalidPayload",
|
|
131
|
+
MessageTooLarge: "MessageTooLarge",
|
|
132
|
+
RouteNotFound: "RouteNotFound",
|
|
133
|
+
RequestTimeout: "RequestTimeout",
|
|
134
|
+
TooManyRequests: "TooManyRequests",
|
|
135
|
+
TransportDisconnected: "TransportDisconnected",
|
|
136
|
+
PluginUnavailable: "PluginUnavailable",
|
|
137
|
+
InternalError: "InternalError",
|
|
138
|
+
Cancelled: "Cancelled",
|
|
139
|
+
RateLimited: "RateLimited"
|
|
140
|
+
};
|
|
141
|
+
var ProtocolVersion = "3.0";
|
|
142
|
+
var EndpointIds = {
|
|
143
|
+
NodeMain: "node-main",
|
|
144
|
+
Host: "host"
|
|
145
|
+
};
|
|
146
|
+
var Routes = {
|
|
147
|
+
Bus: {
|
|
148
|
+
Handshake: "bus.handshake",
|
|
149
|
+
Ping: "bus.ping",
|
|
150
|
+
Cancel: "bus.cancel",
|
|
151
|
+
Subscribe: "bus.subscribe",
|
|
152
|
+
Unsubscribe: "bus.unsubscribe"
|
|
153
|
+
},
|
|
154
|
+
Prefix: {
|
|
155
|
+
PluginCall: "plugin.call.",
|
|
156
|
+
HostCall: "host.call.",
|
|
157
|
+
PluginEvent: "plugin.event.",
|
|
158
|
+
HostEvent: "host.event.",
|
|
159
|
+
Diagnostics: "diagnostics."
|
|
160
|
+
},
|
|
161
|
+
PluginCall: {
|
|
162
|
+
Initialize: "plugin.call.initialize",
|
|
163
|
+
Search: "plugin.call.search",
|
|
164
|
+
InvokeAction: "plugin.call.invokeAction"
|
|
165
|
+
},
|
|
166
|
+
HostEvent: {
|
|
167
|
+
Initialize: "host.event.initialize",
|
|
168
|
+
Search: "host.event.search",
|
|
169
|
+
Key: "host.event.key",
|
|
170
|
+
LanguageChanged: "host.event.languageChanged",
|
|
171
|
+
ThemeChanged: "host.event.themeChanged",
|
|
172
|
+
InputActionCaptured: "host.event.inputActionCaptured"
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function pluginCallRoute(method) {
|
|
176
|
+
return method.startsWith(Routes.Prefix.PluginCall) ? method : `${Routes.Prefix.PluginCall}${method}`;
|
|
177
|
+
}
|
|
178
|
+
function hostCallRoute(method) {
|
|
179
|
+
return method.startsWith(Routes.Prefix.HostCall) ? method : `${Routes.Prefix.HostCall}${method}`;
|
|
180
|
+
}
|
|
181
|
+
function pluginEventRoute(subjectId) {
|
|
182
|
+
return subjectId.startsWith(Routes.Prefix.PluginEvent) ? subjectId : `${Routes.Prefix.PluginEvent}${subjectId}`;
|
|
183
|
+
}
|
|
119
184
|
function canonicalStringify(value) {
|
|
120
185
|
return JSON.stringify(stripNulls(value));
|
|
121
186
|
}
|
|
@@ -141,6 +206,9 @@ var NodeTransport = class {
|
|
|
141
206
|
closed = false;
|
|
142
207
|
onMessage(handler) {
|
|
143
208
|
this.messageHandlers.add(handler);
|
|
209
|
+
return () => {
|
|
210
|
+
this.messageHandlers.delete(handler);
|
|
211
|
+
};
|
|
144
212
|
}
|
|
145
213
|
onDisconnect(handler) {
|
|
146
214
|
this.disconnectHandlers.add(handler);
|
|
@@ -207,14 +275,37 @@ var NodeTransport = class {
|
|
|
207
275
|
};
|
|
208
276
|
|
|
209
277
|
// src/router.ts
|
|
278
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
210
279
|
import { randomBytes } from "node:crypto";
|
|
280
|
+
var requestScope = new AsyncLocalStorage();
|
|
281
|
+
var DefaultHostCallTimeoutMs = 3e4;
|
|
282
|
+
function remainingTimeoutMs() {
|
|
283
|
+
const scope = requestScope.getStore();
|
|
284
|
+
if (!scope || scope.deadlineMs == null) {
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
return scope.deadlineMs - Date.now();
|
|
288
|
+
}
|
|
289
|
+
function resolveHostCallTimeoutMs(explicit) {
|
|
290
|
+
const remaining = remainingTimeoutMs();
|
|
291
|
+
if (explicit != null) {
|
|
292
|
+
return remaining == null ? explicit : Math.min(explicit, remaining);
|
|
293
|
+
}
|
|
294
|
+
return remaining ?? DefaultHostCallTimeoutMs;
|
|
295
|
+
}
|
|
296
|
+
function deadlineFromTimeoutMs(timeoutMs) {
|
|
297
|
+
if (typeof timeoutMs !== "number" || timeoutMs <= 0) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return Date.now() + timeoutMs;
|
|
301
|
+
}
|
|
211
302
|
var HandlerRouter = class {
|
|
212
303
|
handlers = /* @__PURE__ */ new Map();
|
|
213
304
|
pendingHostCalls = /* @__PURE__ */ new Map();
|
|
214
305
|
pluginId = "p";
|
|
215
306
|
entryId = "e";
|
|
216
307
|
sessionId = "s";
|
|
217
|
-
endpointId =
|
|
308
|
+
endpointId = EndpointIds.NodeMain;
|
|
218
309
|
/** Injected transport send fn; tests can override `router.send` directly. */
|
|
219
310
|
send;
|
|
220
311
|
constructor(deps) {
|
|
@@ -232,43 +323,48 @@ var HandlerRouter = class {
|
|
|
232
323
|
}
|
|
233
324
|
/** Dispatches an inbound request/response. Returns once handled. */
|
|
234
325
|
async dispatch(env) {
|
|
235
|
-
if (env.kind ===
|
|
326
|
+
if (env.kind === MessageKind.Response) {
|
|
236
327
|
this.handleHostResponse(env);
|
|
237
328
|
return;
|
|
238
329
|
}
|
|
239
|
-
if (env.kind !==
|
|
240
|
-
if (env.route ===
|
|
330
|
+
if (env.kind !== MessageKind.Request) return;
|
|
331
|
+
if (env.route === Routes.Bus.Ping) {
|
|
241
332
|
this.send(this.responseFor(env, { ok: true }));
|
|
242
333
|
return;
|
|
243
334
|
}
|
|
244
335
|
const handler = this.handlers.get(env.route);
|
|
245
336
|
if (!handler) {
|
|
246
|
-
this.send(this.errorResponseFor(env,
|
|
337
|
+
this.send(this.errorResponseFor(env, ErrorCode.RouteNotFound, `route '${env.route}' has no handler`));
|
|
247
338
|
return;
|
|
248
339
|
}
|
|
340
|
+
const deadlineMs = deadlineFromTimeoutMs(env.timeoutMs);
|
|
249
341
|
try {
|
|
250
|
-
const result = await handler(env.payload);
|
|
342
|
+
const result = await requestScope.run({ deadlineMs }, () => handler(env.payload));
|
|
251
343
|
this.send(this.responseFor(env, result ?? {}));
|
|
252
344
|
} catch (err) {
|
|
253
345
|
const message = err instanceof Error ? err.message : String(err);
|
|
254
|
-
this.send(this.errorResponseFor(env,
|
|
346
|
+
this.send(this.errorResponseFor(env, ErrorCode.InternalError, message));
|
|
255
347
|
}
|
|
256
348
|
}
|
|
257
349
|
/** Calls a host.call.* capability and resolves with the response payload. */
|
|
258
|
-
callHost(route, payload, timeoutMs
|
|
350
|
+
callHost(route, payload, timeoutMs) {
|
|
351
|
+
const effectiveTimeoutMs = resolveHostCallTimeoutMs(timeoutMs);
|
|
352
|
+
if (effectiveTimeoutMs <= 0) {
|
|
353
|
+
return Promise.reject(new Error(`host call ${route} timed out (no time remaining)`));
|
|
354
|
+
}
|
|
259
355
|
return new Promise((resolve, reject) => {
|
|
260
356
|
const id = randomBytesHex();
|
|
261
357
|
const req = {
|
|
262
|
-
version:
|
|
358
|
+
version: ProtocolVersion,
|
|
263
359
|
id,
|
|
264
360
|
traceId: id,
|
|
265
361
|
sessionId: this.sessionId,
|
|
266
362
|
pluginId: this.pluginId,
|
|
267
363
|
entryId: this.entryId,
|
|
268
364
|
endpointId: this.endpointId,
|
|
269
|
-
kind:
|
|
365
|
+
kind: MessageKind.Request,
|
|
270
366
|
route,
|
|
271
|
-
timeoutMs,
|
|
367
|
+
timeoutMs: effectiveTimeoutMs,
|
|
272
368
|
payload
|
|
273
369
|
};
|
|
274
370
|
const pending = { resolve, reject, route };
|
|
@@ -276,9 +372,9 @@ var HandlerRouter = class {
|
|
|
276
372
|
const timer = setTimeout(() => {
|
|
277
373
|
if (this.pendingHostCalls.has(id)) {
|
|
278
374
|
this.pendingHostCalls.delete(id);
|
|
279
|
-
reject(new Error(`host call ${route} timed out after ${
|
|
375
|
+
reject(new Error(`host call ${route} timed out after ${effectiveTimeoutMs}ms`));
|
|
280
376
|
}
|
|
281
|
-
},
|
|
377
|
+
}, effectiveTimeoutMs);
|
|
282
378
|
const origResolve = pending.resolve;
|
|
283
379
|
const origReject = pending.reject;
|
|
284
380
|
pending.resolve = (v) => {
|
|
@@ -305,7 +401,7 @@ var HandlerRouter = class {
|
|
|
305
401
|
}
|
|
306
402
|
responseFor(req, payload) {
|
|
307
403
|
return {
|
|
308
|
-
version:
|
|
404
|
+
version: ProtocolVersion,
|
|
309
405
|
id: randomBytesHex(),
|
|
310
406
|
correlationId: req.id,
|
|
311
407
|
traceId: req.traceId,
|
|
@@ -313,7 +409,7 @@ var HandlerRouter = class {
|
|
|
313
409
|
pluginId: req.pluginId,
|
|
314
410
|
entryId: req.entryId,
|
|
315
411
|
endpointId: this.endpointId,
|
|
316
|
-
kind:
|
|
412
|
+
kind: MessageKind.Response,
|
|
317
413
|
route: req.route,
|
|
318
414
|
payload
|
|
319
415
|
};
|
|
@@ -331,12 +427,29 @@ function randomBytesHex() {
|
|
|
331
427
|
}
|
|
332
428
|
|
|
333
429
|
// src/bootstrap.ts
|
|
430
|
+
var SUPPORTED_VERSIONS = [ProtocolVersion];
|
|
334
431
|
async function runPlugin(handlers) {
|
|
335
432
|
const { pipePath, token } = await readBootstrapLine();
|
|
336
433
|
const transport = new NodeTransport();
|
|
337
434
|
await transport.connect(pipePath);
|
|
435
|
+
const identity = await completeHandshake(transport, token);
|
|
338
436
|
const router = new HandlerRouter({ send: (env) => transport.send(env) });
|
|
437
|
+
router.setIdentity(identity);
|
|
438
|
+
const HOST_LOST_MS = 15e3;
|
|
439
|
+
let lastPingAt = Date.now();
|
|
440
|
+
const watchdog = setInterval(() => {
|
|
441
|
+
if (Date.now() - lastPingAt > HOST_LOST_MS) {
|
|
442
|
+
clearInterval(watchdog);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
}, 1e3);
|
|
446
|
+
watchdog.unref?.();
|
|
447
|
+
transport.onDisconnect(() => {
|
|
448
|
+
clearInterval(watchdog);
|
|
449
|
+
process.exit(1);
|
|
450
|
+
});
|
|
339
451
|
transport.onMessage((env) => {
|
|
452
|
+
if (env.route === Routes.Bus.Ping) lastPingAt = Date.now();
|
|
340
453
|
router.dispatch(env);
|
|
341
454
|
});
|
|
342
455
|
for (const [route, handler] of Object.entries(handlers)) {
|
|
@@ -345,7 +458,10 @@ async function runPlugin(handlers) {
|
|
|
345
458
|
return {
|
|
346
459
|
transport,
|
|
347
460
|
router,
|
|
348
|
-
close: () =>
|
|
461
|
+
close: async () => {
|
|
462
|
+
clearInterval(watchdog);
|
|
463
|
+
await transport.close();
|
|
464
|
+
}
|
|
349
465
|
};
|
|
350
466
|
}
|
|
351
467
|
async function readBootstrapLine() {
|
|
@@ -364,9 +480,55 @@ async function readBootstrapLine() {
|
|
|
364
480
|
rl.close();
|
|
365
481
|
}
|
|
366
482
|
}
|
|
483
|
+
async function completeHandshake(transport, token, timeoutMs = 1e4) {
|
|
484
|
+
const id = randomBytes2(16).toString("hex");
|
|
485
|
+
const req = {
|
|
486
|
+
version: ProtocolVersion,
|
|
487
|
+
id,
|
|
488
|
+
traceId: id,
|
|
489
|
+
sessionId: "",
|
|
490
|
+
pluginId: "",
|
|
491
|
+
entryId: "",
|
|
492
|
+
endpointId: EndpointIds.NodeMain,
|
|
493
|
+
kind: MessageKind.Request,
|
|
494
|
+
route: Routes.Bus.Handshake,
|
|
495
|
+
timeoutMs,
|
|
496
|
+
payload: {
|
|
497
|
+
version: ProtocolVersion,
|
|
498
|
+
supportedVersions: SUPPORTED_VERSIONS,
|
|
499
|
+
token
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
return new Promise((resolve, reject) => {
|
|
503
|
+
const timer = setTimeout(() => {
|
|
504
|
+
unsubscribe();
|
|
505
|
+
reject(new Error(`bus.handshake timed out after ${timeoutMs}ms`));
|
|
506
|
+
}, timeoutMs);
|
|
507
|
+
const unsubscribe = transport.onMessage((env) => {
|
|
508
|
+
if (env.kind !== MessageKind.Response || env.correlationId !== id) return;
|
|
509
|
+
clearTimeout(timer);
|
|
510
|
+
unsubscribe();
|
|
511
|
+
if (env.error) {
|
|
512
|
+
reject(new Error(`${env.error.code}: ${env.error.message}`));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const p = env.payload ?? {};
|
|
516
|
+
const pluginId = String(p.pluginId ?? "");
|
|
517
|
+
const entryId = String(p.entryId ?? "");
|
|
518
|
+
const sessionId = String(p.sessionId ?? "");
|
|
519
|
+
const endpointId = String(p.endpointId ?? EndpointIds.NodeMain);
|
|
520
|
+
if (!pluginId || !entryId || !sessionId) {
|
|
521
|
+
reject(new Error("bus.handshake success response missing bound identity"));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
resolve({ pluginId, entryId, sessionId, endpointId });
|
|
525
|
+
});
|
|
526
|
+
transport.send(req);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
367
529
|
|
|
368
|
-
// src/
|
|
369
|
-
var
|
|
530
|
+
// src/node.ts
|
|
531
|
+
var Plugin = class {
|
|
370
532
|
#handlers = /* @__PURE__ */ new Map();
|
|
371
533
|
#searchHandler = null;
|
|
372
534
|
#actionHandler = null;
|
|
@@ -386,35 +548,38 @@ var NodeTool = class {
|
|
|
386
548
|
}
|
|
387
549
|
handle(action, handler) {
|
|
388
550
|
if (!action || typeof action !== "string") {
|
|
389
|
-
throw new Error("
|
|
551
|
+
throw new Error("plugin.handle requires an action name.");
|
|
390
552
|
}
|
|
391
553
|
if (typeof handler !== "function") {
|
|
392
|
-
throw new Error("
|
|
554
|
+
throw new Error("plugin.handle requires a handler.");
|
|
393
555
|
}
|
|
394
556
|
this.#handlers.set(action, handler);
|
|
395
557
|
return this;
|
|
396
558
|
}
|
|
397
559
|
/** Publishes a plugin.event.<subjectId> event to all webviews in the session. */
|
|
398
560
|
publish(subjectId, payload = {}) {
|
|
399
|
-
if (!this.#runtime) throw new Error("
|
|
400
|
-
const route = subjectId
|
|
561
|
+
if (!this.#runtime) throw new Error("plugin not started");
|
|
562
|
+
const route = pluginEventRoute(subjectId);
|
|
401
563
|
this.#runtime.transport.send({
|
|
402
|
-
version:
|
|
564
|
+
version: ProtocolVersion,
|
|
403
565
|
id: crypto.randomUUID().replace(/-/g, "").slice(0, 32),
|
|
404
566
|
traceId: crypto.randomUUID().replace(/-/g, "").slice(0, 32),
|
|
405
567
|
sessionId: "",
|
|
406
568
|
pluginId: "",
|
|
407
569
|
entryId: "",
|
|
408
|
-
endpointId:
|
|
409
|
-
kind:
|
|
570
|
+
endpointId: EndpointIds.NodeMain,
|
|
571
|
+
kind: MessageKind.Event,
|
|
410
572
|
route,
|
|
411
573
|
payload
|
|
412
574
|
});
|
|
413
575
|
}
|
|
414
|
-
/** Calls a host.call.<method> capability and awaits the response.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
576
|
+
/** Calls a host.call.<method> capability and awaits the response.
|
|
577
|
+
* `timeoutMs` defaults to the remaining timeout of the inbound plugin.call
|
|
578
|
+
* (from a page `bus.call`) when inside a handler; otherwise 30s.
|
|
579
|
+
*/
|
|
580
|
+
hostCall(method, params = {}, timeoutMs) {
|
|
581
|
+
if (!this.#runtime) return Promise.reject(new Error("plugin not started"));
|
|
582
|
+
return this.#runtime.router.callHost(hostCallRoute(method), params, timeoutMs);
|
|
418
583
|
}
|
|
419
584
|
/** Connects to the host pipe and begins dispatching. Must be called last. */
|
|
420
585
|
async start() {
|
|
@@ -428,35 +593,20 @@ var NodeTool = class {
|
|
|
428
593
|
buildRoutes() {
|
|
429
594
|
const routes = {};
|
|
430
595
|
if (this.#initializeHandler) {
|
|
431
|
-
routes[
|
|
596
|
+
routes[Routes.PluginCall.Initialize] = (p) => this.#initializeHandler(asInitializeParams(p));
|
|
432
597
|
}
|
|
433
598
|
if (this.#searchHandler) {
|
|
434
|
-
routes[
|
|
599
|
+
routes[Routes.PluginCall.Search] = (p) => this.#searchHandler(asSearchParams(p));
|
|
435
600
|
}
|
|
436
601
|
if (this.#actionHandler) {
|
|
437
|
-
routes[
|
|
438
|
-
return this.#actionHandler({ ...p, itemId: p.itemId, query: p.query });
|
|
439
|
-
};
|
|
602
|
+
routes[Routes.PluginCall.InvokeAction] = (p) => this.#actionHandler(asActionParams(p));
|
|
440
603
|
}
|
|
441
|
-
routes["plugin.call.detailCall"] = async (p) => {
|
|
442
|
-
const action = p?.action ?? "";
|
|
443
|
-
const handler = this.#handlers.get(action);
|
|
444
|
-
if (!handler) {
|
|
445
|
-
throw new Error(`no handler registered for action '${action}'`);
|
|
446
|
-
}
|
|
447
|
-
const ctx = extractContext(p);
|
|
448
|
-
const result = await handler(p?.payload ?? {}, ctx);
|
|
449
|
-
return { result: result ?? {} };
|
|
450
|
-
};
|
|
451
|
-
routes["plugin.call.detailEvent"] = async (p) => {
|
|
452
|
-
return { state: p?.payload ?? {} };
|
|
453
|
-
};
|
|
454
604
|
for (const [action, handler] of this.#handlers) {
|
|
455
|
-
const route =
|
|
605
|
+
const route = pluginCallRoute(action);
|
|
456
606
|
if (!routes[route]) {
|
|
457
607
|
routes[route] = async (p) => {
|
|
458
|
-
const ctx = extractContext(p);
|
|
459
|
-
return handler(p, ctx);
|
|
608
|
+
const ctx = extractContext(p, action);
|
|
609
|
+
return handler(p ?? {}, ctx);
|
|
460
610
|
};
|
|
461
611
|
}
|
|
462
612
|
}
|
|
@@ -466,19 +616,53 @@ var NodeTool = class {
|
|
|
466
616
|
if (this.#runtime) await this.#runtime.close();
|
|
467
617
|
}
|
|
468
618
|
};
|
|
469
|
-
function
|
|
619
|
+
function asHostEnv(p) {
|
|
620
|
+
return {
|
|
621
|
+
locale: typeof p?.locale === "string" ? p.locale : "en-US",
|
|
622
|
+
fallbackLocale: typeof p?.fallbackLocale === "string" ? p.fallbackLocale : "en-US",
|
|
623
|
+
theme: asTheme(p?.theme)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function asInitializeParams(p) {
|
|
627
|
+
const messages = p?.messages;
|
|
628
|
+
return {
|
|
629
|
+
...asHostEnv(p),
|
|
630
|
+
messages: isStringRecord(messages) ? messages : {}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function asSearchParams(p) {
|
|
634
|
+
return {
|
|
635
|
+
...asHostEnv(p),
|
|
636
|
+
query: typeof p?.query === "string" ? p.query : "",
|
|
637
|
+
mode: p?.mode === "plugin" ? "plugin" : "global"
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function asActionParams(p) {
|
|
641
|
+
return {
|
|
642
|
+
...asHostEnv(p),
|
|
643
|
+
itemId: typeof p?.itemId === "string" ? p.itemId : "",
|
|
644
|
+
actionId: typeof p?.actionId === "string" ? p.actionId : "",
|
|
645
|
+
query: typeof p?.query === "string" ? p.query : ""
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function asTheme(value) {
|
|
649
|
+
return value === "light" ? "light" : "dark";
|
|
650
|
+
}
|
|
651
|
+
function isStringRecord(value) {
|
|
652
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
653
|
+
}
|
|
654
|
+
function extractContext(p, action) {
|
|
470
655
|
return {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
fallbackLocale: p?.fallbackLocale ?? "en-US"
|
|
656
|
+
...asHostEnv(p),
|
|
657
|
+
action,
|
|
658
|
+
itemId: typeof p?.itemId === "string" ? p.itemId : "",
|
|
659
|
+
query: typeof p?.query === "string" ? p.query : ""
|
|
476
660
|
};
|
|
477
661
|
}
|
|
478
|
-
function
|
|
479
|
-
return new
|
|
662
|
+
function createPlugin() {
|
|
663
|
+
return new Plugin();
|
|
480
664
|
}
|
|
481
665
|
export {
|
|
482
|
-
|
|
483
|
-
|
|
666
|
+
Plugin,
|
|
667
|
+
createPlugin
|
|
484
668
|
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written TypeScript protocol types for the plugin message bus v3. These MUST stay
|
|
3
|
+
* byte-for-byte aligned with the C# types in MyTools.Protocol (see the canonical fixtures in
|
|
4
|
+
* MyTools.Protocol.Test/Fixtures/*.json). The drift-prevention self-check
|
|
5
|
+
* (fixtures-selfcheck.mjs) encodes/decodes those fixtures through these types.
|
|
6
|
+
*
|
|
7
|
+
* Field names are camelCase on the wire (System.Text.Json camelCase policy on the C# side).
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MessageKind: {
|
|
14
|
+
readonly Request: "request";
|
|
15
|
+
readonly Response: "response";
|
|
16
|
+
readonly Event: "event";
|
|
17
|
+
};
|
|
18
|
+
export type MessageKind = (typeof MessageKind)[keyof typeof MessageKind];
|
|
19
|
+
export declare const ErrorCode: {
|
|
20
|
+
readonly ProtocolMismatch: "ProtocolMismatch";
|
|
21
|
+
readonly HandshakeFailed: "HandshakeFailed";
|
|
22
|
+
readonly CapabilityNotDeclared: "CapabilityNotDeclared";
|
|
23
|
+
readonly CapabilityDenied: "CapabilityDenied";
|
|
24
|
+
readonly InvalidPayload: "InvalidPayload";
|
|
25
|
+
readonly MessageTooLarge: "MessageTooLarge";
|
|
26
|
+
readonly RouteNotFound: "RouteNotFound";
|
|
27
|
+
readonly RequestTimeout: "RequestTimeout";
|
|
28
|
+
readonly TooManyRequests: "TooManyRequests";
|
|
29
|
+
readonly TransportDisconnected: "TransportDisconnected";
|
|
30
|
+
readonly PluginUnavailable: "PluginUnavailable";
|
|
31
|
+
readonly InternalError: "InternalError";
|
|
32
|
+
readonly Cancelled: "Cancelled";
|
|
33
|
+
readonly RateLimited: "RateLimited";
|
|
34
|
+
};
|
|
35
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
36
|
+
export declare const ProtocolVersion = "3.0";
|
|
37
|
+
export declare const EndpointIds: {
|
|
38
|
+
readonly NodeMain: "node-main";
|
|
39
|
+
readonly Host: "host";
|
|
40
|
+
};
|
|
41
|
+
export declare const Routes: {
|
|
42
|
+
readonly Bus: {
|
|
43
|
+
readonly Handshake: "bus.handshake";
|
|
44
|
+
readonly Ping: "bus.ping";
|
|
45
|
+
readonly Cancel: "bus.cancel";
|
|
46
|
+
readonly Subscribe: "bus.subscribe";
|
|
47
|
+
readonly Unsubscribe: "bus.unsubscribe";
|
|
48
|
+
};
|
|
49
|
+
readonly Prefix: {
|
|
50
|
+
readonly PluginCall: "plugin.call.";
|
|
51
|
+
readonly HostCall: "host.call.";
|
|
52
|
+
readonly PluginEvent: "plugin.event.";
|
|
53
|
+
readonly HostEvent: "host.event.";
|
|
54
|
+
readonly Diagnostics: "diagnostics.";
|
|
55
|
+
};
|
|
56
|
+
readonly PluginCall: {
|
|
57
|
+
readonly Initialize: "plugin.call.initialize";
|
|
58
|
+
readonly Search: "plugin.call.search";
|
|
59
|
+
readonly InvokeAction: "plugin.call.invokeAction";
|
|
60
|
+
};
|
|
61
|
+
readonly HostEvent: {
|
|
62
|
+
readonly Initialize: "host.event.initialize";
|
|
63
|
+
readonly Search: "host.event.search";
|
|
64
|
+
readonly Key: "host.event.key";
|
|
65
|
+
readonly LanguageChanged: "host.event.languageChanged";
|
|
66
|
+
readonly ThemeChanged: "host.event.themeChanged";
|
|
67
|
+
readonly InputActionCaptured: "host.event.inputActionCaptured";
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export declare function pluginCallRoute(method: string): string;
|
|
71
|
+
export declare function hostCallRoute(method: string): string;
|
|
72
|
+
export declare function pluginEventRoute(subjectId: string): string;
|
|
73
|
+
export interface BusError {
|
|
74
|
+
code: ErrorCode;
|
|
75
|
+
message: string;
|
|
76
|
+
retryable: boolean;
|
|
77
|
+
details?: unknown;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The frozen Phase-1 envelope. All fields except correlationId/timeoutMs/error/payload are
|
|
81
|
+
* required; the optional ones are omitted on the wire when null.
|
|
82
|
+
*/
|
|
83
|
+
export interface Envelope {
|
|
84
|
+
version: string;
|
|
85
|
+
id: string;
|
|
86
|
+
correlationId?: string | null;
|
|
87
|
+
traceId: string;
|
|
88
|
+
sessionId: string;
|
|
89
|
+
pluginId: string;
|
|
90
|
+
entryId: string;
|
|
91
|
+
endpointId: string;
|
|
92
|
+
kind: MessageKind;
|
|
93
|
+
route: string;
|
|
94
|
+
timeoutMs?: number | null;
|
|
95
|
+
payload?: unknown;
|
|
96
|
+
error?: BusError | null;
|
|
97
|
+
}
|
|
98
|
+
/** Omit null/undefined-valued keys to match the C# WhenWritingNull behavior. */
|
|
99
|
+
export declare function canonicalStringify(value: unknown): string;
|
|
100
|
+
/** Parse + re-canonicalize, returning the canonical JSON string (stable key order via JSON.stringify). */
|
|
101
|
+
export declare function canonicalize(json: string): string;
|