@toshicloud/sdk 0.0.0-stage → 0.3.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 (45) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +250 -2
  3. package/dist/client.d.ts +153 -0
  4. package/dist/client.js +92 -0
  5. package/dist/errors.d.ts +28 -0
  6. package/dist/errors.js +77 -0
  7. package/dist/execute.d.ts +7 -0
  8. package/dist/execute.js +380 -0
  9. package/dist/gen/toshi/workspace/v1/artifacts_pb.d.ts +843 -0
  10. package/dist/gen/toshi/workspace/v1/artifacts_pb.js +96 -0
  11. package/dist/gen/toshi/workspace/v1/channels_pb.d.ts +2713 -0
  12. package/dist/gen/toshi/workspace/v1/channels_pb.js +336 -0
  13. package/dist/gen/toshi/workspace/v1/display_pb.d.ts +1166 -0
  14. package/dist/gen/toshi/workspace/v1/display_pb.js +214 -0
  15. package/dist/gen/toshi/workspace/v1/projects_pb.d.ts +1540 -0
  16. package/dist/gen/toshi/workspace/v1/projects_pb.js +207 -0
  17. package/dist/gen/toshi/workspace/v1/recordings_pb.d.ts +1024 -0
  18. package/dist/gen/toshi/workspace/v1/recordings_pb.js +201 -0
  19. package/dist/gen/toshi/workspace/v1/secrets_pb.d.ts +1214 -0
  20. package/dist/gen/toshi/workspace/v1/secrets_pb.js +182 -0
  21. package/dist/gen/toshi/workspace/v1/volumes_pb.d.ts +444 -0
  22. package/dist/gen/toshi/workspace/v1/volumes_pb.js +107 -0
  23. package/dist/gen/toshi/workspace/v1/webhook_pb.d.ts +1018 -0
  24. package/dist/gen/toshi/workspace/v1/webhook_pb.js +153 -0
  25. package/dist/gen/toshi/workspace/v1/workloads_pb.d.ts +2858 -0
  26. package/dist/gen/toshi/workspace/v1/workloads_pb.js +261 -0
  27. package/dist/gen/toshi/workspace/v1/workspace_pb.d.ts +5380 -0
  28. package/dist/gen/toshi/workspace/v1/workspace_pb.js +855 -0
  29. package/dist/index.d.ts +15 -0
  30. package/dist/index.js +14 -0
  31. package/dist/interceptors.d.ts +10 -0
  32. package/dist/interceptors.js +159 -0
  33. package/dist/output.d.ts +7 -0
  34. package/dist/output.js +77 -0
  35. package/dist/retry.d.ts +42 -0
  36. package/dist/retry.js +254 -0
  37. package/dist/run.d.ts +10 -0
  38. package/dist/run.js +175 -0
  39. package/dist/stream.d.ts +21 -0
  40. package/dist/stream.js +84 -0
  41. package/dist/wait.d.ts +13 -0
  42. package/dist/wait.js +41 -0
  43. package/dist/watch.d.ts +16 -0
  44. package/dist/watch.js +277 -0
  45. package/package.json +47 -4
