@monosmith/plugin-sdk 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.
Files changed (39) hide show
  1. package/README.md +3 -2
  2. package/codec/index.d.ts +1 -1
  3. package/codec/index.js +1 -1
  4. package/codec/method-codec.algorithm.js +4 -4
  5. package/codec/wire-message.mapper.d.ts +3 -1
  6. package/codec/wire-message.mapper.js +5 -1
  7. package/concurrency/concurrency.policy.d.ts +21 -0
  8. package/concurrency/concurrency.policy.js +30 -0
  9. package/concurrency/index.d.ts +1 -0
  10. package/concurrency/index.js +1 -0
  11. package/conformance/check-plugin.use-case.d.ts +2 -1
  12. package/conformance/check-plugin.use-case.js +4 -1
  13. package/conformance/check-scheduling.use-case.d.ts +11 -0
  14. package/conformance/check-scheduling.use-case.js +98 -0
  15. package/host/call-interrupted.error.d.ts +16 -0
  16. package/host/call-interrupted.error.js +20 -0
  17. package/host/host-options.contract.d.ts +6 -0
  18. package/host/host-options.contract.js +2 -0
  19. package/host/index.d.ts +3 -1
  20. package/host/index.js +2 -1
  21. package/host/plugin-client.client.d.ts +26 -7
  22. package/host/plugin-client.client.js +180 -49
  23. package/index.d.ts +1 -0
  24. package/index.js +1 -0
  25. package/package.json +1 -1
  26. package/protocol/protocol-schema.contract.js +68 -17
  27. package/protocol/protocol-types.contract.d.ts +40 -0
  28. package/protocol/protocol-types.contract.js +7 -0
  29. package/protocol/schema-document.contract.d.ts +4 -1
  30. package/serve/capabilities-of.algorithm.d.ts +2 -1
  31. package/serve/capabilities-of.algorithm.js +4 -1
  32. package/serve/dispatch-request.use-case.d.ts +3 -2
  33. package/serve/dispatch-request.use-case.js +4 -3
  34. package/serve/index.d.ts +1 -1
  35. package/serve/plugin.contract.d.ts +31 -16
  36. package/serve/request-pool.store.d.ts +49 -0
  37. package/serve/request-pool.store.js +110 -0
  38. package/serve/serve.use-case.d.ts +8 -4
  39. package/serve/serve.use-case.js +109 -22
package/README.md CHANGED
@@ -68,8 +68,9 @@ serve (`PLUGIN_MEMBERS`), so a new method has to be given its place in the plugi
68
68
  | `protocol` | the generated types, the schema as data, `RpcError` |
69
69
  | `codec` | the wire form of the messages: key order, `omitempty`, sorted maps and escaping as the Go SDK writes them, so the bytes are equal; params shaped as the types promise |
70
70
  | `framing` | `Content-Length` framing over a byte stream |
