@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.
@@ -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
+ }>;
@@ -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 = "node-main";
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 === "response") {
317
+ if (env.kind === MessageKind.Response) {
236
318
  this.handleHostResponse(env);
237
319
  return;
238
320
  }
239
- if (env.kind !== "request") return;
240
- if (env.route === "bus.ping") {
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, "RouteNotFound", `route '${env.route}' has no handler`));
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, "InternalError", message));
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 = 3e4) {
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: "3.0",
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: "request",
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 ${timeoutMs}ms`));
366
+ reject(new Error(`host call ${route} timed out after ${effectiveTimeoutMs}ms`));
280
367
  }
281
- }, timeoutMs);
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: "3.0",
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: "response",
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: () => transport.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 {};