@swipt/cli 0.1.0-nightly.32 → 0.1.0-nightly.33

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/client.mjs ADDED
@@ -0,0 +1,312 @@
1
+ import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
2
+
3
+ import { LogService } from "./proto/logs_pb.js";
4
+ import { REPLService } from "./proto/repl_pb.js";
5
+ import { SwipletService } from "./proto/swiplets_pb.js";
6
+ import { SystemService } from "./proto/system_pb.js";
7
+
8
+ const largestMessage = 128 << 20;
9
+ const contentType = "application/grpc-web+proto";
10
+ const webSocketSubprotocol = "grpc-websockets";
11
+
12
+ export const Code = {
13
+ Canceled: 1,
14
+ Unknown: 2,
15
+ DeadlineExceeded: 4,
16
+ NotFound: 5,
17
+ Unimplemented: 12,
18
+ Internal: 13,
19
+ Unavailable: 14,
20
+ Unauthenticated: 16,
21
+ };
22
+
23
+ export class UsageError extends Error {}
24
+
25
+ export class RpcError extends Error {
26
+ constructor(code, message) {
27
+ super(message);
28
+ this.code = code;
29
+ }
30
+ }
31
+
32
+ const httpStatusCodes = { 400: Code.Internal, 401: Code.Unauthenticated, 403: Code.Unauthenticated, 404: Code.Unimplemented, 429: Code.Unavailable, 502: Code.Unavailable, 503: Code.Unavailable, 504: Code.Unavailable };
33
+
34
+ function statusMessage(text) {
35
+ if (text === undefined || text === null) {
36
+ return "";
37
+ }
38
+ try {
39
+ return decodeURIComponent(text);
40
+ } catch {
41
+ return text;
42
+ }
43
+ }
44
+
45
+ const transportError = error => {
46
+ if (error instanceof RpcError) {
47
+ return error;
48
+ }
49
+ if (error?.name === "AbortError") {
50
+ return new RpcError(Code.Canceled, "the call was canceled");
51
+ }
52
+ const cause = error?.cause;
53
+ return new RpcError(Code.Unavailable, cause?.message || cause?.code || error?.message || String(error));
54
+ };
55
+
56
+ function frame(schema, init) {
57
+ const body = toBinary(schema, create(schema, init));
58
+ const out = Buffer.allocUnsafe(5 + body.length);
59
+ out[0] = 0;
60
+ out.writeUInt32BE(body.length, 1);
61
+ out.set(body, 5);
62
+ return out;
63
+ }
64
+
65
+ function parseMetadata(body) {
66
+ const headers = {};
67
+ for (const line of Buffer.from(body).toString().split("\r\n")) {
68
+ const separator = line.indexOf(":");
69
+ if (separator > 0) {
70
+ headers[line.slice(0, separator).trim().toLowerCase()] = line.slice(separator + 1).trim();
71
+ }
72
+ }
73
+ return headers;
74
+ }
75
+
76
+ class FrameReader {
77
+ pending = Buffer.alloc(0);
78
+
79
+ constructor(schema) {
80
+ this.schema = schema;
81
+ }
82
+
83
+ push(chunk) {
84
+ this.pending = this.pending.length === 0 ? Buffer.from(chunk) : Buffer.concat([this.pending, Buffer.from(chunk)]);
85
+ const frames = [];
86
+ while (this.pending.length >= 5) {
87
+ const length = this.pending.readUInt32BE(1);
88
+ if (length > largestMessage) {
89
+ throw new RpcError(Code.Internal, `the server sent a message of ${length} bytes`);
90
+ }
91
+ if (this.pending.length < 5 + length) {
92
+ break;
93
+ }
94
+ const body = this.pending.subarray(5, 5 + length);
95
+ frames.push((this.pending[0] & 0x80) !== 0 ? { trailers: parseMetadata(body) } : { message: fromBinary(this.schema, body) });
96
+ this.pending = this.pending.subarray(5 + length);
97
+ }
98
+ return frames;
99
+ }
100
+ }
101
+
102
+ function checkStatus(headers) {
103
+ const status = headers["grpc-status"];
104
+ if (status === undefined || status === null) {
105
+ return false;
106
+ }
107
+ const code = Number(status);
108
+ if (code !== 0) {
109
+ throw new RpcError(code, statusMessage(headers["grpc-message"]));
110
+ }
111
+ return true;
112
+ }
113
+
114
+ function requestHeaders(connection, timeoutMs) {
115
+ const headers = { "content-type": contentType, "x-grpc-web": "1" };
116
+ if (connection.token !== "") {
117
+ headers.authorization = `Bearer ${connection.token}`;
118
+ }
119
+ if (timeoutMs > 0) {
120
+ headers["grpc-timeout"] = `${Math.ceil(timeoutMs)}m`;
121
+ }
122
+ return headers;
123
+ }
124
+
125
+ async function* invokeHTTP(connection, method, input, { signal, timeoutMs }) {
126
+ let response;
127
+ try {
128
+ response = await fetch(`${connection.base}/${method.parent.typeName}/${method.name}`, {
129
+ method: "POST",
130
+ headers: requestHeaders(connection, timeoutMs),
131
+ body: frame(method.input, input),
132
+ signal,
133
+ });
134
+ } catch (error) {
135
+ throw transportError(error);
136
+ }
137
+ const headers = Object.fromEntries(response.headers);
138
+ if (response.status !== 200) {
139
+ await response.body?.cancel();
140
+ throw new RpcError(httpStatusCodes[response.status] ?? Code.Unknown, `HTTP ${response.status}`);
141
+ }
142
+ if (checkStatus(headers)) {
143
+ await response.body?.cancel();
144
+ return;
145
+ }
146
+ const reader = new FrameReader(method.output);
147
+ let ended = false;
148
+ try {
149
+ for await (const chunk of response.body) {
150
+ for (const found of reader.push(chunk)) {
151
+ if (found.trailers) {
152
+ checkStatus(found.trailers);
153
+ ended = true;
154
+ } else if (!ended) {
155
+ yield found.message;
156
+ }
157
+ }
158
+ }
159
+ } catch (error) {
160
+ throw transportError(error);
161
+ }
162
+ if (!ended) {
163
+ throw new RpcError(Code.Unavailable, "the response ended without a status");
164
+ }
165
+ }
166
+
167
+ async function* invokeWebSocket(connection, method, input, { signal, timeoutMs }) {
168
+ const socket = new WebSocket(`${connection.webSocketBase}/${method.parent.typeName}/${method.name}`, [webSocketSubprotocol]);
169
+ socket.binaryType = "arraybuffer";
170
+ const reader = new FrameReader(method.output);
171
+ const messages = [];
172
+ let failure;
173
+ let closed = false;
174
+ let headersSeen = false;
175
+ let wake = () => {};
176
+ const notify = () => {
177
+ const current = wake;
178
+ wake = () => {};
179
+ current();
180
+ };
181
+ const fail = error => {
182
+ failure ??= error;
183
+ socket.close();
184
+ notify();
185
+ };
186
+ const abort = () => fail(new RpcError(Code.Canceled, "the call was canceled"));
187
+ const opened = new Promise((resolve, reject) => {
188
+ socket.addEventListener("open", resolve, { once: true });
189
+ socket.addEventListener("error", () => reject(new RpcError(Code.Unavailable, `cannot connect to ${connection.webSocketBase}`)), { once: true });
190
+ });
191
+ socket.addEventListener("message", event => {
192
+ try {
193
+ for (const found of reader.push(new Uint8Array(event.data))) {
194
+ if (found.message) {
195
+ messages.push(found.message);
196
+ } else if (headersSeen || "grpc-status" in found.trailers) {
197
+ checkStatus(found.trailers);
198
+ closed = true;
199
+ socket.close();
200
+ } else {
201
+ headersSeen = true;
202
+ }
203
+ }
204
+ } catch (error) {
205
+ fail(error);
206
+ }
207
+ notify();
208
+ });
209
+ socket.addEventListener("close", event => {
210
+ if (!closed) {
211
+ failure ??= new RpcError(Code.Unavailable, event.reason || "the connection closed before the response");
212
+ }
213
+ closed = true;
214
+ notify();
215
+ });
216
+ socket.addEventListener("error", () => {
217
+ failure ??= new RpcError(Code.Unavailable, `cannot connect to ${connection.webSocketBase}`);
218
+ });
219
+ signal?.addEventListener("abort", abort, { once: true });
220
+ try {
221
+ await opened;
222
+ const headers = requestHeaders(connection, timeoutMs);
223
+ socket.send(Buffer.from(`${Object.entries(headers).map(([key, value]) => `${key}: ${value}\r\n`).join("")}`));
224
+ const sender = (async () => {
225
+ try {
226
+ const body = method.methodKind === "client_streaming" ? input : [input];
227
+ for await (const message of body) {
228
+ if (closed) {
229
+ return;
230
+ }
231
+ socket.send(Buffer.concat([Buffer.from([0]), frame(method.input, message)]));
232
+ }
233
+ if (!closed) {
234
+ socket.send(Buffer.from([1]));
235
+ }
236
+ } catch (error) {
237
+ fail(transportError(error));
238
+ }
239
+ })();
240
+ while (true) {
241
+ if (messages.length > 0) {
242
+ yield messages.shift();
243
+ continue;
244
+ }
245
+ if (closed) {
246
+ break;
247
+ }
248
+ await new Promise(resolve => {
249
+ wake = resolve;
250
+ });
251
+ }
252
+ await sender;
253
+ if (failure) {
254
+ throw failure;
255
+ }
256
+ } finally {
257
+ signal?.removeEventListener("abort", abort);
258
+ if (socket.readyState !== WebSocket.CLOSED) {
259
+ socket.close();
260
+ }
261
+ }
262
+ }
263
+
264
+ async function single(iterable) {
265
+ let found;
266
+ for await (const message of iterable) {
267
+ found ??= message;
268
+ }
269
+ if (found === undefined) {
270
+ throw new RpcError(Code.Internal, "the server sent no response");
271
+ }
272
+ return found;
273
+ }
274
+
275
+ function bind(service, connection) {
276
+ const client = {};
277
+ for (const method of service.methods) {
278
+ const streamsInput = method.methodKind === "client_streaming" || method.methodKind === "bidi_streaming";
279
+ client[method.localName] = (input, options = {}) => {
280
+ const call = (streamsInput ? invokeWebSocket : invokeHTTP)(connection, method, input, { signal: options.signal, timeoutMs: options.timeoutMs ?? 0 });
281
+ return method.methodKind === "unary" || method.methodKind === "client_streaming" ? single(call) : call;
282
+ };
283
+ }
284
+ return client;
285
+ }
286
+
287
+ export function open({ host, token }) {
288
+ host ||= process.env.SWIPT_HOST ?? "";
289
+ token ||= process.env.SWIPT_TOKEN ?? "";
290
+ if (host === "") {
291
+ throw new UsageError('give --host or set SWIPT_HOST; "swipt devices" lists the hosts on the local network');
292
+ }
293
+ let parsed;
294
+ try {
295
+ parsed = new URL(host.includes("://") ? host : `http://${host}`);
296
+ } catch {
297
+ throw new UsageError(`--host is not a URL: ${host}`);
298
+ }
299
+ if (parsed.hostname === "" || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
300
+ throw new UsageError(`--host is an http or https URL: ${host}`);
301
+ }
302
+ const base = `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
303
+ const connection = { token, base, webSocketBase: base.replace(/^http/, "ws") };
304
+ return {
305
+ system: bind(SystemService, connection),
306
+ logs: bind(LogService, connection),
307
+ swiplets: bind(SwipletService, connection),
308
+ repl: bind(REPLService, connection),
309
+ };
310
+ }
311
+
312
+ export const unauthenticated = error => error instanceof RpcError && error.code === Code.Unauthenticated;