71
- | `serve` | `serve`, `serveStreams`, the plugin interfaces, capability derivation |
72
- | `host` | `startPlugin`, `connectPlugin`, `connectInProcess`, `PluginClient`: the host side, for tests |
71
+ | `serve` | `serve`, `serveStreams` (a bounded pool of workers, `cancel` and the plugin's `concurrency`), the plugin interfaces, capability derivation |
72
+ | `concurrency` | what a declared `concurrency` means: one at a time by default, a count, or unlimited |
73
+ | `host` | `startPlugin`, `connectPlugin`, `connectInProcess`, `PluginClient`: the host side, for tests; it keeps to the plugin's concurrency and cancels a call with an `AbortSignal` |
73
74
  | `conformance` | `checkPlugin`: the checks of `smith plugin check` |
74
75
  | `templating`, `jsondoc` | ports of the Go SDK's `templating` and `jsondoc` |
75
76
  | `tasks`, `scaffold`, `ci` | helpers to build the answers of contribute, generate and ci |
package/codec/index.d.ts CHANGED
@@ -2,5 +2,5 @@ export { decodeParams, decodeResult, encodeParams, encodeResult } from './method
2
2
  export { EncodeError } from './encode-error.error.ts';
3
3
  export { ParamsError } from './params-error.error.ts';
4
4
  export { MessageParseError } from './message-parse.error.ts';
5
- export { parseRequest, parseResponse, writeFailure, writeRequest, writeResult } from './wire-message.mapper.ts';
5
+ export { parseRequest, parseResponse, writeFailure, writeNotification, writeRequest, writeResult } from './wire-message.mapper.ts';
6
6
  export type { ReceivedRequest, ReceivedResponse } from './wire-message.mapper.ts';
package/codec/index.js CHANGED
@@ -2,4 +2,4 @@ export { decodeParams, decodeResult, encodeParams, encodeResult } from './method
2
2
  export { EncodeError } from './encode-error.error.js';
3
3
  export { ParamsError } from './params-error.error.js';
4
4
  export { MessageParseError } from './message-parse.error.js';
5
- export { parseRequest, parseResponse, writeFailure, writeRequest, writeResult } from './wire-message.mapper.js';
5
+ export { parseRequest, parseResponse, writeFailure, writeNotification, writeRequest, writeResult } from './wire-message.mapper.js';
@@ -22,10 +22,10 @@ export function decodeResult(method, result) {
22
22
  return decode(method, 'result', result ?? {});
23
23
  }
24
24
  function encode(method, side, value) {
25
- const row = rowOf(method);
26
- return row === undefined ? encodeJsonValue(value, side) : encodeBySchema(protocolSchema, row[side], value, side);
25
+ const schema = rowOf(method)?.[side];
26
+ return schema === undefined || schema === null ? encodeJsonValue(value, side) : encodeBySchema(protocolSchema, schema, value, side);
27
27
  }
28
28
  function decode(method, side, value) {
29
- const row = rowOf(method);
30
- return row === undefined ? value : normalizeBySchema(protocolSchema, row[side], value, side);
29
+ const schema = rowOf(method)?.[side];
30
+ return schema === undefined || schema === null ? value : normalizeBySchema(protocolSchema, schema, value, side);
31
31
  }
@@ -15,7 +15,9 @@ export interface ReceivedResponse {
15
15
  }
16
16
  /** A request as the Go SDK writes it: `jsonrpc`, `id`, `method`, `params`, the params left out when there are none. */
17
17
  export declare function writeRequest(id: number, method: string, paramsJson: string | undefined): string;
18
- /** A successful response: `id` is `null` only when the request could not be read. */
18
+ /** A notification as the Go SDK writes it: a request with no `id`, which gets no response. */
19
+ export declare function writeNotification(method: string, paramsJson: string): string;
20
+ /** A successful response:`id` is `null` only when the request could not be read. */
19
21
  export declare function writeResult(id: number | null, resultJson: string): string;
20
22
  /** An error response. */
21
23
  export declare function writeFailure(id: number | null, code: number, message: string): string;
@@ -5,7 +5,11 @@ export function writeRequest(id, method, paramsJson) {
5
5
  const params = paramsJson === undefined ? '' : `,"params":${paramsJson}`;
6
6
  return `{"jsonrpc":"2.0","id":${id},"method":${encodeJsonString(method)}${params}}`;
7
7
  }
8
- /** A successful response: `id` is `null` only when the request could not be read. */
8
+ /** A notification as the Go SDK writes it: a request with no `id`, which gets no response. */
9
+ export function writeNotification(method, paramsJson) {
10
+ return `{"jsonrpc":"2.0","method":${encodeJsonString(method)},"params":${paramsJson}}`;
11
+ }
12
+ /** A successful response:`id` is `null` only when the request could not be read. */
9
13
  export function writeResult(id, resultJson) {
10
14
  return `{"jsonrpc":"2.0","id":${id === null ? 'null' : id},"result":${resultJson}}`;
11
15
  }
@@ -0,0 +1,21 @@
1
+ /** The `concurrency` value that leaves the number of simultaneous requests to the host. */
2
+ export declare const CONCURRENCY_UNLIMITED = -1;
3
+ /** How many requests the host keeps in flight to a plugin that declared unlimited concurrency. */
4
+ export declare const UNLIMITED_IN_FLIGHT = 32;
5
+ /** The most workers a plugin that declared unlimited concurrency runs: the host decides, but the pool stays bounded. */
6
+ export declare const UNLIMITED_WORKERS = 1024;
7
+ /** Whether a declared concurrency is one the protocol defines: absent (0), a count (1 and above) or unlimited (-1). */
8
+ export declare function isValidConcurrency(declared: number): boolean;
9
+ /**
10
+ * Reads a declared concurrency: how many requests may be in flight at once,
11
+ * or `unlimited`. An absent, zero or invalid value is 1, the default of every
12
+ * plugin that says nothing.
13
+ */
14
+ export declare function concurrencyLimit(declared: number | undefined): {
15
+ limit: number;
16
+ unlimited: boolean;
17
+ };
18
+ /** How many requests the host keeps in flight to a plugin that declared `declared`. */
19
+ export declare function inFlightLimit(declared: number | undefined): number;
20
+ /** How many requests a plugin that declared `declared` works on at once (its pool size). */
21
+ export declare function workerLimit(declared: number | undefined): number;
@@ -0,0 +1,30 @@
1
+ /** The `concurrency` value that leaves the number of simultaneous requests to the host. */
2
+ export const CONCURRENCY_UNLIMITED = -1;
3
+ /** How many requests the host keeps in flight to a plugin that declared unlimited concurrency. */
4
+ export const UNLIMITED_IN_FLIGHT = 32;
5
+ /** The most workers a plugin that declared unlimited concurrency runs: the host decides, but the pool stays bounded. */
6
+ export const UNLIMITED_WORKERS = 1024;
7
+ /** Whether a declared concurrency is one the protocol defines: absent (0), a count (1 and above) or unlimited (-1). */
8
+ export function isValidConcurrency(declared) {
9
+ return Number.isSafeInteger(declared) && declared >= CONCURRENCY_UNLIMITED;
10
+ }
11
+ /**
12
+ * Reads a declared concurrency: how many requests may be in flight at once,
13
+ * or `unlimited`. An absent, zero or invalid value is 1, the default of every
14
+ * plugin that says nothing.
15
+ */
16
+ export function concurrencyLimit(declared) {
17
+ if (declared === CONCURRENCY_UNLIMITED)
18
+ return { limit: 0, unlimited: true };
19
+ return { limit: declared === undefined || !Number.isSafeInteger(declared) || declared < 1 ? 1 : declared, unlimited: false };
20
+ }
21
+ /** How many requests the host keeps in flight to a plugin that declared `declared`. */
22
+ export function inFlightLimit(declared) {
23
+ const { limit, unlimited } = concurrencyLimit(declared);
24
+ return unlimited ? UNLIMITED_IN_FLIGHT : limit;
25
+ }
26
+ /** How many requests a plugin that declared `declared` works on at once (its pool size). */
27
+ export function workerLimit(declared) {
28
+ const { limit, unlimited } = concurrencyLimit(declared);
29
+ return unlimited ? UNLIMITED_WORKERS : limit;
30
+ }
@@ -0,0 +1 @@
1
+ export { CONCURRENCY_UNLIMITED, UNLIMITED_IN_FLIGHT, UNLIMITED_WORKERS, concurrencyLimit, inFlightLimit, isValidConcurrency, workerLimit } from './concurrency.policy.ts';
@@ -0,0 +1 @@
1
+ export { CONCURRENCY_UNLIMITED, UNLIMITED_IN_FLIGHT, UNLIMITED_WORKERS, concurrencyLimit, inFlightLimit, isValidConcurrency, workerLimit } from './concurrency.policy.js';
@@ -15,7 +15,8 @@ export interface CheckOptions {
15
15
  * itself, has a known kind and known roles, its roles match what it
16
16
  * implements, it names the slots it fills, its references and stacks are
17
17
  * well-formed, every advertised method answers a request with nothing in it,
18
- * and, with `workspace`, the projects it generates are found again by its own
18
+ * requests sent together are answered as the declared `concurrency` allows, a
19
+ * plugin that advertises `cancel` stops a cancelled request, and, with `workspace`, the projects it generates are found again by its own
19
20
  * `discover`. A port of the Go SDK's `conformance.Check`, with the same checks
20
21
  * under the same names. (`smith plugin check` adds one more, "shuts down when
21
22
  * asked", after closing the plugin: call {@link PluginClient.close} and add it
@@ -1,6 +1,7 @@
1
1
  import { describeRef, isEmptyRef } from '../plugin-refs/index.js';
2
2
  import { ROLES, effectiveRoles, isKnownRole } from '../plugin-roles/index.js';
3
3
  import { checkGeneration } from './check-generation.use-case.js';
4
+ import { checkScheduling } from './check-scheduling.use-case.js';
4
5
  import { ConformanceReport } from './conformance-report.model.js';
5
6
  import { goList, goQuote } from './go-list.mapper.js';
6
7
  const CALL_TIMEOUT_MS = 20 * 60 * 1000;
@@ -9,7 +10,8 @@ const CALL_TIMEOUT_MS = 20 * 60 * 1000;
9
10
  * itself, has a known kind and known roles, its roles match what it
10
11
  * implements, it names the slots it fills, its references and stacks are
11
12
  * well-formed, every advertised method answers a request with nothing in it,
12
- * and, with `workspace`, the projects it generates are found again by its own
13
+ * requests sent together are answered as the declared `concurrency` allows, a
14
+ * plugin that advertises `cancel` stops a cancelled request, and, with `workspace`, the projects it generates are found again by its own
13
15
  * `discover`. A port of the Go SDK's `conformance.Check`, with the same checks
14
16
  * under the same names. (`smith plugin check` adds one more, "shuts down when
15
17
  * asked", after closing the plugin: call {@link PluginClient.close} and add it
@@ -52,6 +54,7 @@ export async function checkPlugin(client, options = {}) {
52
54
  report.add(`stack ${stack.name} is complete`, stack.name !== '' && stack.language !== '' && !names.has(stack.name), 'a stack needs a unique name and a language plugin');
53
55
  names.add(stack.name);
54
56
  }
57
+ await checkScheduling(client, report);
55
58
  const answers = async (check, method, params) => {
56
59
  if (!client.supports(method))
57
60
  return;
@@ -0,0 +1,11 @@
1
+ import type { PluginClient } from '../host/index.ts';
2
+ import type { ConformanceReport } from './conformance-report.model.ts';
3
+ /** How long a plugin that advertised cancel has to answer a request the host cancelled. */
4
+ export declare const CANCEL_BOUND_MS = 10000;
5
+ /**
6
+ * Checks what a plugin says about how it is called: the concurrency it
7
+ * declares must be one the protocol defines and be accepted, and the cancel it
8
+ * advertises must stop a request within a bound and leave the plugin serving.
9
+ * The same checks, under the same names, as the Go SDK's.
10
+ */
11
+ export declare function checkScheduling(client: PluginClient, report: ConformanceReport, cancelBoundMs?: number): Promise<void>;
@@ -0,0 +1,98 @@
1
+ import { CallInterruptedError } from '../host/index.js';
2
+ import { ERROR_CODES } from '../protocol/index.js';
3
+ import { inFlightLimit, isValidConcurrency } from '../concurrency/index.js';
4
+ /** How long a plugin that advertised cancel has to answer a request the host cancelled. */
5
+ export const CANCEL_BOUND_MS = 10_000;
6
+ /** An id no request ever had. */
7
+ const UNKNOWN_REQUEST = 2 ** 40;
8
+ /** The cheapest call the plugin answers without doing work: a method that takes a list of projects or manifests, asked about none. */
9
+ function probeOf(client) {
10
+ const timeoutMs = 2 * 60 * 1000;
11
+ if (client.supports('discover'))
12
+ return signal => client.call('discover', { manifests: [] }, { timeoutMs, signal });
13
+ if (client.supports('contribute'))
14
+ return signal => client.call('contribute', { projects: [] }, { timeoutMs, signal });
15
+ if (client.supports('ci'))
16
+ return signal => client.call('ci', { projects: [] }, { timeoutMs, signal });
17
+ return client.supports('doctor') ? signal => client.call('doctor', { projects: [] }, { timeoutMs, signal }) : undefined;
18
+ }
19
+ function messageOf(thrown) {
20
+ return thrown instanceof Error ? thrown.message : String(thrown);
21
+ }
22
+ /**
23
+ * Checks what a plugin says about how it is called: the concurrency it
24
+ * declares must be one the protocol defines and be accepted, and the cancel it
25
+ * advertises must stop a request within a bound and leave the plugin serving.
26
+ * The same checks, under the same names, as the Go SDK's.
27
+ */
28
+ export async function checkScheduling(client, report, cancelBoundMs = CANCEL_BOUND_MS) {
29
+ const info = client.info;
30
+ report.add('declares a concurrency the protocol defines', isValidConcurrency(info.concurrency ?? 0), `concurrency ${info.concurrency} is below -1; it must be absent, 1 or more, or -1 for unlimited`);
31
+ const probe = probeOf(client);
32
+ if (probe === undefined)
33
+ return;
34
+ await checkConcurrency(client, probe, report);
35
+ if (info.capabilities.cancel === true)
36
+ await checkCancel(client, probe, report, cancelBoundMs);
37
+ }
38
+ /**
39
+ * Sends requests together. A plugin that declares nothing is sent them one at
40
+ * a time and must answer them all; one that declares a concurrency is sent that
41
+ * many at once (at most four, to keep the check cheap) and must answer them all.
42
+ */
43
+ async function checkConcurrency(client, probe, report) {
44
+ const limit = inFlightLimit(client.info.concurrency);
45
+ let together = Math.min(limit, 4);
46
+ let name = `accepts ${together} requests at the same time`;
47
+ if (limit === 1) {
48
+ together = 3;
49
+ name = 'answers 3 requests sent together, one at a time';
50
+ }
51
+ const settled = await Promise.allSettled(Array.from({ length: together }, async () => await probe()));
52
+ const failed = settled.find((result) => result.status === 'rejected');
53
+ const peak = client.peakInFlight;
54
+ report.add(name, failed === undefined && peak <= limit, `${failed === undefined ? '<nil>' : messageOf(failed.reason)} (the host had ${peak} in flight, the plugin declared ${limit})`);
55
+ }
56
+ /** How a promise ended: with the error it rejected with, if any. */
57
+ async function settle(promise) {
58
+ try {
59
+ await promise;
60
+ return {};
61
+ }
62
+ catch (error) {
63
+ return { error };
64
+ }
65
+ }
66
+ /** Cancels a request, then a request that does not exist. */
67
+ async function checkCancel(client, probe, report, cancelBoundMs) {
68
+ const controller = new AbortController();
69
+ const began = Date.now();
70
+ const request = settle(probe(controller.signal));
71
+ await new Promise(resolve => setTimeout(resolve, 5)); // let the request leave; a cancel that wins the race is just as good an answer
72
+ controller.abort();
73
+ let timer;
74
+ const late = new Promise((resolve) => {
75
+ timer = setTimeout(() => { resolve('late'); }, cancelBoundMs + client.cancelGraceMs);
76
+ });
77
+ const outcome = await Promise.race([request, late]);
78
+ clearTimeout(timer);
79
+ const error = outcome === 'late' ? new Error(`no answer within ${cancelBoundMs}ms of the cancel`) : outcome.error;
80
+ const stopped = outcome !== 'late' && (error === undefined || (error instanceof CallInterruptedError && !error.killed));
81
+ const detail = `${error === undefined ? '<nil>' : messageOf(error)} (a plugin that advertised cancel answers the request, with ${ERROR_CODES.RequestCancelled} if it stopped, within ${cancelBoundMs}ms)`;
82
+ report.add('answers a cancelled request with its result or the cancelled error', stopped && Date.now() - began < cancelBoundMs, detail);
83
+ let notifyError;
84
+ let followUp;
85
+ try {
86
+ await client.notify('cancel', { id: UNKNOWN_REQUEST });
87
+ }
88
+ catch (error_) {
89
+ notifyError = error_;
90
+ }
91
+ try {
92
+ await probe();
93
+ }
94
+ catch (error_) {
95
+ followUp = error_;
96
+ }
97
+ report.add('ignores a cancel for a request it does not know', notifyError === undefined && followUp === undefined, `${notifyError === undefined ? '<nil>' : messageOf(notifyError)}, then ${followUp === undefined ? '<nil>' : messageOf(followUp)}`);
98
+ }
@@ -0,0 +1,16 @@
1
+ import type { RpcError } from '../protocol/index.ts';
2
+ /** Why the host stopped waiting for a call. */
3
+ export type InterruptionReason = 'cancelled' | 'timeout';
4
+ /**
5
+ * A call the host gave up on, because its `signal` aborted (`cancelled`) or its
6
+ * time ran out (`timeout`). `killed` says the plugin did not stop in time
7
+ * and was killed, which also fails every other call in flight to it. When the
8
+ * plugin answered while being stopped, `response` is its error (the
9
+ * request-cancelled code, usually).
10
+ */
11
+ export declare class CallInterruptedError extends Error {
12
+ readonly reason: InterruptionReason;
13
+ readonly killed: boolean;
14
+ readonly response?: RpcError;
15
+ constructor(message: string, reason: InterruptionReason, killed: boolean, response?: RpcError);
16
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A call the host gave up on, because its `signal` aborted (`cancelled`) or its
3
+ * time ran out (`timeout`). `killed` says the plugin did not stop in time
4
+ * and was killed, which also fails every other call in flight to it. When the
5
+ * plugin answered while being stopped, `response` is its error (the
6
+ * request-cancelled code, usually).
7
+ */
8
+ export class CallInterruptedError extends Error {
9
+ reason;
10
+ killed;
11
+ response;
12
+ constructor(message, reason, killed, response) {
13
+ super(message, response === undefined ? undefined : { cause: response });
14
+ this.name = 'CallInterruptedError';
15
+ this.reason = reason;
16
+ this.killed = killed;
17
+ if (response !== undefined)
18
+ this.response = response;
19
+ }
20
+ }
@@ -7,6 +7,8 @@ export interface HostOptions {
7
7
  host?: Peer;
8
8
  /** How long `initialize` may take. Default: 30 seconds. */
9
9
  initializeTimeoutMs?: number;
10
+ /** How long a plugin has to answer after it was asked to stop (or, for one that cannot be asked, after the caller cancelled) before it is killed. Default: 5 seconds. */
11
+ cancelGraceMs?: number;
10
12
  }
11
13
  /** Options of {@link startPlugin}: a plugin process to launch. */
12
14
  export interface StartOptions extends HostOptions {
@@ -19,10 +21,14 @@ export interface StartOptions extends HostOptions {
19
21
  /** What happens to the plugin's standard error, where it logs. Default: it goes to this process's standard error. */
20
22
  stderr?: 'inherit' | 'ignore';
21
23
  }
24
+ /** How long a plugin has to answer after it was asked to stop, before the host kills it. */
25
+ export declare const DEFAULT_CANCEL_GRACE_MS = 5000;
22
26
  /** The longest a call may take unless the caller says otherwise: generate and publish run real toolchains. */
23
27
  export declare const DEFAULT_CALL_TIMEOUT_MS: number;
24
28
  /** Options of one {@link PluginClient.call}. */
25
29
  export interface CallOptions {
26
30
  /** How long the plugin may take. On expiry the plugin process is killed: its stream can no longer be trusted. Default: 2 minutes. */
27
31
  timeoutMs?: number;
32
+ /** Aborting it cancels the call: the plugin is sent `cancel` if it advertised it, and the call ends within the grace period. */
33
+ signal?: AbortSignal;
28
34
  }
@@ -1,2 +1,4 @@
1
+ /** How long a plugin has to answer after it was asked to stop, before the host kills it. */
2
+ export const DEFAULT_CANCEL_GRACE_MS = 5000;
1
3
  /** The longest a call may take unless the caller says otherwise: generate and publish run real toolchains. */
2
4
  export const DEFAULT_CALL_TIMEOUT_MS = 2 * 60 * 1000;
package/host/index.d.ts CHANGED
@@ -3,5 +3,7 @@ export type { PluginConnection } from './plugin-client.client.ts';
3
3
  export { startPlugin } from './start-plugin.use-case.ts';
4
4
  export { connectInProcess, connectPlugin } from './connect-plugin.use-case.ts';
5
5
  export type { PluginStreams } from './connect-plugin.use-case.ts';
6
- export { DEFAULT_CALL_TIMEOUT_MS } from './host-options.contract.ts';
6
+ export { CallInterruptedError } from './call-interrupted.error.ts';
7
+ export type { InterruptionReason } from './call-interrupted.error.ts';
8
+ export { DEFAULT_CALL_TIMEOUT_MS, DEFAULT_CANCEL_GRACE_MS } from './host-options.contract.ts';
7
9
  export type { CallOptions, HostOptions, StartOptions } from './host-options.contract.ts';
package/host/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { PluginClient } from './plugin-client.client.js';
2
2
  export { startPlugin } from './start-plugin.use-case.js';
3
3
  export { connectInProcess, connectPlugin } from './connect-plugin.use-case.js';
4
- export { DEFAULT_CALL_TIMEOUT_MS } from './host-options.contract.js';
4
+ export { CallInterruptedError } from './call-interrupted.error.js';
5
+ export { DEFAULT_CALL_TIMEOUT_MS, DEFAULT_CANCEL_GRACE_MS } from './host-options.contract.js';
@@ -9,17 +9,20 @@ export interface PluginConnection {
9
9
  write: (chunk: Buffer, callback: (error?: Error | null) => void) => unknown;
10
10
  end: () => unknown;
11
11
  };
12
- /** Called when a call timed out: ends the plugin, if it is a process. */
12
+ /** Called when the plugin did not answer in time: ends the plugin, if it is a process. */
13
13
  kill: () => void;
14
14
  /** Resolves once the plugin has gone, with the reason it did not end well (or `null`). Called by {@link PluginClient.close}. */
15
15
  finish: (graceMs: number) => Promise<string | null>;
16
16
  }
17
17
  /**
18
18
  * The host's side of a connection to one initialized plugin, for a plugin
19
- * author's tests (smith itself is the host in real use). Calls are serialised,
20
- * as smith serialises them, each with a timeout. Open one with {@link startPlugin}
21
- * (a process), {@link connectPlugin} (streams) or {@link connectInProcess}
22
- * (a plugin object served in this process).
19
+ * author's tests (smith itself is the host in real use). It never has more
20
+ * requests in flight than the plugin declared in `concurrency`, as smith does:
21
+ * with the default of one, calls are serialised, each waiting for the one
22
+ * before it to be answered. Each call has a timeout and may be cancelled with
23
+ * an `AbortSignal`. Open one with {@link startPlugin} (a process),
24
+ * {@link connectPlugin} (streams) or {@link connectInProcess} (a plugin object
25
+ * served in this process).
23
26
  */
24
27
  export declare class PluginClient {
25
28
  #private;
@@ -30,11 +33,27 @@ export declare class PluginClient {
30
33
  private constructor();
31
34
  /**
32
35
  * Sends one request and resolves with its decoded result. An error answer
33
- * rejects with an {@link RpcError} carrying its code. If the plugin does not
34
- * answer in time, the plugin is killed and the call rejects.
36
+ * rejects with an {@link RpcError} carrying its code. The call waits for a
37
+ * place first, so the plugin never has more requests in flight than it
38
+ * declared; the timeout counts from the moment the request is sent.
39
+ *
40
+ * When `options.signal` aborts or the time runs out the host stops waiting.
41
+ * A plugin that advertised `cancel` is sent the notification and has the
42
+ * grace period to answer: with the result it had already (the call resolves),
43
+ * or an error (the call rejects with a {@link CallInterruptedError} carrying
44
+ * it). A plugin that does not answer in time is killed, because its stream can
45
+ * no longer be trusted, and the call rejects; a plugin that cannot be asked
46
+ * to stop is killed at once on a timeout, and after the grace period when
47
+ * the caller cancelled.
35
48
  */
36
49
  call<M extends MethodName>(method: M, params: ParamsOf<M>, options?: CallOptions): Promise<ResultOf<M>>;
37
50
  call(method: string, params?: unknown, options?: CallOptions): Promise<unknown>;
51
+ /** How long the plugin has to answer after it was asked to stop. */
52
+ get cancelGraceMs(): number;
53
+ /** The most requests this client has had in flight at once. */
54
+ get peakInFlight(): number;
55
+ /** Sends a message that gets no answer, such as `cancel`. */
56
+ notify(method: string, params: unknown): Promise<void>;
38
57
  /** Whether the plugin advertised a method: `initialize` and `shutdown` always, the others by their capability. */
39
58
  supports(method: string): boolean;
40
59
  /** Asks the plugin to shut down and waits briefly for it to go. Rejects if it did not go quietly. */