@toshicloud/sdk 0.3.1 → 0.4.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/README.md +165 -28
- package/dist/client.d.ts +25 -4
- package/dist/client.js +12 -5
- package/dist/execute.js +15 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/interceptors.d.ts +11 -2
- package/dist/interceptors.js +40 -4
- package/dist/node/client.d.ts +35 -0
- package/dist/node/client.js +178 -0
- package/dist/node/index.d.ts +3 -0
- package/dist/node/index.js +4 -0
- package/dist/output.js +6 -1
- package/dist/paginate.d.ts +36 -0
- package/dist/paginate.js +37 -0
- package/dist/run.js +3 -2
- package/dist/stream.d.ts +8 -1
- package/dist/stream.js +24 -2
- package/dist/test/fake.d.ts +88 -0
- package/dist/test/fake.js +845 -0
- package/dist/test/index.d.ts +58 -0
- package/dist/test/index.js +57 -0
- package/dist/test/procedures.d.ts +18 -0
- package/dist/test/procedures.js +219 -0
- package/dist/timeout.d.ts +29 -0
- package/dist/timeout.js +67 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/wait.js +15 -12
- package/dist/watch.js +15 -2
- package/package.json +28 -4
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { versions } from "node:process";
|
|
3
|
+
import { rootCertificates } from "node:tls";
|
|
4
|
+
import { createConnectTransport } from "@connectrpc/connect-node";
|
|
5
|
+
import { createToshiClient as createRootClient } from "../client.js";
|
|
6
|
+
import { VERSION } from "../version.js";
|
|
7
|
+
/**
|
|
8
|
+
* Creates a Node client. Without a transport of its own it builds one with
|
|
9
|
+
* connect-node over HTTP/2, takes the server from `server` or TOSHI_SERVER and
|
|
10
|
+
* the token from `tokenSource`, `token`, `tokenFile`, TOSHI_TOKEN or
|
|
11
|
+
* TOSHI_TOKEN_FILE.
|
|
12
|
+
*
|
|
13
|
+
* The User-Agent is "toshi-ts/<version> (node/<node version>)", with
|
|
14
|
+
* `userAgent` in front when it is set. Connect exports no version constant, so
|
|
15
|
+
* its name is not part of the string.
|
|
16
|
+
*/
|
|
17
|
+
export function createToshiClient(options = {}) {
|
|
18
|
+
const custom = options.transport;
|
|
19
|
+
const resolved = resolveToken(options, custom !== undefined);
|
|
20
|
+
const agent = userAgent(options.userAgent ?? "");
|
|
21
|
+
const outer = (interceptors) => [
|
|
22
|
+
...(options.interceptors ?? []),
|
|
23
|
+
userAgentInterceptor(agent),
|
|
24
|
+
...interceptors,
|
|
25
|
+
];
|
|
26
|
+
let transport = custom;
|
|
27
|
+
if (transport === undefined) {
|
|
28
|
+
const server = resolveServer(options.server);
|
|
29
|
+
const caFile = resolveCAFile(options.caFile);
|
|
30
|
+
const ca = caFile === undefined ? undefined : caBundle(caFile);
|
|
31
|
+
transport = (interceptors) => createConnectTransport({
|
|
32
|
+
baseUrl: server,
|
|
33
|
+
httpVersion: "2",
|
|
34
|
+
interceptors: outer(interceptors),
|
|
35
|
+
...(ca === undefined ? {} : { nodeOptions: { ca } }),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const transportOfCaller = transport;
|
|
40
|
+
transport = (interceptors) => transportOfCaller(outer(interceptors));
|
|
41
|
+
}
|
|
42
|
+
return createRootClient({
|
|
43
|
+
...options,
|
|
44
|
+
transport,
|
|
45
|
+
token: resolved.token,
|
|
46
|
+
tokenSource: resolved.tokenSource,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** The User-Agent every Node request carries, with product in front when set. */
|
|
50
|
+
export function userAgent(product) {
|
|
51
|
+
const base = `toshi-ts/${VERSION} (node/${versions.node})`;
|
|
52
|
+
return product === "" ? base : `${product} ${base}`;
|
|
53
|
+
}
|
|
54
|
+
function userAgentInterceptor(agent) {
|
|
55
|
+
return (next) => async (req) => {
|
|
56
|
+
const header = new Headers(req.header);
|
|
57
|
+
header.set("User-Agent", agent);
|
|
58
|
+
return next({ ...req, header });
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function resolveServer(given) {
|
|
62
|
+
const server = given !== undefined && given !== "" ? given : env("TOSHI_SERVER");
|
|
63
|
+
if (server === undefined) {
|
|
64
|
+
throw new Error("toshi: no server: use the server option or set TOSHI_SERVER");
|
|
65
|
+
}
|
|
66
|
+
return server;
|
|
67
|
+
}
|
|
68
|
+
// resolveToken picks the token by the documented precedence: the source, the
|
|
69
|
+
// option, the option's file, TOSHI_TOKEN, then TOSHI_TOKEN_FILE
|
|
70
|
+
// (client.go:154-169). A token file is read here, so a missing or empty one
|
|
71
|
+
// fails createToshiClient; the source reads it again when it changes. With a
|
|
72
|
+
// transport of the caller's and no token anywhere, it returns nothing instead
|
|
73
|
+
// of refusing.
|
|
74
|
+
function resolveToken(options, optional) {
|
|
75
|
+
if (options.tokenSource !== undefined) {
|
|
76
|
+
return { tokenSource: options.tokenSource };
|
|
77
|
+
}
|
|
78
|
+
const token = options.token?.trim() ?? "";
|
|
79
|
+
if (token !== "") {
|
|
80
|
+
return { token };
|
|
81
|
+
}
|
|
82
|
+
if (options.tokenFile !== undefined && options.tokenFile !== "") {
|
|
83
|
+
return { tokenSource: fileTokenSource(options.tokenFile) };
|
|
84
|
+
}
|
|
85
|
+
const fromEnv = env("TOSHI_TOKEN")?.trim() ?? "";
|
|
86
|
+
if (fromEnv !== "") {
|
|
87
|
+
return { token: fromEnv };
|
|
88
|
+
}
|
|
89
|
+
const envFile = env("TOSHI_TOKEN_FILE");
|
|
90
|
+
if (envFile !== undefined) {
|
|
91
|
+
return { tokenSource: fileTokenSource(envFile) };
|
|
92
|
+
}
|
|
93
|
+
if (optional) {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
throw new Error("toshi: no token: use the tokenSource, token or tokenFile option, or set TOSHI_TOKEN or TOSHI_TOKEN_FILE");
|
|
97
|
+
}
|
|
98
|
+
// fileTokenSource returns a source over a token file whose first read happens
|
|
99
|
+
// now, so a missing or empty file still fails createToshiClient.
|
|
100
|
+
function fileTokenSource(path) {
|
|
101
|
+
const file = new TokenFile(path);
|
|
102
|
+
return () => file.read();
|
|
103
|
+
}
|
|
104
|
+
// TokenFile is a token file read again when its modification time or size
|
|
105
|
+
// changed since the last read (client.go:203-249).
|
|
106
|
+
class TokenFile {
|
|
107
|
+
#path;
|
|
108
|
+
#token = "";
|
|
109
|
+
#modTimeNs = 0n;
|
|
110
|
+
#size = -1n;
|
|
111
|
+
constructor(path) {
|
|
112
|
+
this.#path = path;
|
|
113
|
+
this.read();
|
|
114
|
+
}
|
|
115
|
+
// read returns the file's token, reading the file again when it changed. It
|
|
116
|
+
// stats before reading, so a change made in between is read next time. After
|
|
117
|
+
// a failure the next call reads the file again.
|
|
118
|
+
read() {
|
|
119
|
+
let stats;
|
|
120
|
+
try {
|
|
121
|
+
stats = statSync(this.#path, { bigint: true });
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
this.#size = -1n;
|
|
125
|
+
throw new Error(`toshi: read token file ${this.#path}: ${message(err)}`, { cause: err });
|
|
126
|
+
}
|
|
127
|
+
if (stats.mtimeNs === this.#modTimeNs && stats.size === this.#size) {
|
|
128
|
+
return this.#token;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
this.#token = readTokenFile(this.#path);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
this.#size = -1n;
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
this.#modTimeNs = stats.mtimeNs;
|
|
138
|
+
this.#size = stats.size;
|
|
139
|
+
return this.#token;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function resolveCAFile(given) {
|
|
143
|
+
if (given !== undefined && given !== "") {
|
|
144
|
+
return given;
|
|
145
|
+
}
|
|
146
|
+
return env("TOSHI_CA_FILE");
|
|
147
|
+
}
|
|
148
|
+
function readTokenFile(path) {
|
|
149
|
+
let contents;
|
|
150
|
+
try {
|
|
151
|
+
contents = readFileSync(path, "utf8");
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
throw new Error(`toshi: read token file ${path}: ${message(err)}`, { cause: err });
|
|
155
|
+
}
|
|
156
|
+
const token = contents.trim();
|
|
157
|
+
if (token === "") {
|
|
158
|
+
throw new Error(`toshi: token file ${path} is empty`);
|
|
159
|
+
}
|
|
160
|
+
return token;
|
|
161
|
+
}
|
|
162
|
+
// The bundle is trusted besides the built-in roots, as the Go client trusts the
|
|
163
|
+
// system pool besides the file (client.go:206-237).
|
|
164
|
+
function caBundle(path) {
|
|
165
|
+
try {
|
|
166
|
+
return [...rootCertificates, readFileSync(path, "utf8")];
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
throw new Error(`toshi: read CA file ${path}: ${message(err)}`, { cause: err });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function env(name) {
|
|
173
|
+
const value = process.env[name];
|
|
174
|
+
return value === undefined || value === "" ? undefined : value;
|
|
175
|
+
}
|
|
176
|
+
function message(err) {
|
|
177
|
+
return err instanceof Error ? err.message : String(err);
|
|
178
|
+
}
|
package/dist/output.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { clone } from "@bufbuild/protobuf";
|
|
2
2
|
import { WatchOutputRequestSchema } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
3
3
|
import { READER_TAKEN, WatchState, createReader, reconnect, streamEnded } from "./stream.js";
|
|
4
|
+
import { timeoutError } from "./timeout.js";
|
|
4
5
|
/** Starts an output watch at the request's offsets, counting the bytes it hands out (output.go:14-52). */
|
|
5
6
|
export function openOutputWatch(service, retry, req, opts) {
|
|
6
|
-
const state = new WatchState("", opts?.signal);
|
|
7
|
+
const state = new WatchState("", opts?.signal, opts?.timeoutMs);
|
|
7
8
|
const request = clone(WatchOutputRequestSchema, req);
|
|
8
9
|
let stdout = req.stdoutOffset;
|
|
9
10
|
let stderr = req.stderrOffset;
|
|
@@ -20,6 +21,7 @@ export function openOutputWatch(service, retry, req, opts) {
|
|
|
20
21
|
request.stderrOffset = stderr;
|
|
21
22
|
const stream = service.watchOutput(request, { signal: state.signal });
|
|
22
23
|
for await (const msg of stream) {
|
|
24
|
+
state.markOpen();
|
|
23
25
|
attempt = 0;
|
|
24
26
|
if (msg.message.case === "end") {
|
|
25
27
|
// output.go:157-160: release the RPC before the caller sees the
|
|
@@ -45,6 +47,9 @@ export function openOutputWatch(service, retry, req, opts) {
|
|
|
45
47
|
if (state.closed) {
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
50
|
+
if (state.timedOut) {
|
|
51
|
+
throw timeoutError(state.timeoutMs);
|
|
52
|
+
}
|
|
48
53
|
cause = err;
|
|
49
54
|
}
|
|
50
55
|
attempt = await reconnect(retry, attempt, cause, state);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CallOptions } from "./client.js";
|
|
2
|
+
/** A List request: it may name the page to read first. */
|
|
3
|
+
export interface PageRequest {
|
|
4
|
+
/** From an earlier response's nextPageToken; empty for the first page. */
|
|
5
|
+
pageToken?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
/** A List response: it names the page after it. */
|
|
8
|
+
export interface PageResponse {
|
|
9
|
+
/** Empty on the last page. */
|
|
10
|
+
readonly nextPageToken: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Yields every item of a page-at-a-time list call, one page after another.
|
|
14
|
+
*
|
|
15
|
+
* `call` is a service client's list method, and `items` picks a response's
|
|
16
|
+
* items:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* for await (const project of paginate(
|
|
20
|
+
* {},
|
|
21
|
+
* (req, opts) => toshi.project.listProjects(req, opts),
|
|
22
|
+
* (resp) => resp.projects,
|
|
23
|
+
* )) {
|
|
24
|
+
* console.log(project.name);
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* `paginate` sends a copy of `req`, so the caller's request keeps its fields
|
|
29
|
+
* and a `pageToken` it set resumes from that page. It reads each response's
|
|
30
|
+
* `nextPageToken` and sends it as the next request's `pageToken`. The
|
|
31
|
+
* iteration ends after a page whose `nextPageToken` is empty, when the loop
|
|
32
|
+
* breaks, or after an error, which the `for await` throws once and then ends.
|
|
33
|
+
* `opts` goes to every page, so `timeoutMs` bounds each page and not the
|
|
34
|
+
* whole iteration.
|
|
35
|
+
*/
|
|
36
|
+
export declare function paginate<Req extends PageRequest, Resp extends PageResponse, Item>(req: Req, call: (req: Req, opts?: CallOptions) => Promise<Resp>, items: (resp: Resp) => readonly Item[], opts?: CallOptions): AsyncGenerator<Item, void, undefined>;
|
package/dist/paginate.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yields every item of a page-at-a-time list call, one page after another.
|
|
3
|
+
*
|
|
4
|
+
* `call` is a service client's list method, and `items` picks a response's
|
|
5
|
+
* items:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* for await (const project of paginate(
|
|
9
|
+
* {},
|
|
10
|
+
* (req, opts) => toshi.project.listProjects(req, opts),
|
|
11
|
+
* (resp) => resp.projects,
|
|
12
|
+
* )) {
|
|
13
|
+
* console.log(project.name);
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* `paginate` sends a copy of `req`, so the caller's request keeps its fields
|
|
18
|
+
* and a `pageToken` it set resumes from that page. It reads each response's
|
|
19
|
+
* `nextPageToken` and sends it as the next request's `pageToken`. The
|
|
20
|
+
* iteration ends after a page whose `nextPageToken` is empty, when the loop
|
|
21
|
+
* breaks, or after an error, which the `for await` throws once and then ends.
|
|
22
|
+
* `opts` goes to every page, so `timeoutMs` bounds each page and not the
|
|
23
|
+
* whole iteration.
|
|
24
|
+
*/
|
|
25
|
+
export async function* paginate(req, call, items, opts) {
|
|
26
|
+
const page = { ...req };
|
|
27
|
+
for (;;) {
|
|
28
|
+
const resp = await call(page, opts);
|
|
29
|
+
for (const item of items(resp)) {
|
|
30
|
+
yield item;
|
|
31
|
+
}
|
|
32
|
+
if (!resp.nextPageToken) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
page.pageToken = resp.nextPageToken;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/run.js
CHANGED
|
@@ -3,13 +3,14 @@ import { JobExitError } from "./errors.js";
|
|
|
3
3
|
import { OutputEndSchema, WatchOutputRequestSchema } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
4
4
|
import { EndReason } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
5
5
|
import { openOutputWatch } from "./output.js";
|
|
6
|
+
import { runWithDeadline } from "./timeout.js";
|
|
6
7
|
/**
|
|
7
8
|
* Starts a job and follows its output, as `toshi run` does: CreateJob, then
|
|
8
9
|
* WatchOutput from the first byte. A job whose create answered its output
|
|
9
10
|
* (wait_for_end was set) is returned finished.
|
|
10
11
|
*/
|
|
11
12
|
export async function runJob(service, retry, req, opts) {
|
|
12
|
-
const created = await service.createJob(req,
|
|
13
|
+
const created = await runWithDeadline(opts, (callOpts) => service.createJob(req, callOpts));
|
|
13
14
|
const job = created.job;
|
|
14
15
|
if (job === undefined) {
|
|
15
16
|
throw new Error("toshi: CreateJob answered no job");
|
|
@@ -21,7 +22,7 @@ export async function runJob(service, retry, req, opts) {
|
|
|
21
22
|
project: req.project,
|
|
22
23
|
environment: req.environment,
|
|
23
24
|
id: job.id,
|
|
24
|
-
}), opts);
|
|
25
|
+
}), opts === undefined ? undefined : { ...opts, timeoutMs: undefined });
|
|
25
26
|
return new LiveRun(job, watch);
|
|
26
27
|
}
|
|
27
28
|
/** A run whose create answered the job's output instead of a live stream. */
|
package/dist/stream.d.ts
CHANGED
|
@@ -12,10 +12,17 @@ export declare class WatchState {
|
|
|
12
12
|
cursor: string;
|
|
13
13
|
closed: boolean;
|
|
14
14
|
readonly signal: AbortSignal;
|
|
15
|
+
/** The open timeout, when the caller set one. */
|
|
16
|
+
readonly timeoutMs: number | undefined;
|
|
17
|
+
/** Whether the open timeout fired rather than the caller's signal. */
|
|
18
|
+
timedOut: boolean;
|
|
15
19
|
private readonly controller;
|
|
16
20
|
private readonly caller;
|
|
21
|
+
private timer;
|
|
17
22
|
private readonly onCallerAbort;
|
|
18
|
-
constructor(initialCursor: string, caller: AbortSignal | undefined);
|
|
23
|
+
constructor(initialCursor: string, caller: AbortSignal | undefined, timeoutMs?: number);
|
|
24
|
+
/** The first message arrived: the watch is open, and the timeout no longer applies. */
|
|
25
|
+
markOpen(): void;
|
|
19
26
|
close(): void;
|
|
20
27
|
}
|
|
21
28
|
export {};
|
package/dist/stream.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { abortReason, backoffDelay, isStreamLoss, sleep } from "./retry.js";
|
|
2
|
+
import { timeoutError } from "./timeout.js";
|
|
2
3
|
/** The one-reader guard every watch shares. */
|
|
3
4
|
export const READER_TAKEN = "toshi: this watch already has a reader";
|
|
4
5
|
// watch.go:473-476, output.go:120-127: a stream that ends without its end
|
|
@@ -47,22 +48,29 @@ export async function reconnect(retry, attempt, cause, state) {
|
|
|
47
48
|
}
|
|
48
49
|
// Owns the AbortController that cancels the open RPC, a pending next() and any
|
|
49
50
|
// backoff sleep; close() is the local end, an abort of the caller's signal is an
|
|
50
|
-
// error for the caller (watch.go:221-233, output.go:194-206).
|
|
51
|
+
// error for the caller (watch.go:221-233, output.go:194-206). A timeout ends the
|
|
52
|
+
// open only: the first message calls markOpen and clears it.
|
|
51
53
|
export class WatchState {
|
|
52
54
|
/** The caller-visible resume point; the output watch passes "" and ignores it. */
|
|
53
55
|
cursor;
|
|
54
56
|
closed = false;
|
|
55
57
|
signal;
|
|
58
|
+
/** The open timeout, when the caller set one. */
|
|
59
|
+
timeoutMs;
|
|
60
|
+
/** Whether the open timeout fired rather than the caller's signal. */
|
|
61
|
+
timedOut = false;
|
|
56
62
|
controller = new AbortController();
|
|
57
63
|
caller;
|
|
64
|
+
timer;
|
|
58
65
|
onCallerAbort = () => {
|
|
59
66
|
if (this.caller !== undefined) {
|
|
60
67
|
this.controller.abort(abortReason(this.caller));
|
|
61
68
|
}
|
|
62
69
|
};
|
|
63
|
-
constructor(initialCursor, caller) {
|
|
70
|
+
constructor(initialCursor, caller, timeoutMs) {
|
|
64
71
|
this.cursor = initialCursor;
|
|
65
72
|
this.caller = caller;
|
|
73
|
+
this.timeoutMs = timeoutMs;
|
|
66
74
|
this.signal = this.controller.signal;
|
|
67
75
|
if (caller !== undefined) {
|
|
68
76
|
if (caller.aborted) {
|
|
@@ -72,12 +80,26 @@ export class WatchState {
|
|
|
72
80
|
caller.addEventListener("abort", this.onCallerAbort, { once: true });
|
|
73
81
|
}
|
|
74
82
|
}
|
|
83
|
+
if (timeoutMs !== undefined && !this.controller.signal.aborted) {
|
|
84
|
+
this.timer = setTimeout(() => {
|
|
85
|
+
this.timedOut = true;
|
|
86
|
+
this.controller.abort(timeoutError(timeoutMs));
|
|
87
|
+
}, timeoutMs);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** The first message arrived: the watch is open, and the timeout no longer applies. */
|
|
91
|
+
markOpen() {
|
|
92
|
+
if (this.timer !== undefined) {
|
|
93
|
+
clearTimeout(this.timer);
|
|
94
|
+
this.timer = undefined;
|
|
95
|
+
}
|
|
75
96
|
}
|
|
76
97
|
close() {
|
|
77
98
|
if (this.closed) {
|
|
78
99
|
return;
|
|
79
100
|
}
|
|
80
101
|
this.closed = true;
|
|
102
|
+
this.markOpen();
|
|
81
103
|
this.caller?.removeEventListener("abort", this.onCallerAbort);
|
|
82
104
|
this.controller.abort();
|
|
83
105
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { DescMethod, Message } from "@bufbuild/protobuf";
|
|
2
|
+
import type { Interceptor, MethodImpl, Transport } from "@connectrpc/connect";
|
|
3
|
+
import type { ToshiClient, ToshiOptions } from "../client.js";
|
|
4
|
+
import { JobState, SandboxState } from "../gen/toshi/workspace/v1/workspace_pb.js";
|
|
5
|
+
/** The token `client` sends. */
|
|
6
|
+
export declare const testToken = "test-token";
|
|
7
|
+
/** One call the fake received. */
|
|
8
|
+
export interface Call {
|
|
9
|
+
/** The procedure the client called, for example `/toshi.workspace.v1.WorkloadService/CreateJob`. */
|
|
10
|
+
readonly procedure: string;
|
|
11
|
+
/** The message the client sent. A bidirectional stream's call is recorded when its first message arrives. */
|
|
12
|
+
readonly request: Message;
|
|
13
|
+
/** The request headers the client sent. */
|
|
14
|
+
readonly header: Headers;
|
|
15
|
+
}
|
|
16
|
+
/** A job of the fake, and what it answers about it. */
|
|
17
|
+
export interface FakeJob {
|
|
18
|
+
/** Sets the state the fake reports for the job. */
|
|
19
|
+
state(state: JobState): FakeJob;
|
|
20
|
+
/** Sets the standard output WatchOutput streams. */
|
|
21
|
+
stdout(stdout: string): FakeJob;
|
|
22
|
+
/** Sets the standard error WatchOutput streams. */
|
|
23
|
+
stderr(stderr: string): FakeJob;
|
|
24
|
+
/** Ends the job with code: the state is ENDED and the end reason EXITED. */
|
|
25
|
+
exitCode(code: number): FakeJob;
|
|
26
|
+
}
|
|
27
|
+
/** A sandbox of the fake, and what it answers about it. */
|
|
28
|
+
export interface FakeSandbox {
|
|
29
|
+
/** Sets the state the fake reports for the sandbox. */
|
|
30
|
+
state(state: SandboxState): FakeSandbox;
|
|
31
|
+
}
|
|
32
|
+
/** What Execute answers in one sandbox. */
|
|
33
|
+
export interface FakeExecution {
|
|
34
|
+
/** Sets the standard output Execute streams. */
|
|
35
|
+
stdout(stdout: string): FakeExecution;
|
|
36
|
+
/** Sets the standard error Execute streams. */
|
|
37
|
+
stderr(stderr: string): FakeExecution;
|
|
38
|
+
/** Sets the exit code the execution ends with. */
|
|
39
|
+
exitCode(code: number): FakeExecution;
|
|
40
|
+
}
|
|
41
|
+
/** A fake Toshi, built with `createFake`. */
|
|
42
|
+
export interface Fake {
|
|
43
|
+
/**
|
|
44
|
+
* Builds the in-memory transport the client uses, with the interceptors it
|
|
45
|
+
* is handed. Pass it as `createToshiClient({ transport: fake.transport })`.
|
|
46
|
+
*/
|
|
47
|
+
readonly transport: (interceptors: Interceptor[]) => Transport;
|
|
48
|
+
/**
|
|
49
|
+
* Returns the job with this id. A job the fake has not created yet is what
|
|
50
|
+
* the next CreateJob answers with. It is running until the test sets its
|
|
51
|
+
* state or its exit code.
|
|
52
|
+
*/
|
|
53
|
+
job(id: string): FakeJob;
|
|
54
|
+
/**
|
|
55
|
+
* Returns the sandbox with this id. A sandbox the fake has not created yet
|
|
56
|
+
* is what the next CreateSandbox answers with. It is running until the test
|
|
57
|
+
* sets its state.
|
|
58
|
+
*/
|
|
59
|
+
sandbox(id: string): FakeSandbox;
|
|
60
|
+
/**
|
|
61
|
+
* Returns what Execute answers in a sandbox: an empty command that exits 0
|
|
62
|
+
* until the test sets its output or its exit code. The sandbox is registered
|
|
63
|
+
* too, so a later GetSandbox finds it.
|
|
64
|
+
*/
|
|
65
|
+
execute(sandboxId: string): FakeExecution;
|
|
66
|
+
/**
|
|
67
|
+
* Answers a method instead of the state machine. The implementation is typed
|
|
68
|
+
* by the method, as `router.rpc` is: one call for unary, server-stream and
|
|
69
|
+
* bidirectional methods. A method from outside the Workspace API throws.
|
|
70
|
+
*/
|
|
71
|
+
on<M extends DescMethod>(method: M, impl: MethodImpl<M>): Fake;
|
|
72
|
+
/** Returns every call the fake received, oldest first. */
|
|
73
|
+
calls(): readonly Call[];
|
|
74
|
+
/**
|
|
75
|
+
* Returns a client wired to the fake: the token `testToken`, the scope
|
|
76
|
+
* default/production, and a retry policy that answers at once. options
|
|
77
|
+
* change any of that, and are applied last.
|
|
78
|
+
*/
|
|
79
|
+
client(options?: FakeOptions): ToshiClient;
|
|
80
|
+
}
|
|
81
|
+
/** The options `client` takes: ToshiOptions without the transport, which the fake supplies. */
|
|
82
|
+
export type FakeOptions = Omit<ToshiOptions, "transport">;
|
|
83
|
+
/** Creates a fake Toshi: an in-memory Workspace API and the state its calls answer from. */
|
|
84
|
+
export declare function createFake(): Fake;
|
|
85
|
+
/** The procedures the state machine answers. The coverage test holds it against the API. */
|
|
86
|
+
export declare const answeredProcedures: readonly DescMethod[];
|
|
87
|
+
/** The methods the state machine does not answer, for the coverage test's comparison. */
|
|
88
|
+
export declare const hookOnlyProcedures: readonly DescMethod[];
|