@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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3 Node SDK bootstrap entry. Reads the bootstrap line from stdin (pipePath\ttoken), connects to
|
|
3
|
+
* the named pipe, completes bus.handshake (presenting the token and receiving bound identity),
|
|
4
|
+
* and starts a HandlerRouter stamped with that identity.
|
|
5
|
+
*
|
|
6
|
+
* This mirrors the C# NodeProcessController's spawn contract: the host writes one line to the
|
|
7
|
+
* Node process's stdin — "<pipePath>\t<token>" — then waits for the Node side to connect the pipe
|
|
8
|
+
* and complete handshake before promoting the session to Ready.
|
|
9
|
+
*/
|
|
10
|
+
import { NodeTransport } from "./transport.ts";
|
|
11
|
+
import { HandlerRouter } from "./router.ts";
|
|
12
|
+
export interface PluginHandlers {
|
|
13
|
+
[route: string]: (payload: any) => Promise<any> | any;
|
|
14
|
+
}
|
|
15
|
+
export interface PluginRuntime {
|
|
16
|
+
transport: NodeTransport;
|
|
17
|
+
router: HandlerRouter;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Connects to the host pipe (reading the bootstrap line from stdin), completes handshake, and
|
|
22
|
+
* returns a runtime whose router dispatches inbound plugin.call.* requests to the given handlers.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runPlugin(handlers: PluginHandlers): Promise<PluginRuntime>;
|
|
25
|
+
/**
|
|
26
|
+
* Sends bus.handshake with the bootstrap token and waits for the host response that binds
|
|
27
|
+
* plugin/entry/session/endpoint identity. Rejects on HandshakeFailed / ProtocolMismatch / timeout.
|
|
28
|
+
*/
|
|
29
|
+
export declare function completeHandshake(transport: NodeTransport, token: string, timeoutMs?: number): Promise<{
|
|
30
|
+
pluginId: string;
|
|
31
|
+
entryId: string;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
endpointId: string;
|
|
34
|
+
}>;
|
package/dist/bootstrap.mjs
CHANGED
|
@@ -110,12 +110,68 @@ 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
|
+
};
|
|
119
175
|
function canonicalStringify(value) {
|
|
120
176
|
return JSON.stringify(stripNulls(value));
|
|
121
177
|
}
|
|
@@ -141,6 +197,9 @@ var NodeTransport = class {
|
|
|
141
197
|
closed = false;
|
|
142
198
|
onMessage(handler) {
|
|
143
199
|
this.messageHandlers.add(handler);
|
|
200
|
+
return () => {
|
|
201
|
+
this.messageHandlers.delete(handler);
|
|
202
|
+
};
|
|
144
203
|
}
|
|
145
204
|
onDisconnect(handler) {
|
|
146
205
|
this.disconnectHandlers.add(handler);
|
|
@@ -207,14 +266,37 @@ var NodeTransport = class {
|
|
|
207
266
|
};
|
|
208
267
|
|
|
209
268
|
// src/router.ts
|
|
269
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
210
270
|
import { randomBytes } from "node:crypto";
|
|
271
|
+
var requestScope = new AsyncLocalStorage();
|
|
272
|
+
var DefaultHostCallTimeoutMs = 3e4;
|
|
273
|
+
function remainingTimeoutMs() {
|
|
274
|
+
const scope = requestScope.getStore();
|
|
275
|
+
if (!scope || scope.deadlineMs == null) {
|
|
276
|
+
return void 0;
|
|
277
|
+
}
|
|
278
|
+
return scope.deadlineMs - Date.now();
|
|
279
|
+
}
|
|
280
|
+
function resolveHostCallTimeoutMs(explicit) {
|
|
281
|
+
const remaining = remainingTimeoutMs();
|
|
282
|
+
if (explicit != null) {
|
|
283
|
+
return remaining == null ? explicit : Math.min(explicit, remaining);
|
|
284
|
+
}
|
|
285
|
+
return remaining ?? DefaultHostCallTimeoutMs;
|
|
286
|
+
}
|
|
287
|
+
function deadlineFromTimeoutMs(timeoutMs) {
|
|
288
|
+
if (typeof timeoutMs !== "number" || timeoutMs <= 0) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
return Date.now() + timeoutMs;
|
|
292
|
+
}
|
|
211
293
|
var HandlerRouter = class {
|
|
212
294
|
handlers = /* @__PURE__ */ new Map();
|
|
213
295
|
pendingHostCalls = /* @__PURE__ */ new Map();
|
|
214
296
|
pluginId = "p";
|
|
215
297
|
entryId = "e";
|
|
216
298
|
sessionId = "s";
|
|
217
|
-
endpointId =
|
|
299
|
+
endpointId = EndpointIds.NodeMain;
|
|
218
300
|
/** Injected transport send fn; tests can override `router.send` directly. */
|
|
219
301
|
send;
|
|
220
302
|
constructor(deps) {
|
|
@@ -232,43 +314,48 @@ var HandlerRouter = class {
|
|
|
232
314
|
}
|
|
233
315
|
/** Dispatches an inbound request/response. Returns once handled. */
|
|
234
316
|
async dispatch(env) {
|
|
235
|
-
if (env.kind ===
|
|
317
|
+
if (env.kind === MessageKind.Response) {
|
|
236
318
|
this.handleHostResponse(env);
|
|
237
319
|
return;
|
|
238
320
|
}
|
|
239
|
-
if (env.kind !==
|
|
240
|
-
if (env.route ===
|
|
321
|
+
if (env.kind !== MessageKind.Request) return;
|
|
322
|
+
if (env.route === Routes.Bus.Ping) {
|
|
241
323
|
this.send(this.responseFor(env, { ok: true }));
|
|
242
324
|
return;
|
|
243
325
|
}
|
|
244
326
|
const handler = this.handlers.get(env.route);
|
|
245
327
|
if (!handler) {
|
|
246
|
-
this.send(this.errorResponseFor(env,
|
|
328
|
+
this.send(this.errorResponseFor(env, ErrorCode.RouteNotFound, `route '${env.route}' has no handler`));
|
|
247
329
|
return;
|
|
248
330
|
}
|
|
331
|
+
const deadlineMs = deadlineFromTimeoutMs(env.timeoutMs);
|
|
249
332
|
try {
|
|
250
|
-
const result = await handler(env.payload);
|
|
333
|
+
const result = await requestScope.run({ deadlineMs }, () => handler(env.payload));
|
|
251
334
|
this.send(this.responseFor(env, result ?? {}));
|
|
252
335
|
} catch (err) {
|
|
253
336
|
const message = err instanceof Error ? err.message : String(err);
|
|
254
|
-
this.send(this.errorResponseFor(env,
|
|
337
|
+
this.send(this.errorResponseFor(env, ErrorCode.InternalError, message));
|
|
255
338
|
}
|
|
256
339
|
}
|
|
257
340
|
/** Calls a host.call.* capability and resolves with the response payload. */
|
|
258
|
-
callHost(route, payload, timeoutMs
|
|
341
|
+
callHost(route, payload, timeoutMs) {
|
|
342
|
+
const effectiveTimeoutMs = resolveHostCallTimeoutMs(timeoutMs);
|
|
343
|
+
if (effectiveTimeoutMs <= 0) {
|
|
344
|
+
return Promise.reject(new Error(`host call ${route} timed out (no time remaining)`));
|
|
345
|
+
}
|
|
259
346
|
return new Promise((resolve, reject) => {
|
|
260
347
|
const id = randomBytesHex();
|
|
261
348
|
const req = {
|
|
262
|
-
version:
|
|
349
|
+
version: ProtocolVersion,
|
|
263
350
|
id,
|
|
264
351
|
traceId: id,
|
|
265
352
|
sessionId: this.sessionId,
|
|
266
353
|
pluginId: this.pluginId,
|
|
267
354
|
entryId: this.entryId,
|
|
268
355
|
endpointId: this.endpointId,
|
|
269
|
-
kind:
|
|
356
|
+
kind: MessageKind.Request,
|
|
270
357
|
route,
|
|
271
|
-
timeoutMs,
|
|
358
|
+
timeoutMs: effectiveTimeoutMs,
|
|
272
359
|
payload
|
|
273
360
|
};
|
|
274
361
|
const pending = { resolve, reject, route };
|
|
@@ -276,9 +363,9 @@ var HandlerRouter = class {
|
|
|
276
363
|
const timer = setTimeout(() => {
|
|
277
364
|
if (this.pendingHostCalls.has(id)) {
|
|
278
365
|
this.pendingHostCalls.delete(id);
|
|
279
|
-
reject(new Error(`host call ${route} timed out after ${
|
|
366
|
+
reject(new Error(`host call ${route} timed out after ${effectiveTimeoutMs}ms`));
|
|
280
367
|
}
|
|
281
|
-
},
|
|
368
|
+
}, effectiveTimeoutMs);
|
|
282
369
|
const origResolve = pending.resolve;
|
|
283
370
|
const origReject = pending.reject;
|
|
284
371
|
pending.resolve = (v) => {
|
|
@@ -305,7 +392,7 @@ var HandlerRouter = class {
|
|
|
305
392
|
}
|
|
306
393
|
responseFor(req, payload) {
|
|
307
394
|
return {
|
|
308
|
-
version:
|
|
395
|
+
version: ProtocolVersion,
|
|
309
396
|
id: randomBytesHex(),
|
|
310
397
|
correlationId: req.id,
|
|
311
398
|
traceId: req.traceId,
|
|
@@ -313,7 +400,7 @@ var HandlerRouter = class {
|
|
|
313
400
|
pluginId: req.pluginId,
|
|
314
401
|
entryId: req.entryId,
|
|
315
402
|
endpointId: this.endpointId,
|
|
316
|
-
kind:
|
|
403
|
+
kind: MessageKind.Response,
|
|
317
404
|
route: req.route,
|
|
318
405
|
payload
|
|
319
406
|
};
|
|
@@ -331,12 +418,29 @@ function randomBytesHex() {
|
|
|
331
418
|
}
|
|
332
419
|
|
|
333
420
|
// src/bootstrap.ts
|
|
421
|
+
var SUPPORTED_VERSIONS = [ProtocolVersion];
|
|
334
422
|
async function runPlugin(handlers) {
|
|
335
423
|
const { pipePath, token } = await readBootstrapLine();
|
|
336
424
|
const transport = new NodeTransport();
|
|
337
425
|
await transport.connect(pipePath);
|
|
426
|
+
const identity = await completeHandshake(transport, token);
|
|
338
427
|
const router = new HandlerRouter({ send: (env) => transport.send(env) });
|
|
428
|
+
router.setIdentity(identity);
|
|
429
|
+
const HOST_LOST_MS = 15e3;
|
|
430
|
+
let lastPingAt = Date.now();
|
|
431
|
+
const watchdog = setInterval(() => {
|
|
432
|
+
if (Date.now() - lastPingAt > HOST_LOST_MS) {
|
|
433
|
+
clearInterval(watchdog);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
}, 1e3);
|
|
437
|
+
watchdog.unref?.();
|
|
438
|
+
transport.onDisconnect(() => {
|
|
439
|
+
clearInterval(watchdog);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
});
|
|
339
442
|
transport.onMessage((env) => {
|
|
443
|
+
if (env.route === Routes.Bus.Ping) lastPingAt = Date.now();
|
|
340
444
|
router.dispatch(env);
|
|
341
445
|
});
|
|
342
446
|
for (const [route, handler] of Object.entries(handlers)) {
|
|
@@ -345,7 +449,10 @@ async function runPlugin(handlers) {
|
|
|
345
449
|
return {
|
|
346
450
|
transport,
|
|
347
451
|
router,
|
|
348
|
-
close: () =>
|
|
452
|
+
close: async () => {
|
|
453
|
+
clearInterval(watchdog);
|
|
454
|
+
await transport.close();
|
|
455
|
+
}
|
|
349
456
|
};
|
|
350
457
|
}
|
|
351
458
|
async function readBootstrapLine() {
|
|
@@ -364,6 +471,53 @@ async function readBootstrapLine() {
|
|
|
364
471
|
rl.close();
|
|
365
472
|
}
|
|
366
473
|
}
|
|
474
|
+
async function completeHandshake(transport, token, timeoutMs = 1e4) {
|
|
475
|
+
const id = randomBytes2(16).toString("hex");
|
|
476
|
+
const req = {
|
|
477
|
+
version: ProtocolVersion,
|
|
478
|
+
id,
|
|
479
|
+
traceId: id,
|
|
480
|
+
sessionId: "",
|
|
481
|
+
pluginId: "",
|
|
482
|
+
entryId: "",
|
|
483
|
+
endpointId: EndpointIds.NodeMain,
|
|
484
|
+
kind: MessageKind.Request,
|
|
485
|
+
route: Routes.Bus.Handshake,
|
|
486
|
+
timeoutMs,
|
|
487
|
+
payload: {
|
|
488
|
+
version: ProtocolVersion,
|
|
489
|
+
supportedVersions: SUPPORTED_VERSIONS,
|
|
490
|
+
token
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
return new Promise((resolve, reject) => {
|
|
494
|
+
const timer = setTimeout(() => {
|
|
495
|
+
unsubscribe();
|
|
496
|
+
reject(new Error(`bus.handshake timed out after ${timeoutMs}ms`));
|
|
497
|
+
}, timeoutMs);
|
|
498
|
+
const unsubscribe = transport.onMessage((env) => {
|
|
499
|
+
if (env.kind !== MessageKind.Response || env.correlationId !== id) return;
|
|
500
|
+
clearTimeout(timer);
|
|
501
|
+
unsubscribe();
|
|
502
|
+
if (env.error) {
|
|
503
|
+
reject(new Error(`${env.error.code}: ${env.error.message}`));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const p = env.payload ?? {};
|
|
507
|
+
const pluginId = String(p.pluginId ?? "");
|
|
508
|
+
const entryId = String(p.entryId ?? "");
|
|
509
|
+
const sessionId = String(p.sessionId ?? "");
|
|
510
|
+
const endpointId = String(p.endpointId ?? EndpointIds.NodeMain);
|
|
511
|
+
if (!pluginId || !entryId || !sessionId) {
|
|
512
|
+
reject(new Error("bus.handshake success response missing bound identity"));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
resolve({ pluginId, entryId, sessionId, endpointId });
|
|
516
|
+
});
|
|
517
|
+
transport.send(req);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
367
520
|
export {
|
|
521
|
+
completeHandshake,
|
|
368
522
|
runPlugin
|
|
369
523
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Length-prefixed framing for the v3 named-pipe transport, mirroring the C# FrameCodec/FrameDecoder.
|
|
3
|
+
* Wire format: [4-byte little-endian unsigned length][UTF-8 JSON payload].
|
|
4
|
+
*
|
|
5
|
+
* The incremental decoder handles fragmented, sticky and truncated streams, and rejects an oversize
|
|
6
|
+
* length prefix as fatal *before* allocating the payload buffer (so a malicious/buggy peer cannot
|
|
7
|
+
* force a huge allocation). After a fatal error the decoder stays dead.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
10
|
+
export declare const PREFIX_BYTES = 4;
|
|
11
|
+
/** Encodes a raw payload buffer into a length-prefixed frame. */
|
|
12
|
+
export declare function encodeFrame(payload: Buffer): Buffer;
|
|
13
|
+
/** Encodes a UTF-8 JSON string into a length-prefixed frame. */
|
|
14
|
+
export declare function encodeFrameString(json: string): Buffer;
|
|
15
|
+
export interface FrameFeedResult {
|
|
16
|
+
hasFrame: boolean;
|
|
17
|
+
payload: Buffer;
|
|
18
|
+
isFatal: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Incremental length-prefixed frame decoder. Feed byte chunks (fragmented/sticky/partial) and get
|
|
22
|
+
* back one complete payload at a time. Leftover bytes from a chunk that contained more than one
|
|
23
|
+
* frame are buffered internally and surfaced by subsequent feeds (including an empty buffer).
|
|
24
|
+
*/
|
|
25
|
+
export declare class FrameDecoder {
|
|
26
|
+
private prefixBuf;
|
|
27
|
+
private prefixFilled;
|
|
28
|
+
private payload;
|
|
29
|
+
private payloadFilled;
|
|
30
|
+
private payloadLength;
|
|
31
|
+
private fatal;
|
|
32
|
+
private pending;
|
|
33
|
+
feed(chunk: Buffer): FrameFeedResult;
|
|
34
|
+
private bufferLeftover;
|
|
35
|
+
private reset;
|
|
36
|
+
}
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type TranslationValues = Record<string, unknown>;
|
|
2
|
+
export type TranslationOptions = TranslationValues & {
|
|
3
|
+
defaultValue: string;
|
|
4
|
+
translatorComment?: string;
|
|
5
|
+
};
|
|
6
|
+
type LocalizationPayload = {
|
|
7
|
+
locale?: string;
|
|
8
|
+
fallbackLocale?: string;
|
|
9
|
+
messages?: Record<string, string>;
|
|
10
|
+
};
|
|
11
|
+
declare class MyToolsI18n {
|
|
12
|
+
#private;
|
|
13
|
+
get language(): string;
|
|
14
|
+
configure(payload: LocalizationPayload | Record<string, unknown> | null | undefined): void;
|
|
15
|
+
t(key: string, options: TranslationOptions): string;
|
|
16
|
+
apply(root?: ParentNode): void;
|
|
17
|
+
}
|
|
18
|
+
export declare const mytoolsI18n: MyToolsI18n;
|
|
19
|
+
export {};
|