@toshicloud/sdk 0.3.2 → 0.5.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 (59) hide show
  1. package/README.md +189 -30
  2. package/dist/client.d.ts +30 -4
  3. package/dist/client.js +19 -6
  4. package/dist/errors.d.ts +47 -0
  5. package/dist/errors.js +136 -0
  6. package/dist/execute.js +15 -0
  7. package/dist/gen/toshi/permission_pb.d.ts +39 -0
  8. package/dist/gen/toshi/permission_pb.js +42 -0
  9. package/dist/gen/toshi/workspace/v1/artifacts_pb.d.ts +1 -1
  10. package/dist/gen/toshi/workspace/v1/artifacts_pb.js +3 -2
  11. package/dist/gen/toshi/workspace/v1/audit_events_pb.js +2 -1
  12. package/dist/gen/toshi/workspace/v1/channels_pb.js +2 -1
  13. package/dist/gen/toshi/workspace/v1/discovery_pb.d.ts +676 -0
  14. package/dist/gen/toshi/workspace/v1/discovery_pb.js +182 -0
  15. package/dist/gen/toshi/workspace/v1/display_pb.js +2 -1
  16. package/dist/gen/toshi/workspace/v1/fillers_pb.js +2 -1
  17. package/dist/gen/toshi/workspace/v1/hostjobs_pb.js +2 -1
  18. package/dist/gen/toshi/workspace/v1/hosts_pb.js +2 -1
  19. package/dist/gen/toshi/workspace/v1/products_pb.d.ts +703 -0
  20. package/dist/gen/toshi/workspace/v1/products_pb.js +106 -0
  21. package/dist/gen/toshi/workspace/v1/projects_pb.js +2 -1
  22. package/dist/gen/toshi/workspace/v1/recordings_pb.js +2 -1
  23. package/dist/gen/toshi/workspace/v1/secrets_pb.js +2 -1
  24. package/dist/gen/toshi/workspace/v1/storage_providers_pb.js +2 -1
  25. package/dist/gen/toshi/workspace/v1/tokens_pb.js +2 -1
  26. package/dist/gen/toshi/workspace/v1/volumes_pb.js +2 -1
  27. package/dist/gen/toshi/workspace/v1/webhook_pb.js +2 -1
  28. package/dist/gen/toshi/workspace/v1/workloads_pb.js +2 -1
  29. package/dist/gen/toshi/workspace/v1/workspace_pb.d.ts +2 -2
  30. package/dist/gen/toshi/workspace/v1/workspace_pb.js +1 -1
  31. package/dist/gen/toshi/workspace/v1/workspace_service_pb.js +2 -1
  32. package/dist/index.d.ts +6 -1
  33. package/dist/index.js +5 -1
  34. package/dist/interceptors.d.ts +17 -2
  35. package/dist/interceptors.js +69 -4
  36. package/dist/node/client.d.ts +35 -0
  37. package/dist/node/client.js +178 -0
  38. package/dist/node/index.d.ts +3 -0
  39. package/dist/node/index.js +4 -0
  40. package/dist/output.js +6 -1
  41. package/dist/paginate.d.ts +36 -0
  42. package/dist/paginate.js +37 -0
  43. package/dist/retry.js +4 -0
  44. package/dist/run.js +3 -2
  45. package/dist/stream.d.ts +8 -1
  46. package/dist/stream.js +24 -2
  47. package/dist/test/fake.d.ts +88 -0
  48. package/dist/test/fake.js +845 -0
  49. package/dist/test/index.d.ts +58 -0
  50. package/dist/test/index.js +57 -0
  51. package/dist/test/procedures.d.ts +18 -0
  52. package/dist/test/procedures.js +219 -0
  53. package/dist/timeout.d.ts +29 -0
  54. package/dist/timeout.js +67 -0
  55. package/dist/version.d.ts +2 -0
  56. package/dist/version.js +2 -0
  57. package/dist/wait.js +15 -12
  58. package/dist/watch.js +15 -2
  59. package/package.json +19 -3
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A fake Toshi for tests: an in-memory Workspace API served through
3
+ * `@connectrpc/connect`'s router transport.
4
+ *
5
+ * A test creates a fake, tells it what to answer, and calls the client:
6
+ *
7
+ * ```ts
8
+ * const fake = createFake();
9
+ * fake.job("job-1").stdout("deployed\n").exitCode(0);
10
+ * const toshi = fake.client();
11
+ * ```
12
+ *
13
+ * `createFake` serves every service of `toshi.workspace.v1` from one
14
+ * in-memory transport: no server, no port, nothing to close, and no Node
15
+ * module, so it runs wherever the client runs. `client` returns a
16
+ * `ToshiClient` wired to it, with the token "test-token" and the scope
17
+ * default/production.
18
+ *
19
+ * # What the fake answers
20
+ *
21
+ * The fake holds one workspace and has no clock: every call answers at once
22
+ * with what the test set. It knows the job and the sandbox lifecycle and
23
+ * nothing else.
24
+ *
25
+ * - `job`, `sandbox` and `execute` set what the fake answers: a job's or a
26
+ * sandbox's state, and the standard output, the standard error and the exit
27
+ * code of a job or of a command run in a sandbox. A job or a sandbox the
28
+ * test set no state for is running; a job or a command the test set no exit
29
+ * code for exits 0.
30
+ * - CreateJob answers the jobs registered with `job`, in the order they were
31
+ * registered, and then creates job-1, job-2, ... of its own. CreateSandbox
32
+ * does the same for `sandbox` and sandbox-1, sandbox-2, .... A create whose
33
+ * request key the fake has already answered returns the same workload.
34
+ * - GetJob, ListJobs, CancelJob, WaitJob and WatchOutput answer from the jobs
35
+ * that exist; GetSandbox, ListSandboxes, WaitSandbox and WatchSandboxes
36
+ * from the sandboxes. An id the fake does not hold is NOT_FOUND. The lists
37
+ * answer every workload the fake holds, in creation order: they do not apply
38
+ * the request's filters or page size.
39
+ * - WaitJob and WaitSandbox answer at once, reached, with the workload as the
40
+ * test set it.
41
+ * - WatchOutput streams a job's standard output and standard error from the
42
+ * offsets the request names, then the job's end.
43
+ * - WatchJobs and WatchSandboxes report each change made after the cursor the
44
+ * request names: a workload the fake creates, and each state the test sets.
45
+ * ListJobs and ListSandboxes answer the cursor a watch starts at.
46
+ * - Execute answers the start message with an execution id, the standard
47
+ * output, the standard error and the exit code the test set for the sandbox.
48
+ * It does not read standard input.
49
+ *
50
+ * Every other procedure is hook-only. An unhooked procedure the state machine
51
+ * does not cover answers UNIMPLEMENTED, naming the procedure and the `on` call
52
+ * that supplies an answer. A hook replaces the state machine for its method.
53
+ *
54
+ * `calls` returns every call the fake received, in order, with its request and
55
+ * its headers.
56
+ */
57
+ export { createFake, testToken } from "./fake.js";
58
+ export type { Call, Fake, FakeExecution, FakeJob, FakeOptions, FakeSandbox } from "./fake.js";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A fake Toshi for tests: an in-memory Workspace API served through
3
+ * `@connectrpc/connect`'s router transport.
4
+ *
5
+ * A test creates a fake, tells it what to answer, and calls the client:
6
+ *
7
+ * ```ts
8
+ * const fake = createFake();
9
+ * fake.job("job-1").stdout("deployed\n").exitCode(0);
10
+ * const toshi = fake.client();
11
+ * ```
12
+ *
13
+ * `createFake` serves every service of `toshi.workspace.v1` from one
14
+ * in-memory transport: no server, no port, nothing to close, and no Node
15
+ * module, so it runs wherever the client runs. `client` returns a
16
+ * `ToshiClient` wired to it, with the token "test-token" and the scope
17
+ * default/production.
18
+ *
19
+ * # What the fake answers
20
+ *
21
+ * The fake holds one workspace and has no clock: every call answers at once
22
+ * with what the test set. It knows the job and the sandbox lifecycle and
23
+ * nothing else.
24
+ *
25
+ * - `job`, `sandbox` and `execute` set what the fake answers: a job's or a
26
+ * sandbox's state, and the standard output, the standard error and the exit
27
+ * code of a job or of a command run in a sandbox. A job or a sandbox the
28
+ * test set no state for is running; a job or a command the test set no exit
29
+ * code for exits 0.
30
+ * - CreateJob answers the jobs registered with `job`, in the order they were
31
+ * registered, and then creates job-1, job-2, ... of its own. CreateSandbox
32
+ * does the same for `sandbox` and sandbox-1, sandbox-2, .... A create whose
33
+ * request key the fake has already answered returns the same workload.
34
+ * - GetJob, ListJobs, CancelJob, WaitJob and WatchOutput answer from the jobs
35
+ * that exist; GetSandbox, ListSandboxes, WaitSandbox and WatchSandboxes
36
+ * from the sandboxes. An id the fake does not hold is NOT_FOUND. The lists
37
+ * answer every workload the fake holds, in creation order: they do not apply
38
+ * the request's filters or page size.
39
+ * - WaitJob and WaitSandbox answer at once, reached, with the workload as the
40
+ * test set it.
41
+ * - WatchOutput streams a job's standard output and standard error from the
42
+ * offsets the request names, then the job's end.
43
+ * - WatchJobs and WatchSandboxes report each change made after the cursor the
44
+ * request names: a workload the fake creates, and each state the test sets.
45
+ * ListJobs and ListSandboxes answer the cursor a watch starts at.
46
+ * - Execute answers the start message with an execution id, the standard
47
+ * output, the standard error and the exit code the test set for the sandbox.
48
+ * It does not read standard input.
49
+ *
50
+ * Every other procedure is hook-only. An unhooked procedure the state machine
51
+ * does not cover answers UNIMPLEMENTED, naming the procedure and the `on` call
52
+ * that supplies an answer. A hook replaces the state machine for its method.
53
+ *
54
+ * `calls` returns every call the fake received, in order, with its request and
55
+ * its headers.
56
+ */
57
+ export { createFake, testToken } from "./fake.js";
@@ -0,0 +1,18 @@
1
+ import type { DescMethod, DescService } from "@bufbuild/protobuf";
2
+ /** The package the fake serves. */
3
+ export declare const apiPackage = "toshi.workspace.v1";
4
+ /** Every service of the Workspace API. */
5
+ export declare const apiServices: readonly DescService[];
6
+ /** The path a client calls a method by, for example `/toshi.workspace.v1.WorkloadService/CreateJob`. */
7
+ export declare function procedurePath(method: DescMethod): string;
8
+ /** Every method of the Workspace API, keyed by the path a client calls it by. */
9
+ export declare const apiMethods: ReadonlyMap<string, DescMethod>;
10
+ /** Whether method is a method of the Workspace API this package was generated from. */
11
+ export declare function isApiMethod(method: DescMethod): boolean;
12
+ /**
13
+ * The methods the fake has no built-in answer for: a test supplies their
14
+ * answer with `on`. Every method of the Workspace API is either answered by
15
+ * the state machine or listed here, and the coverage test fails when one is
16
+ * neither, so a new method is a decision and not a surprise at a caller.
17
+ */
18
+ export declare const hookOnly: readonly DescMethod[];
@@ -0,0 +1,219 @@
1
+ import { ArtifactService, ArtifactUploadService } from "../gen/toshi/workspace/v1/artifacts_pb.js";
2
+ import { AuditEventService } from "../gen/toshi/workspace/v1/audit_events_pb.js";
3
+ import { ChannelService } from "../gen/toshi/workspace/v1/channels_pb.js";
4
+ import { DisplayService } from "../gen/toshi/workspace/v1/display_pb.js";
5
+ import { FillerService } from "../gen/toshi/workspace/v1/fillers_pb.js";
6
+ import { HostJobService } from "../gen/toshi/workspace/v1/hostjobs_pb.js";
7
+ import { HostService } from "../gen/toshi/workspace/v1/hosts_pb.js";
8
+ import { ProjectService } from "../gen/toshi/workspace/v1/projects_pb.js";
9
+ import { RecordingService } from "../gen/toshi/workspace/v1/recordings_pb.js";
10
+ import { SecretService } from "../gen/toshi/workspace/v1/secrets_pb.js";
11
+ import { StorageProviderService } from "../gen/toshi/workspace/v1/storage_providers_pb.js";
12
+ import { TokenService } from "../gen/toshi/workspace/v1/tokens_pb.js";
13
+ import { VolumeService } from "../gen/toshi/workspace/v1/volumes_pb.js";
14
+ import { WebhookService } from "../gen/toshi/workspace/v1/webhook_pb.js";
15
+ import { WorkloadService } from "../gen/toshi/workspace/v1/workloads_pb.js";
16
+ import { WorkspaceService } from "../gen/toshi/workspace/v1/workspace_service_pb.js";
17
+ /** The package the fake serves. */
18
+ export const apiPackage = "toshi.workspace.v1";
19
+ /** Every service of the Workspace API. */
20
+ export const apiServices = [
21
+ WorkloadService,
22
+ SecretService,
23
+ ProjectService,
24
+ WebhookService,
25
+ DisplayService,
26
+ ChannelService,
27
+ RecordingService,
28
+ VolumeService,
29
+ ArtifactService,
30
+ ArtifactUploadService,
31
+ AuditEventService,
32
+ FillerService,
33
+ HostJobService,
34
+ HostService,
35
+ TokenService,
36
+ StorageProviderService,
37
+ WorkspaceService,
38
+ ];
39
+ /** The path a client calls a method by, for example `/toshi.workspace.v1.WorkloadService/CreateJob`. */
40
+ export function procedurePath(method) {
41
+ return `/${method.parent.typeName}/${method.name}`;
42
+ }
43
+ /** Every method of the Workspace API, keyed by the path a client calls it by. */
44
+ export const apiMethods = buildApiMethods();
45
+ function buildApiMethods() {
46
+ const found = new Map();
47
+ for (const service of apiServices) {
48
+ for (const method of service.methods) {
49
+ found.set(procedurePath(method), method);
50
+ }
51
+ }
52
+ return found;
53
+ }
54
+ /** Whether method is a method of the Workspace API this package was generated from. */
55
+ export function isApiMethod(method) {
56
+ return apiMethods.get(procedurePath(method)) === method;
57
+ }
58
+ /**
59
+ * The methods the fake has no built-in answer for: a test supplies their
60
+ * answer with `on`. Every method of the Workspace API is either answered by
61
+ * the state machine or listed here, and the coverage test fails when one is
62
+ * neither, so a new method is a decision and not a surprise at a caller.
63
+ */
64
+ export const hookOnly = [
65
+ // WorkloadService
66
+ WorkloadService.method.updateJob,
67
+ WorkloadService.method.deleteJob,
68
+ WorkloadService.method.updateSandbox,
69
+ WorkloadService.method.deleteSandbox,
70
+ WorkloadService.method.cancelExecution,
71
+ WorkloadService.method.watchTelemetry,
72
+ WorkloadService.method.watchGuestLog,
73
+ WorkloadService.method.createPreviewShareLink,
74
+ WorkloadService.method.listPreviewShareLinks,
75
+ WorkloadService.method.revokePreviewShareLink,
76
+ WorkloadService.method.createService,
77
+ WorkloadService.method.getService,
78
+ WorkloadService.method.listServices,
79
+ WorkloadService.method.updateService,
80
+ WorkloadService.method.deleteService,
81
+ WorkloadService.method.waitService,
82
+ WorkloadService.method.watchServices,
83
+ WorkloadService.method.startService,
84
+ WorkloadService.method.stopService,
85
+ WorkloadService.method.setServiceMode,
86
+ WorkloadService.method.wakeService,
87
+ WorkloadService.method.holdService,
88
+ WorkloadService.method.releaseService,
89
+ WorkloadService.method.listServiceRuns,
90
+ WorkloadService.method.listEvents,
91
+ WorkloadService.method.watchEvents,
92
+ WorkloadService.method.getOperation,
93
+ WorkloadService.method.waitOperation,
94
+ // SecretService
95
+ SecretService.method.createSecretSet,
96
+ SecretService.method.listSecretSets,
97
+ SecretService.method.deleteSecretSet,
98
+ SecretService.method.putSecret,
99
+ SecretService.method.deleteSecret,
100
+ SecretService.method.listSecrets,
101
+ SecretService.method.listSecretVersions,
102
+ SecretService.method.grantSecretSet,
103
+ SecretService.method.revokeSecretSet,
104
+ SecretService.method.listSecretSetGrants,
105
+ // ProjectService
106
+ ProjectService.method.listProjects,
107
+ ProjectService.method.getProject,
108
+ ProjectService.method.listRepositories,
109
+ ProjectService.method.createProject,
110
+ ProjectService.method.claimRepository,
111
+ ProjectService.method.createEnvironment,
112
+ ProjectService.method.deleteEnvironment,
113
+ ProjectService.method.listBranchRules,
114
+ ProjectService.method.setBranchRules,
115
+ ProjectService.method.createEmbedToken,
116
+ ProjectService.method.listEmbedTokens,
117
+ ProjectService.method.revokeEmbedToken,
118
+ // WebhookService
119
+ WebhookService.method.createWebhookEndpoint,
120
+ WebhookService.method.getWebhookEndpoint,
121
+ WebhookService.method.listWebhookEndpoints,
122
+ WebhookService.method.updateWebhookEndpoint,
123
+ WebhookService.method.deleteWebhookEndpoint,
124
+ WebhookService.method.rotateWebhookEndpointSecret,
125
+ WebhookService.method.listWebhookDeliveries,
126
+ WebhookService.method.replayWebhookEvents,
127
+ // DisplayService
128
+ DisplayService.method.openDisplaySession,
129
+ DisplayService.method.getDisplaySession,
130
+ DisplayService.method.listDisplaySessions,
131
+ DisplayService.method.closeDisplaySession,
132
+ DisplayService.method.sendDisplayKeys,
133
+ DisplayService.method.sendDisplayPointer,
134
+ DisplayService.method.setDisplayClipboard,
135
+ DisplayService.method.takeDisplayScreenshot,
136
+ // ChannelService
137
+ ChannelService.method.createChannel,
138
+ ChannelService.method.listChannels,
139
+ ChannelService.method.getChannel,
140
+ ChannelService.method.getChannelPolicy,
141
+ ChannelService.method.setChannelPolicy,
142
+ ChannelService.method.listChannelRequests,
143
+ ChannelService.method.getChannelRequest,
144
+ ChannelService.method.exportChannelLog,
145
+ ChannelService.method.createChannelConnectToken,
146
+ ChannelService.method.revokeChannelConnectToken,
147
+ ChannelService.method.deleteChannel,
148
+ ChannelService.method.listForwards,
149
+ ChannelService.method.getLink,
150
+ // RecordingService
151
+ RecordingService.method.listRecordings,
152
+ RecordingService.method.getRecording,
153
+ RecordingService.method.createRecordingShareLink,
154
+ RecordingService.method.listRecordingShareLinks,
155
+ RecordingService.method.revokeRecordingShareLink,
156
+ // VolumeService
157
+ VolumeService.method.listVolumes,
158
+ VolumeService.method.attachVolume,
159
+ VolumeService.method.detachVolume,
160
+ // ArtifactService
161
+ ArtifactService.method.listArtifacts,
162
+ ArtifactService.method.getArtifact,
163
+ ArtifactService.method.getArtifactDownloadURL,
164
+ // ArtifactUploadService
165
+ ArtifactUploadService.method.reserveUpload,
166
+ ArtifactUploadService.method.completeUpload,
167
+ // AuditEventService
168
+ AuditEventService.method.listAuditEvents,
169
+ // FillerService
170
+ FillerService.method.listFillers,
171
+ FillerService.method.putFiller,
172
+ FillerService.method.deleteFiller,
173
+ FillerService.method.setFillerEnabled,
174
+ FillerService.method.listFillerRuns,
175
+ // HostJobService
176
+ HostJobService.method.listHostJobs,
177
+ HostJobService.method.getHostJob,
178
+ HostJobService.method.cancelHostJob,
179
+ HostJobService.method.getHostJobFailure,
180
+ HostJobService.method.getHostJobTelemetry,
181
+ HostJobService.method.getHostJobDebugAccess,
182
+ HostJobService.method.stopHostJobDebug,
183
+ // HostService
184
+ HostService.method.listHosts,
185
+ HostService.method.listHostStorage,
186
+ HostService.method.listHostAlerts,
187
+ HostService.method.setHostState,
188
+ HostService.method.retireHost,
189
+ HostService.method.createCloudHost,
190
+ HostService.method.checkCloudHost,
191
+ HostService.method.createEnrollmentToken,
192
+ // TokenService
193
+ TokenService.method.listTokens,
194
+ TokenService.method.createWorkspaceToken,
195
+ TokenService.method.revokeToken,
196
+ // StorageProviderService
197
+ StorageProviderService.method.listStorageProviders,
198
+ StorageProviderService.method.createStorageProvider,
199
+ StorageProviderService.method.checkStorageProvider,
200
+ StorageProviderService.method.deleteStorageProvider,
201
+ // WorkspaceService
202
+ WorkspaceService.method.getWorkspace,
203
+ WorkspaceService.method.listWorkspaces,
204
+ WorkspaceService.method.getWorkspaceBilling,
205
+ WorkspaceService.method.getWorkspaceStatement,
206
+ WorkspaceService.method.setWorkspaceOwnHostsOnly,
207
+ WorkspaceService.method.setWorkspaceAllowOverflow,
208
+ WorkspaceService.method.setWorkspaceAllowCacheWarm,
209
+ WorkspaceService.method.setWorkspaceCancelRunOnVmDeath,
210
+ WorkspaceService.method.setWorkspaceSpendLimits,
211
+ WorkspaceService.method.listEgressIPs,
212
+ WorkspaceService.method.getUsage,
213
+ WorkspaceService.method.getQueueTimes,
214
+ WorkspaceService.method.getWorkflowUsage,
215
+ WorkspaceService.method.listPlans,
216
+ WorkspaceService.method.createCheckoutSession,
217
+ WorkspaceService.method.createBillingPortalSession,
218
+ WorkspaceService.method.getVersion,
219
+ ];
@@ -0,0 +1,29 @@
1
+ import { ConnectError } from "@connectrpc/connect";
2
+ import type { Transport } from "@connectrpc/connect";
3
+ /** The error a call ends with when its timeout fires. */
4
+ export declare function timeoutError(timeoutMs: number): ConnectError;
5
+ /** A running deadline: one timer, one signal, and its state for the caller. */
6
+ export interface Deadline {
7
+ /** Aborts when the timeout fires or the caller's signal aborts. */
8
+ readonly signal: AbortSignal;
9
+ /** Whether the timeout fired, rather than the caller's signal. */
10
+ readonly timedOut: boolean;
11
+ /** Whether the caller's own signal aborted. */
12
+ readonly callerAborted: boolean;
13
+ /** Stops the timer; safe to call more than once. */
14
+ clear(): void;
15
+ }
16
+ /** Starts the deadline for a call's timeoutMs, or returns undefined without one. */
17
+ export declare function startDeadline(timeoutMs: number | undefined, caller?: AbortSignal): Deadline | undefined;
18
+ /** Runs body with a deadline signal, and reports the timeout instead of an abort. */
19
+ export declare function runWithDeadline<O extends {
20
+ signal?: AbortSignal;
21
+ timeoutMs?: number;
22
+ }, T>(opts: O | undefined, body: (opts: O | undefined) => Promise<T>): Promise<T>;
23
+ /**
24
+ * Gives every unary call defaultTimeoutMs when the caller set no timeout. The
25
+ * Wait calls are long polls: their request's timeout bounds them, so the
26
+ * default skips them. A stream keeps the timeout it was given, so a default
27
+ * never ends a watch.
28
+ */
29
+ export declare function withDefaultUnaryTimeout(transport: Transport, timeoutMs: number | undefined): Transport;
@@ -0,0 +1,67 @@
1
+ import { Code, ConnectError } from "@connectrpc/connect";
2
+ /** The error a call ends with when its timeout fires. */
3
+ export function timeoutError(timeoutMs) {
4
+ return new ConnectError(`the call timed out after ${timeoutMs} ms`, Code.DeadlineExceeded);
5
+ }
6
+ /** Starts the deadline for a call's timeoutMs, or returns undefined without one. */
7
+ export function startDeadline(timeoutMs, caller) {
8
+ if (timeoutMs === undefined) {
9
+ return undefined;
10
+ }
11
+ const controller = new AbortController();
12
+ let fired = false;
13
+ const timer = setTimeout(() => {
14
+ fired = true;
15
+ controller.abort(timeoutError(timeoutMs));
16
+ }, timeoutMs);
17
+ return {
18
+ signal: caller === undefined ? controller.signal : AbortSignal.any([controller.signal, caller]),
19
+ get timedOut() {
20
+ return fired;
21
+ },
22
+ get callerAborted() {
23
+ return caller?.aborted === true;
24
+ },
25
+ clear() {
26
+ clearTimeout(timer);
27
+ },
28
+ };
29
+ }
30
+ /** Runs body with a deadline signal, and reports the timeout instead of an abort. */
31
+ export async function runWithDeadline(opts, body) {
32
+ const deadline = startDeadline(opts?.timeoutMs, opts?.signal);
33
+ const withDeadline = (deadline === undefined ? opts : { ...opts, signal: deadline.signal });
34
+ try {
35
+ return await body(withDeadline);
36
+ }
37
+ catch (err) {
38
+ if (deadline?.timedOut === true && !deadline.callerAborted) {
39
+ throw timeoutError(opts?.timeoutMs);
40
+ }
41
+ throw err;
42
+ }
43
+ finally {
44
+ deadline?.clear();
45
+ }
46
+ }
47
+ /**
48
+ * Gives every unary call defaultTimeoutMs when the caller set no timeout. The
49
+ * Wait calls are long polls: their request's timeout bounds them, so the
50
+ * default skips them. A stream keeps the timeout it was given, so a default
51
+ * never ends a watch.
52
+ */
53
+ export function withDefaultUnaryTimeout(transport, timeoutMs) {
54
+ if (timeoutMs === undefined) {
55
+ return transport;
56
+ }
57
+ const unary = transport.unary.bind(transport);
58
+ return {
59
+ unary: (method, signal, timeout, header, input, contextValues) => unary(method, signal, timeout ?? (isWaitCall(method) ? undefined : timeoutMs), header, input, contextValues),
60
+ stream: transport.stream.bind(transport),
61
+ };
62
+ }
63
+ // WaitJob, WaitSandbox, WaitService and WaitOperation: the server holds the
64
+ // call for the timeout the request names, so a client default would cut it.
65
+ function isWaitCall(method) {
66
+ return method.name.startsWith("Wait");
67
+ }
@@ -0,0 +1,2 @@
1
+ /** The package version; test/node.test.ts keeps it equal to package.json. */
2
+ export declare const VERSION = "0.5.0";
@@ -0,0 +1,2 @@
1
+ /** The package version; test/node.test.ts keeps it equal to package.json. */
2
+ export const VERSION = "0.5.0";
package/dist/wait.js CHANGED
@@ -1,32 +1,35 @@
1
1
  import { abortReason } from "./retry.js";
