@toshicloud/sdk 0.3.2 → 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 +145 -27
- 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 +19 -3
package/README.md
CHANGED
|
@@ -4,40 +4,26 @@ A TypeScript client for the Toshi Workspace API, `toshi.workspace.v1`,
|
|
|
4
4
|
published as ES modules with type declarations.
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
|
-
npm install @toshicloud/sdk @connectrpc/connect
|
|
7
|
+
npm install @toshicloud/sdk @connectrpc/connect
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
+
The browser entry needs a transport such as `@connectrpc/connect-web`; the
|
|
11
|
+
Node entry needs `@connectrpc/connect-node`. Both are peer dependencies, so
|
|
12
|
+
install the ones your application uses.
|
|
13
|
+
|
|
10
14
|
Versions up to 0.1.2 were published as `@toshicloud/toshi-ts`.
|
|
11
15
|
|
|
12
16
|
## Using the client
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function, and that function builds a transport with them. Every call then
|
|
17
|
-
gets the same token, scope and retries: the helpers below and the
|
|
18
|
-
generated clients alike.
|
|
19
|
-
|
|
20
|
-
In Node, with `@connectrpc/connect-node`:
|
|
18
|
+
In Node, `@toshicloud/sdk/node` builds the transport and fills the server
|
|
19
|
+
and the token from the environment:
|
|
21
20
|
|
|
22
21
|
<!-- examples/quick-start.ts -->
|
|
23
22
|
|
|
24
23
|
```ts
|
|
25
|
-
import {
|
|
26
|
-
import { createConnectTransport } from "@connectrpc/connect-node";
|
|
27
|
-
import { createToshiClient } from "@toshicloud/sdk";
|
|
24
|
+
import { createToshiClient } from "@toshicloud/sdk/node";
|
|
28
25
|
|
|
29
|
-
const toshi = createToshiClient({
|
|
30
|
-
transport: (interceptors) =>
|
|
31
|
-
createConnectTransport({
|
|
32
|
-
baseUrl: "https://toshi.example.com",
|
|
33
|
-
httpVersion: "2",
|
|
34
|
-
interceptors,
|
|
35
|
-
// Only for a server whose certificate a private CA signed.
|
|
36
|
-
nodeOptions: { ca: readFileSync("ca.pem") },
|
|
37
|
-
}),
|
|
38
|
-
token: process.env.TOSHI_TOKEN,
|
|
39
|
-
project: "default",
|
|
40
|
-
});
|
|
26
|
+
const toshi = createToshiClient({ project: "default" });
|
|
41
27
|
|
|
42
28
|
const { projects } = await toshi.project.listProjects({});
|
|
43
29
|
console.log(projects.length);
|
|
@@ -62,14 +48,21 @@ export const toshi = createToshiClient({
|
|
|
62
48
|
});
|
|
63
49
|
```
|
|
64
50
|
|
|
65
|
-
|
|
66
|
-
`interceptors
|
|
51
|
+
Interceptors of your own go before the client's. The Node entry takes
|
|
52
|
+
`interceptors`; with a transport of your own, put them first:
|
|
53
|
+
`interceptors: [...mine, ...interceptors]`. Each unary call enters them
|
|
54
|
+
once, with its retries inside, and they never see the Authorization
|
|
55
|
+
header: the client sets it on a copy of the request below them.
|
|
67
56
|
|
|
68
|
-
The
|
|
69
|
-
|
|
57
|
+
The browser entry reads no environment variable and no file. The Node
|
|
58
|
+
entry reads `TOSHI_SERVER`, `TOSHI_TOKEN`, `TOSHI_TOKEN_FILE` and
|
|
59
|
+
`TOSHI_CA_FILE`; see Node options.
|
|
70
60
|
|
|
71
61
|
### Options
|
|
72
62
|
|
|
63
|
+
- `tokenSource`: a function that gives each request attempt its token, so a
|
|
64
|
+
refreshed token is used without a new client; it wins over `token`. See
|
|
65
|
+
Tokens that change.
|
|
73
66
|
- `token`: sent as `Authorization: Bearer <token>` on every call.
|
|
74
67
|
- `project`, `environment`: fill the request's `project` and
|
|
75
68
|
`environment` fields when they are empty. A value in the request wins.
|
|
@@ -77,6 +70,65 @@ server and any CA come from the caller.
|
|
|
77
70
|
- `retry`: `{ maxAttempts, initialBackoffMs, maxBackoffMs }`, by default
|
|
78
71
|
5, 200 ms and 10 s, with full jitter. `retry: false` turns off retries
|
|
79
72
|
and watch reconnects.
|
|
73
|
+
- `defaultTimeoutMs`: bounds each unary call except the Wait calls,
|
|
74
|
+
which their request's timeout bounds; streams ignore it.
|
|
75
|
+
|
|
76
|
+
A call may set `timeoutMs` instead: it bounds the whole call, retries
|
|
77
|
+
included, and on a watch it bounds the open. It composes with `signal`.
|
|
78
|
+
|
|
79
|
+
### Node options
|
|
80
|
+
|
|
81
|
+
`@toshicloud/sdk/node` builds a connect-node transport over HTTP/2 and
|
|
82
|
+
fills what is missing from the environment:
|
|
83
|
+
|
|
84
|
+
- `server`, else `TOSHI_SERVER`: the control plane's URL. Without either,
|
|
85
|
+
the client refuses to start.
|
|
86
|
+
- `tokenSource`, `token`, `tokenFile`, `TOSHI_TOKEN`, `TOSHI_TOKEN_FILE`, in
|
|
87
|
+
that order. A token file is trimmed, and an empty file is an error.
|
|
88
|
+
- `caFile`, else `TOSHI_CA_FILE`: a PEM bundle trusted besides Node's
|
|
89
|
+
built-in roots.
|
|
90
|
+
- `userAgent`: put in front of the User-Agent, for example "myapp/1.2".
|
|
91
|
+
- `interceptors`: run before the client's own, in the order given.
|
|
92
|
+
- `defaultTimeoutMs`: bounds each unary call except the Wait calls,
|
|
93
|
+
which their request's timeout bounds; streams ignore it.
|
|
94
|
+
- `transport`: a transport of your own; then `server` and `caFile` are not
|
|
95
|
+
required.
|
|
96
|
+
|
|
97
|
+
Every request carries `User-Agent: toshi-ts/<version> (node/<node
|
|
98
|
+
version>)`, with `userAgent` and one space in front when it is set. The
|
|
99
|
+
root entry sends no User-Agent: browsers own it.
|
|
100
|
+
|
|
101
|
+
### Tokens that change
|
|
102
|
+
|
|
103
|
+
`tokenSource` gives the client a function it calls for the token of every
|
|
104
|
+
request attempt, so a token that is refreshed, for example from a secret
|
|
105
|
+
manager, is used without a new client. It wins over every other token
|
|
106
|
+
option. The client asks it once per attempt, a retry included, and may
|
|
107
|
+
ask it concurrently, so it must be fast: keep the token somewhere and
|
|
108
|
+
refresh it outside the call. A source that throws, or that answers an
|
|
109
|
+
empty token, fails the call with `UNAUTHENTICATED`, with the error as the
|
|
110
|
+
cause, and the call is not retried.
|
|
111
|
+
|
|
112
|
+
<!-- examples/token-source.ts -->
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// A token a refresh loop replaces; the client asks for it on every call.
|
|
116
|
+
let token = "first-token";
|
|
117
|
+
|
|
118
|
+
export const toshi = createToshiClient({
|
|
119
|
+
project: "default",
|
|
120
|
+
tokenSource: () => token,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export function rotate(next: string): void {
|
|
124
|
+
token = next;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A token file, from `tokenFile` or `TOSHI_TOKEN_FILE`, is read again
|
|
129
|
+
before a request when its modification time or size has changed, so a
|
|
130
|
+
rotated token, such as a Kubernetes projected service account token, is
|
|
131
|
+
picked up without a new client.
|
|
80
132
|
|
|
81
133
|
### The client's shape
|
|
82
134
|
|
|
@@ -102,6 +154,43 @@ const { job } = await toshi.workload.jobs.get({ id: jobs[0].id });
|
|
|
102
154
|
console.log(job?.id);
|
|
103
155
|
```
|
|
104
156
|
|
|
157
|
+
### Lists
|
|
158
|
+
|
|
159
|
+
`workload.jobs.all`, `workload.sandboxes.all`, `workload.services.all`,
|
|
160
|
+
`workload.services.allRuns` and `workload.allEvents` take the request their
|
|
161
|
+
`list` method takes and yield every item, one page after another. Each
|
|
162
|
+
response's `nextPageToken` becomes the next request's `pageToken`, and a
|
|
163
|
+
`pageToken` in the request resumes from that page. A service with no
|
|
164
|
+
`all` method is one `paginate` call away: it takes the list call and a
|
|
165
|
+
function that picks a response's items.
|
|
166
|
+
|
|
167
|
+
<!-- examples/lists.ts -->
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { paginate } from "@toshicloud/sdk";
|
|
171
|
+
|
|
172
|
+
import { toshi } from "./client.js";
|
|
173
|
+
|
|
174
|
+
// Every job that matches the request, one page after another.
|
|
175
|
+
for await (const job of toshi.workload.jobs.all({ pageSize: 10 })) {
|
|
176
|
+
console.log(job.id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// A service with no all() method: paginate takes its list call and items.
|
|
180
|
+
for await (const project of paginate(
|
|
181
|
+
{},
|
|
182
|
+
(req, opts) => toshi.project.listProjects(req, opts),
|
|
183
|
+
(resp) => resp.projects,
|
|
184
|
+
)) {
|
|
185
|
+
console.log(project.name);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`paginate` sends a copy of the request, so the caller's request keeps its
|
|
190
|
+
fields. Every page is a call of its own, so `opts` goes to each one, and
|
|
191
|
+
`timeoutMs` bounds a page and not the whole iteration. An error throws once
|
|
192
|
+
and ends the iteration; leaving the loop sends no further request.
|
|
193
|
+
|
|
105
194
|
### Retries
|
|
106
195
|
|
|
107
196
|
The client retries a read, and a write that carries a request key, when
|
|
@@ -216,6 +305,35 @@ await run.wait(); // rejects with JobExitError on a non-zero exit
|
|
|
216
305
|
Stopping the loop stops only the output stream: the job keeps running,
|
|
217
306
|
and `workload.jobs.cancel` ends it.
|
|
218
307
|
|
|
308
|
+
## Testing with `@toshicloud/sdk/test`
|
|
309
|
+
|
|
310
|
+
`createFake()` serves the whole Workspace API in memory: no server, no port,
|
|
311
|
+
nothing to close, and no Node module, so it runs in the same runtimes as the
|
|
312
|
+
client. `fake.client()` returns a `ToshiClient` wired to it, with the token
|
|
313
|
+
`test-token` and the scope `default`/`production`, which is enough to drive
|
|
314
|
+
`run`, the watchers and the generated clients from a test.
|
|
315
|
+
|
|
316
|
+
<!-- examples/testing.ts -->
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { createFake } from "@toshicloud/sdk/test";
|
|
320
|
+
|
|
321
|
+
const fake = createFake();
|
|
322
|
+
fake.job("job-1").stdout("deployed\n").exitCode(0);
|
|
323
|
+
|
|
324
|
+
const toshi = fake.client();
|
|
325
|
+
const run = await toshi.run({ spec: { command: { argv: ["./deploy.sh"] } } });
|
|
326
|
+
for await (const chunk of run) {
|
|
327
|
+
process[chunk.stream].write(chunk.data);
|
|
328
|
+
}
|
|
329
|
+
await run.wait();
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`fake.on(method, impl)` replaces the state machine for one method with a hook
|
|
333
|
+
typed by that method: one call for unary, server-stream and bidirectional
|
|
334
|
+
methods alike. `fake.calls()` returns every call the fake received, oldest
|
|
335
|
+
first, with the procedure, the request and the headers.
|
|
336
|
+
|
|
219
337
|
## Generated code
|
|
220
338
|
|
|
221
339
|
`src/gen/toshi/workspace/v1/` is generated by
|
package/dist/client.d.ts
CHANGED
|
@@ -18,21 +18,32 @@ import { WorkspaceService } from "./gen/toshi/workspace/v1/workspace_service_pb.
|
|
|
18
18
|
import { CancelJobRequestSchema, CreateJobRequestSchema, CreateSandboxRequestSchema, CreateServiceRequestSchema, DeleteJobRequestSchema, DeleteSandboxRequestSchema, DeleteServiceRequestSchema, GetJobRequestSchema, GetSandboxRequestSchema, GetServiceRequestSchema, ListJobsRequestSchema, ListSandboxesRequestSchema, ListServicesRequestSchema, UpdateJobRequestSchema, UpdateSandboxRequestSchema, UpdateServiceRequestSchema, WaitJobRequestSchema, WaitSandboxRequestSchema, WaitServiceRequestSchema, WatchJobsRequestSchema, WatchSandboxesRequestSchema, WatchServicesRequestSchema } from "./gen/toshi/workspace/v1/workloads_pb.js";
|
|
19
19
|
import type { CancelJobResponse, CreateJobResponse, CreateSandboxResponse, CreateServiceResponse, DeleteJobResponse, DeleteSandboxResponse, DeleteServiceResponse, GetJobResponse, GetSandboxResponse, GetServiceResponse, ListJobsResponse, ListSandboxesResponse, ListServicesResponse, UpdateJobResponse, UpdateSandboxResponse, UpdateServiceResponse, WaitJobResponse, WaitSandboxResponse, WaitServiceResponse, WatchJobsResponse, WatchSandboxesResponse, WatchServicesResponse } from "./gen/toshi/workspace/v1/workloads_pb.js";
|
|
20
20
|
import { ExecuteStartSchema, GetOperationRequestSchema, HoldServiceRequestSchema, ListEventsRequestSchema, ListServiceRunsRequestSchema, ReleaseServiceRequestSchema, SetServiceModeRequestSchema, StartServiceRequestSchema, StopServiceRequestSchema, WaitOperationRequestSchema, WakeServiceRequestSchema, WatchEventsRequestSchema, WatchOutputRequestSchema } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
21
|
-
import type { GetOperationResponse, HoldServiceResponse, Job, ListEventsResponse, ListServiceRunsResponse, Operation, ReleaseServiceResponse, Sandbox, Service, SetServiceModeResponse, StartServiceResponse, StopServiceResponse, WakeServiceResponse, WatchEventsResponse, WatchOutputResponse } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
21
|
+
import type { Event, GetOperationResponse, HoldServiceResponse, Job, ListEventsResponse, ListServiceRunsResponse, Operation, ReleaseServiceResponse, Sandbox, Service, ServiceRun, SetServiceModeResponse, StartServiceResponse, StopServiceResponse, WakeServiceResponse, WatchEventsResponse, WatchOutputResponse } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
22
22
|
import type { RetryPolicy } from "./retry.js";
|
|
23
23
|
/** How the client is configured; the caller builds the transport. */
|
|
24
24
|
export interface ToshiOptions {
|
|
25
25
|
/** Builds the caller's transport (connect-node or connect-web) with the client's interceptors. */
|
|
26
26
|
transport: (interceptors: Interceptor[]) => Transport;
|
|
27
27
|
token?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Gives each request attempt its token, so a refreshed token is used without
|
|
30
|
+
* a new client; it wins over `token` and a retry asks it again. A source that
|
|
31
|
+
* throws or answers an empty token ends the call with Code.Unauthenticated,
|
|
32
|
+
* with its error as the cause, and the call is not retried.
|
|
33
|
+
*/
|
|
34
|
+
tokenSource?: (signal: AbortSignal | undefined) => string | Promise<string>;
|
|
28
35
|
project?: string;
|
|
29
36
|
environment?: string;
|
|
30
37
|
/** false turns off retries and watch reconnects. */
|
|
31
38
|
retry?: Partial<RetryPolicy> | false;
|
|
39
|
+
/** Bounds each unary call except the Wait calls, which their request's timeout bounds; streams ignore it. */
|
|
40
|
+
defaultTimeoutMs?: number;
|
|
32
41
|
}
|
|
33
42
|
/** Per-call options. */
|
|
34
43
|
export interface CallOptions {
|
|
35
44
|
signal?: AbortSignal;
|
|
45
|
+
/** Bounds the whole call, retries included; on a watch, the open. */
|
|
46
|
+
timeoutMs?: number;
|
|
36
47
|
}
|
|
37
48
|
/** One client per Workspace API service, plus the helpers that live on top of them. */
|
|
38
49
|
export interface ToshiClient {
|
|
@@ -63,6 +74,8 @@ export interface WorkloadClient {
|
|
|
63
74
|
waitOperation(req: MessageInitShape<typeof WaitOperationRequestSchema>, opts?: CallOptions): Promise<Operation>;
|
|
64
75
|
/** Lists the events after a cursor, a page at a time; `watchEvents` is the live form. */
|
|
65
76
|
listEvents(req: MessageInitShape<typeof ListEventsRequestSchema>, opts?: CallOptions): Promise<ListEventsResponse>;
|
|
77
|
+
/** Yields every event listEvents lists, following the page token from one page to the next. */
|
|
78
|
+
allEvents(req: MessageInitShape<typeof ListEventsRequestSchema>, opts?: CallOptions): AsyncIterable<Event>;
|
|
66
79
|
watchEvents(req: MessageInitShape<typeof WatchEventsRequestSchema>, opts?: CallOptions): Promise<EventWatch>;
|
|
67
80
|
/** Returns one operation: the id a long call hands back, asked for again. */
|
|
68
81
|
getOperation(req: MessageInitShape<typeof GetOperationRequestSchema>, opts?: CallOptions): Promise<GetOperationResponse>;
|
|
@@ -72,6 +85,8 @@ export interface JobsClient {
|
|
|
72
85
|
create(req: MessageInitShape<typeof CreateJobRequestSchema>, opts?: CallOptions): Promise<CreateJobResponse>;
|
|
73
86
|
get(req: MessageInitShape<typeof GetJobRequestSchema>, opts?: CallOptions): Promise<GetJobResponse>;
|
|
74
87
|
list(req: MessageInitShape<typeof ListJobsRequestSchema>, opts?: CallOptions): Promise<ListJobsResponse>;
|
|
88
|
+
/** Yields every job that matches every filter that is set, following the page token from one page to the next. */
|
|
89
|
+
all(req: MessageInitShape<typeof ListJobsRequestSchema>, opts?: CallOptions): AsyncIterable<Job>;
|
|
75
90
|
update(req: MessageInitShape<typeof UpdateJobRequestSchema>, opts?: CallOptions): Promise<UpdateJobResponse>;
|
|
76
91
|
delete(req: MessageInitShape<typeof DeleteJobRequestSchema>, opts?: CallOptions): Promise<DeleteJobResponse>;
|
|
77
92
|
cancel(req: MessageInitShape<typeof CancelJobRequestSchema>, opts?: CallOptions): Promise<CancelJobResponse>;
|
|
@@ -84,6 +99,8 @@ export interface SandboxesClient {
|
|
|
84
99
|
create(req: MessageInitShape<typeof CreateSandboxRequestSchema>, opts?: CallOptions): Promise<CreateSandboxResponse>;
|
|
85
100
|
get(req: MessageInitShape<typeof GetSandboxRequestSchema>, opts?: CallOptions): Promise<GetSandboxResponse>;
|
|
86
101
|
list(req: MessageInitShape<typeof ListSandboxesRequestSchema>, opts?: CallOptions): Promise<ListSandboxesResponse>;
|
|
102
|
+
/** Yields every sandbox that matches every filter that is set, following the page token from one page to the next. */
|
|
103
|
+
all(req: MessageInitShape<typeof ListSandboxesRequestSchema>, opts?: CallOptions): AsyncIterable<Sandbox>;
|
|
87
104
|
update(req: MessageInitShape<typeof UpdateSandboxRequestSchema>, opts?: CallOptions): Promise<UpdateSandboxResponse>;
|
|
88
105
|
delete(req: MessageInitShape<typeof DeleteSandboxRequestSchema>, opts?: CallOptions): Promise<DeleteSandboxResponse>;
|
|
89
106
|
wait(req: MessageInitShape<typeof WaitSandboxRequestSchema>, opts?: CallOptions): Promise<WaitSandboxResponse>;
|
|
@@ -95,6 +112,8 @@ export interface ServicesClient {
|
|
|
95
112
|
create(req: MessageInitShape<typeof CreateServiceRequestSchema>, opts?: CallOptions): Promise<CreateServiceResponse>;
|
|
96
113
|
get(req: MessageInitShape<typeof GetServiceRequestSchema>, opts?: CallOptions): Promise<GetServiceResponse>;
|
|
97
114
|
list(req: MessageInitShape<typeof ListServicesRequestSchema>, opts?: CallOptions): Promise<ListServicesResponse>;
|
|
115
|
+
/** Yields every service that matches every filter that is set, following the page token from one page to the next. */
|
|
116
|
+
all(req: MessageInitShape<typeof ListServicesRequestSchema>, opts?: CallOptions): AsyncIterable<Service>;
|
|
98
117
|
update(req: MessageInitShape<typeof UpdateServiceRequestSchema>, opts?: CallOptions): Promise<UpdateServiceResponse>;
|
|
99
118
|
delete(req: MessageInitShape<typeof DeleteServiceRequestSchema>, opts?: CallOptions): Promise<DeleteServiceResponse>;
|
|
100
119
|
wait(req: MessageInitShape<typeof WaitServiceRequestSchema>, opts?: CallOptions): Promise<WaitServiceResponse>;
|
|
@@ -106,6 +125,8 @@ export interface ServicesClient {
|
|
|
106
125
|
wake(req: MessageInitShape<typeof WakeServiceRequestSchema>, opts?: CallOptions): Promise<WakeServiceResponse>;
|
|
107
126
|
hold(req: MessageInitShape<typeof HoldServiceRequestSchema>, opts?: CallOptions): Promise<HoldServiceResponse>;
|
|
108
127
|
listRuns(req: MessageInitShape<typeof ListServiceRunsRequestSchema>, opts?: CallOptions): Promise<ListServiceRunsResponse>;
|
|
128
|
+
/** Yields every run listRuns returns, the newest first, following the page token from one page to the next. */
|
|
129
|
+
allRuns(req: MessageInitShape<typeof ListServiceRunsRequestSchema>, opts?: CallOptions): AsyncIterable<ServiceRun>;
|
|
109
130
|
}
|
|
110
131
|
/** A live stream of a job's changes. */
|
|
111
132
|
export interface JobWatch extends AsyncIterable<WatchJobsResponse> {
|
|
@@ -160,9 +181,9 @@ export interface JobRun extends AsyncIterable<OutputChunk> {
|
|
|
160
181
|
wait(): Promise<void>;
|
|
161
182
|
}
|
|
162
183
|
/**
|
|
163
|
-
* Creates the client: one normalized retry setting, the
|
|
164
|
-
*
|
|
165
|
-
* client and helper.
|
|
184
|
+
* Creates the client: one normalized retry setting, the scope, key, retry and
|
|
185
|
+
* auth interceptors (scope outermost, auth innermost, so no interceptor sees
|
|
186
|
+
* the Authorization header), one transport for every client and helper.
|
|
166
187
|
*
|
|
167
188
|
* Since 0.2.0 the workload calls are grouped by the type they name under
|
|
168
189
|
* `workload`; every other service, WorkspaceService's included, is handed out
|
package/dist/client.js
CHANGED
|
@@ -19,15 +19,17 @@ import { WorkspaceService } from "./gen/toshi/workspace/v1/workspace_service_pb.
|
|
|
19
19
|
import { CancelJobRequestSchema, CreateJobRequestSchema, CreateSandboxRequestSchema, CreateServiceRequestSchema, DeleteJobRequestSchema, DeleteSandboxRequestSchema, DeleteServiceRequestSchema, GetJobRequestSchema, GetSandboxRequestSchema, GetServiceRequestSchema, ListJobsRequestSchema, ListSandboxesRequestSchema, ListServicesRequestSchema, UpdateJobRequestSchema, UpdateSandboxRequestSchema, UpdateServiceRequestSchema, WaitJobRequestSchema, WaitSandboxRequestSchema, WaitServiceRequestSchema, WatchJobsRequestSchema, WatchSandboxesRequestSchema, WatchServicesRequestSchema, WorkloadService, } from "./gen/toshi/workspace/v1/workloads_pb.js";
|
|
20
20
|
import { ExecuteStartSchema, GetOperationRequestSchema, HoldServiceRequestSchema, ListEventsRequestSchema, ListServiceRunsRequestSchema, ReleaseServiceRequestSchema, SetServiceModeRequestSchema, StartServiceRequestSchema, StopServiceRequestSchema, WaitOperationRequestSchema, WakeServiceRequestSchema, WatchEventsRequestSchema, WatchOutputRequestSchema, } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
21
21
|
import { openOutputWatch } from "./output.js";
|
|
22
|
+
import { paginate } from "./paginate.js";
|
|
22
23
|
import { authInterceptor, keyInterceptor, retryInterceptor, scopeInterceptor } from "./interceptors.js";
|
|
23
24
|
import { normalizeRetry } from "./retry.js";
|
|
25
|
+
import { withDefaultUnaryTimeout } from "./timeout.js";
|
|
24
26
|
import { openEventWatch, openJobWatch, openSandboxWatch, openServiceWatch } from "./watch.js";
|
|
25
27
|
import { runJob } from "./run.js";
|
|
26
28
|
import { waitJob, waitOperation, waitSandbox, waitService } from "./wait.js";
|
|
27
29
|
/**
|
|
28
|
-
* Creates the client: one normalized retry setting, the
|
|
29
|
-
*
|
|
30
|
-
* client and helper.
|
|
30
|
+
* Creates the client: one normalized retry setting, the scope, key, retry and
|
|
31
|
+
* auth interceptors (scope outermost, auth innermost, so no interceptor sees
|
|
32
|
+
* the Authorization header), one transport for every client and helper.
|
|
31
33
|
*
|
|
32
34
|
* Since 0.2.0 the workload calls are grouped by the type they name under
|
|
33
35
|
* `workload`; every other service, WorkspaceService's included, is handed out
|
|
@@ -36,12 +38,12 @@ import { waitJob, waitOperation, waitSandbox, waitService } from "./wait.js";
|
|
|
36
38
|
export function createToshiClient(options) {
|
|
37
39
|
const retry = normalizeRetry(options.retry);
|
|
38
40
|
const interceptors = [
|
|
39
|
-
authInterceptor(options.token ?? ""),
|
|
40
41
|
scopeInterceptor(options.project ?? "", options.environment ?? ""),
|
|
41
42
|
keyInterceptor(),
|
|
42
43
|
retryInterceptor(retry),
|
|
44
|
+
authInterceptor(options.token ?? "", options.tokenSource),
|
|
43
45
|
];
|
|
44
|
-
const transport = options.transport(interceptors);
|
|
46
|
+
const transport = withDefaultUnaryTimeout(options.transport(interceptors), options.defaultTimeoutMs);
|
|
45
47
|
const workloadService = createClient(WorkloadService, transport);
|
|
46
48
|
return {
|
|
47
49
|
project: createClient(ProjectService, transport),
|
|
@@ -58,6 +60,7 @@ export function createToshiClient(options) {
|
|
|
58
60
|
create: (req, opts) => workloadService.createJob(req, opts),
|
|
59
61
|
get: (req, opts) => workloadService.getJob(req, opts),
|
|
60
62
|
list: (req, opts) => workloadService.listJobs(req, opts),
|
|
63
|
+
all: (req, opts) => paginate(req, workloadService.listJobs, (resp) => resp.jobs, opts),
|
|
61
64
|
update: (req, opts) => workloadService.updateJob(req, opts),
|
|
62
65
|
delete: (req, opts) => workloadService.deleteJob(req, opts),
|
|
63
66
|
cancel: (req, opts) => workloadService.cancelJob(req, opts),
|
|
@@ -69,6 +72,7 @@ export function createToshiClient(options) {
|
|
|
69
72
|
create: (req, opts) => workloadService.createSandbox(req, opts),
|
|
70
73
|
get: (req, opts) => workloadService.getSandbox(req, opts),
|
|
71
74
|
list: (req, opts) => workloadService.listSandboxes(req, opts),
|
|
75
|
+
all: (req, opts) => paginate(req, workloadService.listSandboxes, (resp) => resp.sandboxes, opts),
|
|
72
76
|
update: (req, opts) => workloadService.updateSandbox(req, opts),
|
|
73
77
|
delete: (req, opts) => workloadService.deleteSandbox(req, opts),
|
|
74
78
|
wait: (req, opts) => waitSandbox(workloadService, create(WaitSandboxRequestSchema, req), opts),
|
|
@@ -79,6 +83,7 @@ export function createToshiClient(options) {
|
|
|
79
83
|
create: (req, opts) => workloadService.createService(req, opts),
|
|
80
84
|
get: (req, opts) => workloadService.getService(req, opts),
|
|
81
85
|
list: (req, opts) => workloadService.listServices(req, opts),
|
|
86
|
+
all: (req, opts) => paginate(req, workloadService.listServices, (resp) => resp.services, opts),
|
|
82
87
|
update: (req, opts) => workloadService.updateService(req, opts),
|
|
83
88
|
delete: (req, opts) => workloadService.deleteService(req, opts),
|
|
84
89
|
wait: (req, opts) => waitService(workloadService, create(WaitServiceRequestSchema, req), opts),
|
|
@@ -90,9 +95,11 @@ export function createToshiClient(options) {
|
|
|
90
95
|
wake: (req, opts) => workloadService.wakeService(req, opts),
|
|
91
96
|
hold: (req, opts) => workloadService.holdService(req, opts),
|
|
92
97
|
listRuns: (req, opts) => workloadService.listServiceRuns(req, opts),
|
|
98
|
+
allRuns: (req, opts) => paginate(req, workloadService.listServiceRuns, (resp) => resp.runs, opts),
|
|
93
99
|
},
|
|
94
100
|
waitOperation: (req, opts) => waitOperation(workloadService, create(WaitOperationRequestSchema, req), opts),
|
|
95
101
|
listEvents: (req, opts) => workloadService.listEvents(req, opts),
|
|
102
|
+
allEvents: (req, opts) => paginate(req, workloadService.listEvents, (resp) => resp.events, opts),
|
|
96
103
|
watchEvents: (req, opts) => openEventWatch(workloadService, retry, create(WatchEventsRequestSchema, req), opts),
|
|
97
104
|
getOperation: (req, opts) => workloadService.getOperation(req, opts),
|
|
98
105
|
},
|
package/dist/execute.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Code, ConnectError } from "@connectrpc/connect";
|
|
|
3
3
|
import { createWritableIterable } from "@connectrpc/connect/protocol";
|
|
4
4
|
import { ExitError, StreamLostError } from "./errors.js";
|
|
5
5
|
import { isStreamLoss } from "./retry.js";
|
|
6
|
+
import { startDeadline, timeoutError } from "./timeout.js";
|
|
6
7
|
import { ExecuteRequestSchema, ExecuteStartSchema, } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
7
8
|
// The largest standard-input frame Execute sends (execute.go:14-16).
|
|
8
9
|
const STDIN_CHUNK = 64 << 10;
|
|
@@ -35,13 +36,24 @@ export async function executeCommand(service, start, opts) {
|
|
|
35
36
|
}
|
|
36
37
|
owner.process.cancel();
|
|
37
38
|
};
|
|
39
|
+
// A timeout bounds the handshake only; the process outlives it.
|
|
40
|
+
const deadline = startDeadline(opts?.timeoutMs, opts?.signal);
|
|
41
|
+
const onDeadline = () => {
|
|
42
|
+
if (owner.process === undefined) {
|
|
43
|
+
abort.abort(deadline?.signal.reason);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
38
46
|
// A signal that is already aborted starts nothing.
|
|
39
47
|
if (signal?.aborted === true) {
|
|
48
|
+
deadline?.clear();
|
|
40
49
|
throw cancelledError();
|
|
41
50
|
}
|
|
42
51
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
52
|
+
deadline?.signal.addEventListener("abort", onDeadline, { once: true });
|
|
43
53
|
const finishHandshake = () => {
|
|
44
54
|
signal?.removeEventListener("abort", onAbort);
|
|
55
|
+
deadline?.signal.removeEventListener("abort", onDeadline);
|
|
56
|
+
deadline?.clear();
|
|
45
57
|
};
|
|
46
58
|
const responses = service.execute(input, { signal: abort.signal });
|
|
47
59
|
const responseIterator = responses[Symbol.asyncIterator]();
|
|
@@ -73,6 +85,9 @@ export async function executeCommand(service, start, opts) {
|
|
|
73
85
|
if (signal !== undefined && signal.aborted) {
|
|
74
86
|
throw cancelledError();
|
|
75
87
|
}
|
|
88
|
+
if (deadline?.timedOut === true) {
|
|
89
|
+
throw timeoutError(opts?.timeoutMs);
|
|
90
|
+
}
|
|
76
91
|
throw beforeStarted(err, readRequestKey(startMessage));
|
|
77
92
|
}
|
|
78
93
|
const message = first.done === true ? undefined : first.value.message;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,10 @@ export * from "./gen/toshi/workspace/v1/storage_providers_pb.js";
|
|
|
16
16
|
export * from "./gen/toshi/workspace/v1/tokens_pb.js";
|
|
17
17
|
export * from "./gen/toshi/workspace/v1/workspace_service_pb.js";
|
|
18
18
|
export { createToshiClient } from "./client.js";
|
|
19
|
+
export { paginate } from "./paginate.js";
|
|
19
20
|
export { ExitError, JobExitError, StreamLostError } from "./errors.js";
|
|
20
21
|
export { defaultRetryPolicy } from "./retry.js";
|
|
22
|
+
export { VERSION } from "./version.js";
|
|
23
|
+
export type { PageRequest, PageResponse } from "./paginate.js";
|
|
21
24
|
export type { RetryPolicy } from "./retry.js";
|
|
22
25
|
export type { CallOptions, EventWatch, JobRun, JobWatch, JobsClient, OutputChunk, OutputWatch, Process, SandboxWatch, SandboxesClient, ServiceWatch, ServicesClient, ToshiClient, ToshiOptions, WorkloadClient, } from "./client.js";
|
package/dist/index.js
CHANGED
|
@@ -17,5 +17,7 @@ export * from "./gen/toshi/workspace/v1/storage_providers_pb.js";
|
|
|
17
17
|
export * from "./gen/toshi/workspace/v1/tokens_pb.js";
|
|
18
18
|
export * from "./gen/toshi/workspace/v1/workspace_service_pb.js";
|
|
19
19
|
export { createToshiClient } from "./client.js";
|
|
20
|
+
export { paginate } from "./paginate.js";
|
|
20
21
|
export { ExitError, JobExitError, StreamLostError } from "./errors.js";
|
|
21
22
|
export { defaultRetryPolicy } from "./retry.js";
|
|
23
|
+
export { VERSION } from "./version.js";
|
package/dist/interceptors.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { Interceptor } from "@connectrpc/connect";
|
|
2
2
|
import type { RetrySetting } from "./retry.js";
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
3
|
+
/** Gives one request attempt its token; the signal is the call's. */
|
|
4
|
+
export type TokenSource = (signal: AbortSignal | undefined) => string | Promise<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Sends `Authorization: Bearer <token>` on every call and stream, on a copy of
|
|
7
|
+
* the request: it is the innermost interceptor, so none outside it sees the
|
|
8
|
+
* header (intercept.go:13-31). Never Toshi-Tenant.
|
|
9
|
+
*
|
|
10
|
+
* With a source, the token is asked for on every attempt the innermost
|
|
11
|
+
* interceptor runs for, so a retry asks again (client.go:154-169).
|
|
12
|
+
*/
|
|
13
|
+
export declare function authInterceptor(token: string, source?: TokenSource): Interceptor;
|
|
5
14
|
/** Fills an empty project and environment from the options; caller values win (intercept.go:38-129). */
|
|
6
15
|
export declare function scopeInterceptor(project: string, environment: string): Interceptor;
|
|
7
16
|
/** Puts a request key on keyed procedures and on Execute's start (retry.go:110-116, 202-231). */
|
package/dist/interceptors.js
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
1
|
import { ScalarType } from "@bufbuild/protobuf";
|
|
2
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
2
3
|
import { backoffDelay, isTransportLoss, newRequestKey, procedurePath, retryClassFor, sleep, } from "./retry.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Sends `Authorization: Bearer <token>` on every call and stream, on a copy of
|
|
6
|
+
* the request: it is the innermost interceptor, so none outside it sees the
|
|
7
|
+
* header (intercept.go:13-31). Never Toshi-Tenant.
|
|
8
|
+
*
|
|
9
|
+
* With a source, the token is asked for on every attempt the innermost
|
|
10
|
+
* interceptor runs for, so a retry asks again (client.go:154-169).
|
|
11
|
+
*/
|
|
12
|
+
export function authInterceptor(token, source) {
|
|
13
|
+
if (source !== undefined) {
|
|
14
|
+
return (next) => async (req) => {
|
|
15
|
+
const value = await sourceToken(source, req.signal);
|
|
16
|
+
const header = new Headers(req.header);
|
|
17
|
+
header.set("Authorization", `Bearer ${value}`);
|
|
18
|
+
return next({ ...req, header });
|
|
19
|
+
};
|
|
20
|
+
}
|
|
5
21
|
if (token === "") {
|
|
6
22
|
return (next) => next;
|
|
7
23
|
}
|
|
8
24
|
return (next) => async (req) => {
|
|
9
|
-
req.header
|
|
10
|
-
|
|
25
|
+
const header = new Headers(req.header);
|
|
26
|
+
header.set("Authorization", `Bearer ${token}`);
|
|
27
|
+
return next({ ...req, header });
|
|
11
28
|
};
|
|
12
29
|
}
|
|
30
|
+
// Asks the source for this attempt's token, trimmed; a source that throws or
|
|
31
|
+
// answers empty ends the call as UNAUTHENTICATED with the source's error as the
|
|
32
|
+
// cause, which is not a transport loss and so is not retried (client.go:186-201).
|
|
33
|
+
async function sourceToken(source, signal) {
|
|
34
|
+
let token;
|
|
35
|
+
try {
|
|
36
|
+
token = await source(signal);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw new ConnectError(`toshi: token source: ${message(err)}`, Code.Unauthenticated, undefined, undefined, err);
|
|
40
|
+
}
|
|
41
|
+
if (token.trim() === "") {
|
|
42
|
+
throw new ConnectError("toshi: token source: empty token", Code.Unauthenticated);
|
|
43
|
+
}
|
|
44
|
+
return token.trim();
|
|
45
|
+
}
|
|
46
|
+
function message(err) {
|
|
47
|
+
return err instanceof Error ? err.message : String(err);
|
|
48
|
+
}
|
|
13
49
|
/** Fills an empty project and environment from the options; caller values win (intercept.go:38-129). */
|
|
14
50
|
export function scopeInterceptor(project, environment) {
|
|
15
51
|
if (project === "" && environment === "") {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Interceptor, Transport } from "@connectrpc/connect";
|
|
2
|
+
import type { ToshiClient, ToshiOptions } from "../client.js";
|
|
3
|
+
/** Options for the Node entry: the root options, a built transport, and env defaults. */
|
|
4
|
+
export interface NodeToshiOptions extends Omit<ToshiOptions, "transport"> {
|
|
5
|
+
/** A transport of your own; without one, connect-node over HTTP/2 to `server` is built. */
|
|
6
|
+
transport?: (interceptors: Interceptor[]) => Transport;
|
|
7
|
+
/** The control plane's base URL; defaults to TOSHI_SERVER. */
|
|
8
|
+
server?: string;
|
|
9
|
+
/**
|
|
10
|
+
* A file holding the token, trimmed; an empty file is an error. It is read
|
|
11
|
+
* again before a request when its modification time or size changed.
|
|
12
|
+
*/
|
|
13
|
+
tokenFile?: string;
|
|
14
|
+
/** A PEM bundle trusted besides Node's built-in roots; defaults to TOSHI_CA_FILE. */
|
|
15
|
+
caFile?: string;
|
|
16
|
+
/** Put in front of the User-Agent, for example "myapp/1.2"; empty adds nothing. */
|
|
17
|
+
userAgent?: string;
|
|
18
|
+
/** Run before the client's scope, key, retry and auth interceptors, in this order. */
|
|
19
|
+
interceptors?: Interceptor[];
|
|
20
|
+
/** Bounds each unary call except the Wait calls, which their request's timeout bounds; streams ignore it. */
|
|
21
|
+
defaultTimeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Creates a Node client. Without a transport of its own it builds one with
|
|
25
|
+
* connect-node over HTTP/2, takes the server from `server` or TOSHI_SERVER and
|
|
26
|
+
* the token from `tokenSource`, `token`, `tokenFile`, TOSHI_TOKEN or
|
|
27
|
+
* TOSHI_TOKEN_FILE.
|
|
28
|
+
*
|
|
29
|
+
* The User-Agent is "toshi-ts/<version> (node/<node version>)", with
|
|
30
|
+
* `userAgent` in front when it is set. Connect exports no version constant, so
|
|
31
|
+
* its name is not part of the string.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createToshiClient(options?: NodeToshiOptions): ToshiClient;
|
|
34
|
+
/** The User-Agent every Node request carries, with product in front when set. */
|
|
35
|
+
export declare function userAgent(product: string): string;
|