@monosmith/plugin-sdk 0.1.0 → 0.2.1
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/README.md +3 -2
- package/codec/index.d.ts +1 -1
- package/codec/index.js +1 -1
- package/codec/method-codec.algorithm.js +4 -4
- package/codec/wire-message.mapper.d.ts +3 -1
- package/codec/wire-message.mapper.js +5 -1
- package/concurrency/concurrency.policy.d.ts +21 -0
- package/concurrency/concurrency.policy.js +30 -0
- package/concurrency/index.d.ts +1 -0
- package/concurrency/index.js +1 -0
- package/conformance/check-plugin.use-case.d.ts +2 -1
- package/conformance/check-plugin.use-case.js +4 -1
- package/conformance/check-scheduling.use-case.d.ts +11 -0
- package/conformance/check-scheduling.use-case.js +98 -0
- package/host/call-interrupted.error.d.ts +16 -0
- package/host/call-interrupted.error.js +20 -0
- package/host/host-options.contract.d.ts +6 -0
- package/host/host-options.contract.js +2 -0
- package/host/index.d.ts +3 -1
- package/host/index.js +2 -1
- package/host/plugin-client.client.d.ts +26 -7
- package/host/plugin-client.client.js +180 -49
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/protocol/protocol-schema.contract.js +76 -17
- package/protocol/protocol-types.contract.d.ts +52 -0
- package/protocol/protocol-types.contract.js +7 -0
- package/protocol/schema-document.contract.d.ts +4 -1
- package/serve/capabilities-of.algorithm.d.ts +2 -1
- package/serve/capabilities-of.algorithm.js +4 -1
- package/serve/dispatch-request.use-case.d.ts +3 -2
- package/serve/dispatch-request.use-case.js +4 -3
- package/serve/index.d.ts +1 -1
- package/serve/plugin.contract.d.ts +31 -16
- package/serve/request-pool.store.d.ts +49 -0
- package/serve/request-pool.store.js +110 -0
- package/serve/serve.use-case.d.ts +8 -4
- 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
|
|
72
|
-
| `
|
|
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
|
|
26
|
-
return
|
|
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
|
|
30
|
-
return
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 {
|
|
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 {
|
|
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
|
|
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).
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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.
|
|
34
|
-
*
|
|
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. */
|