package/dist/run.js ADDED
@@ -0,0 +1,175 @@
1
+ import { create } from "@bufbuild/protobuf";
2
+ import { JobExitError } from "./errors.js";
3
+ import { OutputEndSchema, WatchOutputRequestSchema } from "./gen/toshi/workspace/v1/workspace_pb.js";
4
+ import { EndReason } from "./gen/toshi/workspace/v1/workspace_pb.js";
5
+ import { openOutputWatch } from "./output.js";
6
+ /**
7
+ * Starts a job and follows its output, as `toshi run` does: CreateJob, then
8
+ * WatchOutput from the first byte. A job whose create answered its output
9
+ * (wait_for_end was set) is returned finished.
10
+ */
11
+ export async function runJob(service, retry, req, opts) {
12
+ const created = await service.createJob(req, opts);
13
+ const job = created.job;
14
+ if (job === undefined) {
15
+ throw new Error("toshi: CreateJob answered no job");
16
+ }
17
+ if (created.output !== undefined) {
18
+ return new FinishedRun(job, created.output);
19
+ }
20
+ const watch = await openOutputWatch(service, retry, create(WatchOutputRequestSchema, {
21
+ project: req.project,
22
+ environment: req.environment,
23
+ id: job.id,
24
+ }), opts);
25
+ return new LiveRun(job, watch);
26
+ }
27
+ /** A run whose create answered the job's output instead of a live stream. */
28
+ class FinishedRun {
29
+ job;
30
+ output;
31
+ queue;
32
+ constructor(job, output) {
33
+ this.job = job;
34
+ this.output = output;
35
+ }
36
+ [Symbol.asyncIterator]() {
37
+ this.queue ??= chunksOf(this.output);
38
+ const queue = this.queue;
39
+ return {
40
+ next: () => {
41
+ const chunk = queue.shift();
42
+ return Promise.resolve(chunk === undefined ? { done: true, value: undefined } : { done: false, value: chunk });
43
+ },
44
+ };
45
+ }
46
+ wait() {
47
+ const end = create(OutputEndSchema, {
48
+ exitCode: this.job.status?.exitCode,
49
+ cancelled: this.job.status?.endReason === EndReason.CANCELLED,
50
+ truncated: this.output.truncated,
51
+ });
52
+ if (end.exitCode === 0) {
53
+ return Promise.resolve();
54
+ }
55
+ return Promise.reject(new JobExitError(this.job.id, end));
56
+ }
57
+ }
58
+ /** A run whose output the server streams while the job runs. */
59
+ class LiveRun {
60
+ job;
61
+ watch;
62
+ responses;
63
+ queue = [];
64
+ end;
65
+ failure;
66
+ readerOwned = false;
67
+ waitPromise;
68
+ constructor(job, watch) {
69
+ this.job = job;
70
+ this.watch = watch;
71
+ this.responses = watch[Symbol.asyncIterator]();
72
+ }
73
+ [Symbol.asyncIterator]() {
74
+ if (this.readerOwned) {
75
+ throw new Error("toshi: this run already has an output reader");
76
+ }
77
+ this.readerOwned = true;
78
+ return {
79
+ next: () => this.pull(),
80
+ // A break ends only this reader: the job keeps running.
81
+ return: () => {
82
+ this.watch.close();
83
+ return Promise.resolve({ done: true, value: undefined });
84
+ },
85
+ };
86
+ }
87
+ wait() {
88
+ this.waitPromise ??= this.awaitOutcome();
89
+ return this.waitPromise;
90
+ }
91
+ async awaitOutcome() {
92
+ if (!this.readerOwned) {
93
+ // No reader owns the output: take it and drain it, so wait() never stalls.
94
+ this.readerOwned = true;
95
+ try {
96
+ for (;;) {
97
+ const result = await this.pull();
98
+ if (result.done === true) {
99
+ break;
100
+ }
101
+ }
102
+ }
103
+ catch {
104
+ // pull() recorded the outcome; it is reported below.
105
+ }
106
+ }
107
+ for (;;) {
108
+ if (this.failure !== undefined) {
109
+ throw this.failure;
110
+ }
111
+ if (this.end !== undefined) {
112
+ if (this.end.exitCode === 0 && !this.end.cancelled) {
113
+ return;
114
+ }
115
+ throw new JobExitError(this.job.id, this.end);
116
+ }
117
+ await this.readOne();
118
+ }
119
+ }
120
+ async pull() {
121
+ for (;;) {
122
+ const chunk = this.queue.shift();
123
+ if (chunk !== undefined) {
124
+ return { done: false, value: chunk };
125
+ }
126
+ if (this.failure !== undefined) {
127
+ throw this.failure;
128
+ }
129
+ if (this.end !== undefined) {
130
+ return { done: true, value: undefined };
131
+ }
132
+ await this.readOne();
133
+ }
134
+ }
135
+ async readOne() {
136
+ let result;
137
+ try {
138
+ result = await this.responses.next();
139
+ }
140
+ catch (err) {
141
+ this.failure = err instanceof Error ? err : new Error(String(err));
142
+ return;
143
+ }
144
+ if (result.done === true) {
145
+ this.failure = new Error("toshi: the output stream ended without its end message");
146
+ return;
147
+ }
148
+ const message = result.value.message;
149
+ switch (message.case) {
150
+ case "stdout":
151
+ this.queue.push({ stream: "stdout", data: message.value });
152
+ return;
153
+ case "stderr":
154
+ this.queue.push({ stream: "stderr", data: message.value });
155
+ return;
156
+ case "end":
157
+ this.end = message.value;
158
+ // The watch has nothing left to send; release the RPC.
159
+ this.watch.close();
160
+ return;
161
+ default:
162
+ return;
163
+ }
164
+ }
165
+ }
166
+ function chunksOf(output) {
167
+ const chunks = [];
168
+ if (output.stdout.length > 0) {
169
+ chunks.push({ stream: "stdout", data: output.stdout });
170
+ }
171
+ if (output.stderr.length > 0) {
172
+ chunks.push({ stream: "stderr", data: output.stderr });
173
+ }
174
+ return chunks;
175
+ }
@@ -0,0 +1,21 @@
1
+ import type { RetrySetting } from "./retry.js";
2
+ /** The one-reader guard every watch shares. */
3
+ export declare const READER_TAKEN = "toshi: this watch already has a reader";
4
+ export declare const streamEnded: Error;
5
+ interface AsyncGeneratorFactory<T> {
6
+ (): AsyncGenerator<T, void, undefined>;
7
+ }
8
+ export declare function createReader<T>(start: AsyncGeneratorFactory<T>, close: () => void): AsyncIterableIterator<T>;
9
+ export declare function reconnect(retry: RetrySetting, attempt: number, cause: unknown, state: WatchState): Promise<number>;
10
+ export declare class WatchState {
11
+ /** The caller-visible resume point; the output watch passes "" and ignores it. */
12
+ cursor: string;
13
+ closed: boolean;
14
+ readonly signal: AbortSignal;
15
+ private readonly controller;
16
+ private readonly caller;
17
+ private readonly onCallerAbort;
18
+ constructor(initialCursor: string, caller: AbortSignal | undefined);
19
+ close(): void;
20
+ }
21
+ export {};
package/dist/stream.js ADDED
@@ -0,0 +1,84 @@
1
+ import { abortReason, backoffDelay, isStreamLoss, sleep } from "./retry.js";
2
+ /** The one-reader guard every watch shares. */
3
+ export const READER_TAKEN = "toshi: this watch already has a reader";
4
+ // watch.go:473-476, output.go:120-127: a stream that ends without its end
5
+ // message is a loss, so it is retried like one and reported with Go's wording
6
+ // once no attempt remains.
7
+ export const streamEnded = new Error("toshi: stream ended without an error");
8
+ // connect's generated client exposes only next() (promise-client.ts:201-215), so
9
+ // the watch owns the release: close() aborts the controller, and the reader's
10
+ // return() does the same before ending the iteration.
11
+ export function createReader(start, close) {
12
+ const generator = start();
13
+ return {
14
+ // Every end, an error included, releases the watch: a rejected next() is
15
+ // not followed by return(), so the caller's abort listener goes here.
16
+ next() {
17
+ return generator.next().then((result) => {
18
+ if (result.done === true) {
19
+ close();
20
+ }
21
+ return result;
22
+ }, (err) => {
23
+ close();
24
+ throw err;
25
+ });
26
+ },
27
+ async return() {
28
+ close();
29
+ await generator.return(undefined);
30
+ return { done: true, value: undefined };
31
+ },
32
+ [Symbol.asyncIterator]() {
33
+ return this;
34
+ },
35
+ };
36
+ }
37
+ // watch.go:104-118, output.go:74-86: maxAttempts - 1 reconnects in a row, the
38
+ // same jittered backoff, and the count reset by every message received. Anything
39
+ // that is not a lost stream ends the iteration with that error.
40
+ export async function reconnect(retry, attempt, cause, state) {
41
+ const lost = cause === streamEnded || isStreamLoss(cause);
42
+ if (retry === false || !lost || attempt >= retry.maxAttempts - 1) {
43
+ throw cause;
44
+ }
45
+ await sleep(backoffDelay(retry, attempt), state.signal);
46
+ return attempt + 1;
47
+ }
48
+ // Owns the AbortController that cancels the open RPC, a pending next() and any
49
+ // 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
+ export class WatchState {
52
+ /** The caller-visible resume point; the output watch passes "" and ignores it. */
53
+ cursor;
54
+ closed = false;
55
+ signal;
56
+ controller = new AbortController();
57
+ caller;
58
+ onCallerAbort = () => {
59
+ if (this.caller !== undefined) {
60
+ this.controller.abort(abortReason(this.caller));
61
+ }
62
+ };
63
+ constructor(initialCursor, caller) {
64
+ this.cursor = initialCursor;
65
+ this.caller = caller;
66
+ this.signal = this.controller.signal;
67
+ if (caller !== undefined) {
68
+ if (caller.aborted) {
69
+ this.onCallerAbort();
70
+ }
71
+ else {
72
+ caller.addEventListener("abort", this.onCallerAbort, { once: true });
73
+ }
74
+ }
75
+ }
76
+ close() {
77
+ if (this.closed) {
78
+ return;
79
+ }
80
+ this.closed = true;
81
+ this.caller?.removeEventListener("abort", this.onCallerAbort);
82
+ this.controller.abort();
83
+ }
84
+ }
package/dist/wait.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Client } from "@connectrpc/connect";
2
+ import type { CallOptions } from "./client.js";
3
+ import type { WorkloadService } from "./gen/toshi/workspace/v1/workloads_pb.js";
4
+ import type { WaitJobRequest, WaitJobResponse, WaitSandboxRequest, WaitSandboxResponse, WaitServiceRequest, WaitServiceResponse } from "./gen/toshi/workspace/v1/workloads_pb.js";
5
+ import type { Operation, WaitOperationRequest } from "./gen/toshi/workspace/v1/workspace_pb.js";
6
+ /** Asks again while the job is not in a state it waits for, and returns the answer when it is. */
7
+ export declare function waitJob(service: Client<typeof WorkloadService>, req: WaitJobRequest, opts?: CallOptions): Promise<WaitJobResponse>;
8
+ /** Asks again while the sandbox is not in a state it waits for. */
9
+ export declare function waitSandbox(service: Client<typeof WorkloadService>, req: WaitSandboxRequest, opts?: CallOptions): Promise<WaitSandboxResponse>;
10
+ /** Asks again while the service is not in a state it waits for. */
11
+ export declare function waitService(service: Client<typeof WorkloadService>, req: WaitServiceRequest, opts?: CallOptions): Promise<WaitServiceResponse>;
12
+ /** Asks again while the operation is not done, and returns it when it is (wait.go:29-45). */
13
+ export declare function waitOperation(service: Client<typeof WorkloadService>, req: WaitOperationRequest, opts?: CallOptions): Promise<Operation>;
package/dist/wait.js ADDED
@@ -0,0 +1,41 @@
1
+ import { abortReason } from "./retry.js";
2
+ /** Asks again while the job is not in a state it waits for, and returns the answer when it is. */
3
+ export function waitJob(service, req, opts) {
4
+ return waitReached(() => service.waitJob(req, opts), opts);
5
+ }
6
+ /** Asks again while the sandbox is not in a state it waits for. */
7
+ export function waitSandbox(service, req, opts) {
8
+ return waitReached(() => service.waitSandbox(req, opts), opts);
9
+ }
10
+ /** Asks again while the service is not in a state it waits for. */
11
+ export function waitService(service, req, opts) {
12
+ return waitReached(() => service.waitService(req, opts), opts);
13
+ }
14
+ /** 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;
22
+ }
23
+ }
24
+ }
25
+ // The three waits share one loop: each answers reached when the state arrived.
26
+ async function waitReached(call, opts) {
27
+ for (;;) {
28
+ throwIfAborted(opts);
29
+ const resp = await call();
30
+ if (resp.reached) {
31
+ return resp;
32
+ }
33
+ }
34
+ }
35
+ // wait.go:16-18: the caller's context ends the loop with its own error.
36
+ function throwIfAborted(opts) {
37
+ const signal = opts?.signal;
38
+ if (signal?.aborted === true) {
39
+ throw abortReason(signal);
40
+ }
41
+ }
@@ -0,0 +1,16 @@
1
+ import type { Client } from "@connectrpc/connect";
2
+ import type { CallOptions, EventWatch, JobWatch, SandboxWatch, ServiceWatch } from "./client.js";
3
+ import type { WatchJobsRequest, WatchSandboxesRequest, WatchServicesRequest, WorkloadService } from "./gen/toshi/workspace/v1/workloads_pb.js";
4
+ import type { WatchEventsRequest } from "./gen/toshi/workspace/v1/workspace_pb.js";
5
+ import type { RetrySetting } from "./retry.js";
6
+ /** Starts a job watch; with an empty cursor it lists the jobs first. */
7
+ export declare function openJobWatch(service: Client<typeof WorkloadService>, retry: RetrySetting, req: WatchJobsRequest, opts?: CallOptions): Promise<JobWatch>;
8
+ /** Starts a sandbox watch; with an empty cursor it lists the sandboxes first. */
9
+ export declare function openSandboxWatch(service: Client<typeof WorkloadService>, retry: RetrySetting, req: WatchSandboxesRequest, opts?: CallOptions): Promise<SandboxWatch>;
10
+ /** Starts a service watch; with an empty cursor it lists the services first. */
11
+ export declare function openServiceWatch(service: Client<typeof WorkloadService>, retry: RetrySetting, req: WatchServicesRequest, opts?: CallOptions): Promise<ServiceWatch>;
12
+ /**
13
+ * Starts an event watch; with an empty cursor it pages the event log as the
14
+ * caller iterates. Every message it yields is the stream's response.
15
+ */
16
+ export declare function openEventWatch(service: Client<typeof WorkloadService>, retry: RetrySetting, req: WatchEventsRequest, opts?: CallOptions): Promise<EventWatch>;
package/dist/watch.js ADDED
@@ -0,0 +1,277 @@
1
+ import { clone, create } from "@bufbuild/protobuf";
2
+ import { ListJobsRequestSchema, ListSandboxesRequestSchema, ListServicesRequestSchema, WatchJobsRequestSchema, WatchSandboxesRequestSchema, WatchServicesRequestSchema, } from "./gen/toshi/workspace/v1/workloads_pb.js";
3
+ import { ListEventsRequestSchema, WatchEventsRequestSchema, WatchEventsResponseSchema, } from "./gen/toshi/workspace/v1/workspace_pb.js";
4
+ import { READER_TAKEN, WatchState, createReader, reconnect, streamEnded } from "./stream.js";
5
+ /** Starts a job watch; with an empty cursor it lists the jobs first. */
6
+ export function openJobWatch(service, retry, req, opts) {
7
+ const request = clone(WatchJobsRequestSchema, req);
8
+ return openWatch({
9
+ request,
10
+ list: async (signal) => {
11
+ const list = create(ListJobsRequestSchema, {
12
+ project: request.project,
13
+ environment: request.environment,
14
+ });
15
+ const items = [];
16
+ for (;;) {
17
+ const page = await service.listJobs(list, { signal });
18
+ items.push(...page.jobs);
19
+ if (page.nextPageToken === "") {
20
+ return { items, cursor: page.cursor };
21
+ }
22
+ list.pageToken = page.nextPageToken;
23
+ }
24
+ },
25
+ watch: (signal) => service.watchJobs(request, { signal }),
26
+ cursor: (message) => message.cursor,
27
+ keepalive: (message) => message.keepalive,
28
+ }, retry, opts);
29
+ }
30
+ /** Starts a sandbox watch; with an empty cursor it lists the sandboxes first. */
31
+ export function openSandboxWatch(service, retry, req, opts) {
32
+ const request = clone(WatchSandboxesRequestSchema, req);
33
+ return openWatch({
34
+ request,
35
+ list: async (signal) => {
36
+ const list = create(ListSandboxesRequestSchema, {
37
+ project: request.project,
38
+ environment: request.environment,
39
+ });
40
+ const items = [];
41
+ for (;;) {
42
+ const page = await service.listSandboxes(list, { signal });
43
+ items.push(...page.sandboxes);
44
+ if (page.nextPageToken === "") {
45
+ return { items, cursor: page.cursor };
46
+ }
47
+ list.pageToken = page.nextPageToken;
48
+ }
49
+ },
50
+ watch: (signal) => service.watchSandboxes(request, { signal }),
51
+ cursor: (message) => message.cursor,
52
+ keepalive: (message) => message.keepalive,
53
+ }, retry, opts);
54
+ }
55
+ /** Starts a service watch; with an empty cursor it lists the services first. */
56
+ export function openServiceWatch(service, retry, req, opts) {
57
+ const request = clone(WatchServicesRequestSchema, req);
58
+ return openWatch({
59
+ request,
60
+ list: async (signal) => {
61
+ const list = create(ListServicesRequestSchema, {
62
+ project: request.project,
63
+ environment: request.environment,
64
+ });
65
+ const items = [];
66
+ for (;;) {
67
+ const page = await service.listServices(list, { signal });
68
+ items.push(...page.services);
69
+ if (page.nextPageToken === "") {
70
+ return { items, cursor: page.cursor };
71
+ }
72
+ list.pageToken = page.nextPageToken;
73
+ }
74
+ },
75
+ watch: (signal) => service.watchServices(request, { signal }),
76
+ cursor: (message) => message.cursor,
77
+ keepalive: (message) => message.keepalive,
78
+ }, retry, opts);
79
+ }
80
+ async function openWatch(wire, retry, opts) {
81
+ const state = new WatchState(wire.request.cursor, opts?.signal);
82
+ let snapshot;
83
+ if (wire.request.cursor === "") {
84
+ try {
85
+ const listed = await wire.list(state.signal);
86
+ snapshot = listed.items;
87
+ state.cursor = listed.cursor;
88
+ }
89
+ catch (err) {
90
+ state.close();
91
+ throw err;
92
+ }
93
+ }
94
+ wire.request.cursor = state.cursor;
95
+ wire.request.keepalive = true;
96
+ let reader;
97
+ return {
98
+ get cursor() {
99
+ return state.cursor;
100
+ },
101
+ get snapshot() {
102
+ return snapshot;
103
+ },
104
+ close() {
105
+ state.close();
106
+ },
107
+ [Symbol.asyncIterator]() {
108
+ if (reader !== undefined) {
109
+ throw new Error(READER_TAKEN);
110
+ }
111
+ reader = createReader(() => runWatch(wire, retry, state), () => state.close());
112
+ return reader;
113
+ },
114
+ };
115
+ }
116
+ async function* runWatch(wire, retry, state) {
117
+ let attempt = 0;
118
+ try {
119
+ for (;;) {
120
+ if (state.closed) {
121
+ return;
122
+ }
123
+ let cause;
124
+ try {
125
+ wire.request.cursor = state.cursor;
126
+ for await (const message of wire.watch(state.signal)) {
127
+ attempt = 0;
128
+ const keepalive = wire.keepalive(message);
129
+ if (keepalive !== undefined) {
130
+ if (keepalive.cursor !== "") {
131
+ state.cursor = keepalive.cursor;
132
+ }
133
+ continue;
134
+ }
135
+ const cursor = wire.cursor(message);
136
+ if (cursor !== "") {
137
+ state.cursor = cursor;
138
+ }
139
+ yield message;
140
+ }
141
+ if (state.closed) {
142
+ return;
143
+ }
144
+ cause = streamEnded;
145
+ }
146
+ catch (err) {
147
+ if (state.closed) {
148
+ return;
149
+ }
150
+ cause = err;
151
+ }
152
+ attempt = await reconnect(retry, attempt, cause, state);
153
+ }
154
+ }
155
+ catch (err) {
156
+ if (state.closed) {
157
+ return;
158
+ }
159
+ throw err;
160
+ }
161
+ }
162
+ /**
163
+ * Starts an event watch; with an empty cursor it pages the event log as the
164
+ * caller iterates. Every message it yields is the stream's response.
165
+ */
166
+ export function openEventWatch(service, retry, req, opts) {
167
+ const state = new WatchState(req.cursor, opts?.signal);
168
+ const request = clone(WatchEventsRequestSchema, req);
169
+ request.keepalive = true;
170
+ const list = create(ListEventsRequestSchema, {
171
+ project: req.project,
172
+ environment: req.environment,
173
+ workloadId: req.workloadId,
174
+ });
175
+ const listing = req.cursor === "";
176
+ let reader;
177
+ const watch = {
178
+ get cursor() {
179
+ return state.cursor;
180
+ },
181
+ close() {
182
+ state.close();
183
+ },
184
+ [Symbol.asyncIterator]() {
185
+ if (reader !== undefined) {
186
+ throw new Error(READER_TAKEN);
187
+ }
188
+ reader = createReader(() => runEvents(service, retry, request, list, listing, state), () => state.close());
189
+ return reader;
190
+ },
191
+ };
192
+ return Promise.resolve(watch);
193
+ }
194
+ async function* runEvents(service, retry, request, list, listingAtStart, state) {
195
+ let attempt = 0;
196
+ let page = [];
197
+ let pageIndex = 0;
198
+ let listing = listingAtStart;
199
+ // Resume is the position the next stream opens at, separate from the cursor
200
+ // the caller sees, which only moves on events handed out and keepalives read.
201
+ let resume = state.cursor;
202
+ try {
203
+ for (;;) {
204
+ if (state.closed) {
205
+ return;
206
+ }
207
+ let cause;
208
+ try {
209
+ if (listing) {
210
+ if (pageIndex < page.length) {
211
+ const event = page[pageIndex];
212
+ pageIndex += 1;
213
+ attempt = 0;
214
+ if (event.cursor !== "") {
215
+ state.cursor = event.cursor;
216
+ resume = event.cursor;
217
+ }
218
+ yield create(WatchEventsResponseSchema, { event });
219
+ continue;
220
+ }
221
+ const listed = await service.listEvents(list, { signal: state.signal });
222
+ page = listed.events;
223
+ pageIndex = 0;
224
+ if (listed.cursor !== "") {
225
+ list.pageToken = listed.cursor;
226
+ }
227
+ if (page.length > 0) {
228
+ continue;
229
+ }
230
+ if (listed.cursor !== "") {
231
+ resume = listed.cursor;
232
+ }
233
+ listing = false;
234
+ continue;
235
+ }
236
+ request.cursor = resume;
237
+ for await (const message of service.watchEvents(request, { signal: state.signal })) {
238
+ attempt = 0;
239
+ const keepalive = message.keepalive;
240
+ if (keepalive !== undefined) {
241
+ if (keepalive.cursor !== "") {
242
+ state.cursor = keepalive.cursor;
243
+ resume = keepalive.cursor;
244
+ }
245
+ continue;
246
+ }
247
+ const event = message.event;
248
+ if (event === undefined) {
249
+ continue;
250
+ }
251
+ if (event.cursor !== "") {
252
+ state.cursor = event.cursor;
253
+ resume = event.cursor;
254
+ }
255
+ yield message;
256
+ }
257
+ if (state.closed) {
258
+ return;
259
+ }
260
+ cause = streamEnded;
261
+ }
262
+ catch (err) {
263
+ if (state.closed) {
264
+ return;
265
+ }
266
+ cause = err;
267
+ }
268
+ attempt = await reconnect(retry, attempt, cause, state);
269
+ }
270
+ }
271
+ catch (err) {
272
+ if (state.closed) {
273
+ return;
274
+ }
275
+ throw err;
276
+ }
277
+ }
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@toshicloud/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "TypeScript client for the Toshi Workspace API",
5
+ "license": "Apache-2.0",
6
+ "packageManager": "pnpm@12.15.0+sha512.b3951d6d6ff4a9f13dc7cf56cd28d2b8eb37dbcc9c68e52e1e1191873ea9f82a017287ab50baadc5ccc494f036c67c0e47eb13608c2fa36f47711b61ea128e61",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "engines": {
21
+ "node": ">=24.22 <25"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/toshicloud/toshi-ts.git"
26
+ },
27
+ "scripts": {
28
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "eslint .",
31
+ "test": "vitest run --coverage",
32
+ "generate": "scripts/generate.sh",
33
+ "prepack": "pnpm run build"
34
+ },
35
+ "dependencies": {
36
+ "@bufbuild/protobuf": "^2.16.0",
37
+ "@connectrpc/connect": "^2.2.0"
38
+ },
39
+ "devDependencies": {
40
+ "@connectrpc/connect-node": "2.2.0",
41
+ "@eslint/js": "10.0.1",
42
+ "@types/node": "26.6.5",
43
+ "@vitest/coverage-v8": "5.0.5",
44
+ "eslint": "10.13.0",
45
+ "typescript": "6.0.3",
46
+ "typescript-eslint": "8.72.0",
47
+ "vitest": "5.0.5"
48
+ }
49
+ }