2
+ import { runWithDeadline } from "./timeout.js";
2
3
  /** Asks again while the job is not in a state it waits for, and returns the answer when it is. */
3
4
  export function waitJob(service, req, opts) {
4
- return waitReached(() => service.waitJob(req, opts), opts);
5
+ return runWithDeadline(opts, (callOpts) => waitReached((o) => service.waitJob(req, o), callOpts));
5
6
  }
6
7
  /** Asks again while the sandbox is not in a state it waits for. */
7
8
  export function waitSandbox(service, req, opts) {
8
- return waitReached(() => service.waitSandbox(req, opts), opts);
9
+ return runWithDeadline(opts, (callOpts) => waitReached((o) => service.waitSandbox(req, o), callOpts));
9
10
  }
10
11
  /** Asks again while the service is not in a state it waits for. */
11
12
  export function waitService(service, req, opts) {
12
- return waitReached(() => service.waitService(req, opts), opts);
13
+ return runWithDeadline(opts, (callOpts) => waitReached((o) => service.waitService(req, o), callOpts));
13
14
  }
14
15
  /** Asks again while the operation is not done, and returns it when it is (wait.go:29-45). */
15
- export async function waitOperation(service, req, opts) {
16
- for (;;) {
17
- throwIfAborted(opts);
18
- const resp = await service.waitOperation(req, opts);
19
- const op = resp.operation;
20
- if (op !== undefined && op.done) {
21
- return op;
16
+ export function waitOperation(service, req, opts) {
17
+ return runWithDeadline(opts, async (callOpts) => {
18
+ for (;;) {
19
+ throwIfAborted(callOpts);
20
+ const resp = await service.waitOperation(req, callOpts);
21
+ const op = resp.operation;
22
+ if (op !== undefined && op.done) {
23
+ return op;
24
+ }
22
25
  }
23
- }
26
+ });
24
27
  }
25
28
  // The three waits share one loop: each answers reached when the state arrived.
26
29
  async function waitReached(call, opts) {
27
30
  for (;;) {
28
31
  throwIfAborted(opts);
29
- const resp = await call();
32
+ const resp = await call(opts);
30
33
  if (resp.reached) {
31
34
  return resp;
32
35
  }
package/dist/watch.js CHANGED
@@ -2,6 +2,7 @@ import { clone, create } from "@bufbuild/protobuf";
2
2
  import { ListJobsRequestSchema, ListSandboxesRequestSchema, ListServicesRequestSchema, WatchJobsRequestSchema, WatchSandboxesRequestSchema, WatchServicesRequestSchema, } from "./gen/toshi/workspace/v1/workloads_pb.js";
3
3
  import { ListEventsRequestSchema, WatchEventsRequestSchema, WatchEventsResponseSchema, } from "./gen/toshi/workspace/v1/workspace_pb.js";
4
4
  import { READER_TAKEN, WatchState, createReader, reconnect, streamEnded } from "./stream.js";
5
+ import { timeoutError } from "./timeout.js";
5
6
  /** Starts a job watch; with an empty cursor it lists the jobs first. */
6
7
  export function openJobWatch(service, retry, req, opts) {
7
8
  const request = clone(WatchJobsRequestSchema, req);
@@ -78,7 +79,7 @@ export function openServiceWatch(service, retry, req, opts) {
78
79
  }, retry, opts);
79
80
  }
80
81
  async function openWatch(wire, retry, opts) {
81
- const state = new WatchState(wire.request.cursor, opts?.signal);
82
+ const state = new WatchState(wire.request.cursor, opts?.signal, opts?.timeoutMs);
82
83
  let snapshot;
83
84
  if (wire.request.cursor === "") {
84
85
  try {
@@ -88,6 +89,9 @@ async function openWatch(wire, retry, opts) {
88
89
  }
89
90
  catch (err) {
90
91
  state.close();
92
+ if (state.timedOut) {
93
+ throw timeoutError(state.timeoutMs);
94
+ }
91
95
  throw err;
92
96
  }
93
97
  }
@@ -124,6 +128,7 @@ async function* runWatch(wire, retry, state) {
124
128
  try {
125
129
  wire.request.cursor = state.cursor;
126
130
  for await (const message of wire.watch(state.signal)) {
131
+ state.markOpen();
127
132
  attempt = 0;
128
133
  const keepalive = wire.keepalive(message);
129
134
  if (keepalive !== undefined) {
@@ -147,6 +152,9 @@ async function* runWatch(wire, retry, state) {
147
152
  if (state.closed) {
148
153
  return;
149
154
  }
155
+ if (state.timedOut) {
156
+ throw timeoutError(state.timeoutMs);
157
+ }
150
158
  cause = err;
151
159
  }
152
160
  attempt = await reconnect(retry, attempt, cause, state);
@@ -164,7 +172,7 @@ async function* runWatch(wire, retry, state) {
164
172
  * caller iterates. Every message it yields is the stream's response.
165
173
  */
166
174
  export function openEventWatch(service, retry, req, opts) {
167
- const state = new WatchState(req.cursor, opts?.signal);
175
+ const state = new WatchState(req.cursor, opts?.signal, opts?.timeoutMs);
168
176
  const request = clone(WatchEventsRequestSchema, req);
169
177
  request.keepalive = true;
170
178
  const list = create(ListEventsRequestSchema, {
@@ -215,6 +223,7 @@ async function* runEvents(service, retry, request, list, listingAtStart, state)
215
223
  state.cursor = event.cursor;
216
224
  resume = event.cursor;
217
225
  }
226
+ state.markOpen();
218
227
  yield create(WatchEventsResponseSchema, { event });
219
228
  continue;
220
229
  }
@@ -235,6 +244,7 @@ async function* runEvents(service, retry, request, list, listingAtStart, state)
235
244
  }
236
245
  request.cursor = resume;
237
246
  for await (const message of service.watchEvents(request, { signal: state.signal })) {
247
+ state.markOpen();
238
248
  attempt = 0;
239
249
  const keepalive = message.keepalive;
240
250
  if (keepalive !== undefined) {
@@ -263,6 +273,9 @@ async function* runEvents(service, retry, request, list, listingAtStart, state)
263
273
  if (state.closed) {
264
274
  return;
265
275
  }
276
+ if (state.timedOut) {
277
+ throw timeoutError(state.timeoutMs);
278
+ }
266
279
  cause = err;
267
280
  }
268
281
  attempt = await reconnect(retry, attempt, cause, state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toshicloud/sdk",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "TypeScript client for the Toshi Workspace API",
5
5
  "license": "Apache-2.0",
6
6
  "packageManager": "pnpm@12.15.0+sha512.b3951d6d6ff4a9f13dc7cf56cd28d2b8eb37dbcc9c68e52e1e1191873ea9f82a017287ab50baadc5ccc494f036c67c0e47eb13608c2fa36f47711b61ea128e61",
@@ -10,6 +10,14 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "default": "./dist/index.js"
12
12
  },
13
+ "./node": {
14
+ "types": "./dist/node/index.d.ts",
15
+ "default": "./dist/node/index.js"
16
+ },
17
+ "./test": {
18
+ "types": "./dist/test/index.d.ts",
19
+ "default": "./dist/test/index.js"
20
+ },
13
21
  "./package.json": "./package.json"
14
22
  },
15
23
  "types": "./dist/index.d.ts",
@@ -39,11 +47,19 @@
39
47
  "generate": "scripts/generate.sh",
40
48
  "prepack": "pnpm run build"
41
49
  },
42
- "dependencies": {
50
+ "peerDependencies": {
43
51
  "@bufbuild/protobuf": "^2.16.0",
44
- "@connectrpc/connect": "^2.2.0"
52
+ "@connectrpc/connect": "^2.2.0",
53
+ "@connectrpc/connect-node": "^2.2.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@connectrpc/connect-node": {
57
+ "optional": true
58
+ }
45
59
  },
46
60
  "devDependencies": {
61
+ "@bufbuild/protobuf": "^2.16.0",
62
+ "@connectrpc/connect": "^2.2.0",
47
63
  "@connectrpc/connect-node": "2.2.0",
48
64
  "@connectrpc/connect-web": "2.2.0",
49
65
  "@eslint/js": "10.0.1",