@simantic/cloud-sim 0.1.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.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * This file was auto-generated by openapi-typescript.
3
+ * Do not make direct changes to the file.
4
+ */
5
+ export {};
package/dist/http.d.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { CloudSimError } from './errors.js';
2
+ export declare const DEFAULT_BASE_URL = "https://cloud.simantic.com";
3
+ export type ApiKey = string | (() => string | Promise<string>);
4
+ /** A retry about to be made, as `onRetry` hears of it. */
5
+ export interface RetryInfo {
6
+ /** Which retry this is: 1 for the first. */
7
+ attempt: number;
8
+ /** The failure being retried. */
9
+ error: CloudSimError;
10
+ /** How long until the retry is sent. */
11
+ delayMs: number;
12
+ }
13
+ export interface TransportOptions {
14
+ apiKey: ApiKey;
15
+ version?: string;
16
+ baseUrl?: string;
17
+ fetch?: typeof fetch;
18
+ onUnauthorized?: () => Promise<void> | void;
19
+ maxRetries?: number;
20
+ timeoutMs?: number;
21
+ onRetry?: (info: RetryInfo) => void;
22
+ }
23
+ export type Query = Record<string, string | number | boolean | undefined>;
24
+ export interface Call {
25
+ method: 'GET' | 'POST';
26
+ path: string;
27
+ query?: Query;
28
+ /** A JSON body. */
29
+ json?: unknown;
30
+ /** A multipart body. fetch sets its Content-Type, boundary included. */
31
+ form?: FormData;
32
+ headers?: Record<string, string>;
33
+ /** `Cloud-Sim-Version` for this call, in place of the client's. */
34
+ version?: string | undefined;
35
+ signal?: AbortSignal | undefined;
36
+ /** The time limit on the answer's headers for this call, in place of the client's. */
37
+ timeoutMs?: number | undefined;
38
+ /** Told of each retry of this call, after the client's `onRetry`. */
39
+ onRetry?: ((info: RetryInfo) => void) | undefined;
40
+ /**
41
+ * Whether a transient failure may be sent again: true for reads, for a
42
+ * create that carries an Idempotency-Key, which the server answers with
43
+ * the run it already made, and for a cancel, which it answers with the
44
+ * run as it is.
45
+ */
46
+ retry: boolean;
47
+ }
48
+ export declare class Transport {
49
+ readonly version: string | undefined;
50
+ readonly baseUrl: string;
51
+ /** How many times a transient failure is tried again. */
52
+ readonly maxRetries: number;
53
+ private readonly apiKey;
54
+ private readonly fetch;
55
+ private readonly onUnauthorized;
56
+ private readonly timeoutMs;
57
+ private readonly onRetry;
58
+ constructor(options: TransportOptions);
59
+ /** The answer as JSON. */
60
+ json<T>(call: Call): Promise<T>;
61
+ /** The answer as text. */
62
+ text(call: Call): Promise<string>;
63
+ /**
64
+ * The answer itself, its body unread (an event stream). Aborting the
65
+ * call's signal later still ends the body.
66
+ */
67
+ stream(call: Call): Promise<Response>;
68
+ /**
69
+ * Makes `call` until it is answered with a 2xx, and reads the answer with
70
+ * `read`. Each attempt has `timeoutMs` until its answer's headers arrive,
71
+ * which counts the upload of a request body but not the reading of the
72
+ * answer's (an event stream's runs for minutes). A 401 calls
73
+ * `onUnauthorized` (once) and tries again with the credential as it is
74
+ * then. A transient failure is tried again, up to `maxRetries` times, when
75
+ * `call.retry` allows it; reading the body is part of the attempt, so a
76
+ * connection lost mid-body is retried too.
77
+ */
78
+ request<T>(call: Call, read: (response: Response) => Promise<T>, streaming?: boolean): Promise<T>;
79
+ /** What one attempt at `call` sends, `body` already encoded. */
80
+ private init;
81
+ private url;
82
+ }
83
+ /** `id` as a path segment. */
84
+ export declare function segment(id: string): string;
85
+ /** The wait before retry `retry` (0 for the first) when the server names none. */
86
+ export declare function backoff(retry: number): number;
87
+ /** Waits `ms`, or rejects with the signal's reason once it aborts. */
88
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
89
+ export declare function isRecord(v: unknown): v is Record<string, unknown>;
package/dist/http.js ADDED
@@ -0,0 +1,284 @@
1
+ // One request to the API, as every resource makes it: the credential, the
2
+ // version header, a time limit on the answer, retries of what is safe to
3
+ // retry, and the server's error body turned into a CloudSimError.
4
+ import { CloudSimError, generalCode } from './errors.js';
5
+ export const DEFAULT_BASE_URL = 'https://cloud.simantic.com';
6
+ const DEFAULT_MAX_RETRIES = 3;
7
+ // The CLI's and the dashboard's own limit: a submit can take a while, as the
8
+ // server checks the credential upstream and its database may cold-start.
9
+ const DEFAULT_TIMEOUT_MS = 60_000;
10
+ // Backoff between retries when the server names no wait: 0.5 s doubling to
11
+ // 8 s, each spread over its upper half so many clients do not retry in step.
12
+ const FIRST_BACKOFF_MS = 500;
13
+ const MAX_BACKOFF_MS = 8_000;
14
+ // A Retry-After longer than this is not waited out: a call that would sit
15
+ // for minutes is the caller's to decide on, so it fails with the error.
16
+ const MAX_RETRY_AFTER_MS = 60_000;
17
+ export class Transport {
18
+ version;
19
+ baseUrl;
20
+ /** How many times a transient failure is tried again. */
21
+ maxRetries;
22
+ apiKey;
23
+ fetch;
24
+ onUnauthorized;
25
+ timeoutMs;
26
+ onRetry;
27
+ constructor(options) {
28
+ this.apiKey = options.apiKey;
29
+ this.version = options.version;
30
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
31
+ // Called as a plain function: a browser's fetch throws "Illegal
32
+ // invocation" when called as a method of anything but the window.
33
+ const f = options.fetch ?? globalThis.fetch;
34
+ this.fetch = (input, init) => f(input, init);
35
+ this.onUnauthorized = options.onUnauthorized;
36
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
37
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
38
+ this.onRetry = options.onRetry;
39
+ }
40
+ /** The answer as JSON. */
41
+ json(call) {
42
+ return this.request(call, async (response) => {
43
+ const text = await response.text();
44
+ try {
45
+ return JSON.parse(text);
46
+ }
47
+ catch {
48
+ throw new CloudSimError(`The Cloud Sim API sent a reply that is not JSON (HTTP ${response.status}).`, { status: response.status, code: 'invalid_response', requestId: requestId(response) });
49
+ }
50
+ });
51
+ }
52
+ /** The answer as text. */
53
+ text(call) {
54
+ const headers = { Accept: 'text/plain', ...call.headers };
55
+ return this.request({ ...call, headers }, (response) => response.text());
56
+ }
57
+ /**
58
+ * The answer itself, its body unread (an event stream). Aborting the
59
+ * call's signal later still ends the body.
60
+ */
61
+ stream(call) {
62
+ return this.request(call, async (response) => response, true);
63
+ }
64
+ /**
65
+ * Makes `call` until it is answered with a 2xx, and reads the answer with
66
+ * `read`. Each attempt has `timeoutMs` until its answer's headers arrive,
67
+ * which counts the upload of a request body but not the reading of the
68
+ * answer's (an event stream's runs for minutes). A 401 calls
69
+ * `onUnauthorized` (once) and tries again with the credential as it is
70
+ * then. A transient failure is tried again, up to `maxRetries` times, when
71
+ * `call.retry` allows it; reading the body is part of the attempt, so a
72
+ * connection lost mid-body is retried too.
73
+ */
74
+ async request(call, read, streaming = false) {
75
+ // Encoded once: a run's JSON can hold its whole ELF, and a retry sends
76
+ // it again. A FormData is sent as many times as asked.
77
+ const body = call.form ?? (call.json === undefined ? undefined : JSON.stringify(call.json));
78
+ const url = this.url(call.path, call.query);
79
+ let refreshed = false;
80
+ for (let retries = 0;; retries++) {
81
+ if (call.signal?.aborted)
82
+ throw call.signal.reason;
83
+ // Outside the try: a credential function that fails is the caller's
84
+ // failure, not the network's.
85
+ const init = await this.init(call, body);
86
+ const attempt = new Attempt(call.signal, call.timeoutMs ?? this.timeoutMs);
87
+ let response;
88
+ let failure;
89
+ let wait = null;
90
+ try {
91
+ response = await this.fetch(url, { ...init, signal: attempt.signal });
92
+ attempt.answered();
93
+ if (response.ok) {
94
+ const value = await read(response);
95
+ // A stream's body is read after this returns: the caller's
96
+ // signal must still be able to end it.
97
+ if (streaming)
98
+ attempt.keep();
99
+ return value;
100
+ }
101
+ }
102
+ catch (e) {
103
+ if (call.signal?.aborted)
104
+ throw call.signal.reason;
105
+ if (attempt.timedOut) {
106
+ failure = new CloudSimError(`The Cloud Sim API did not answer within ${Math.round(attempt.timeoutMs / 1000)} s.`, { status: 0, code: 'timeout' });
107
+ }
108
+ else if (e instanceof CloudSimError) {
109
+ throw e;
110
+ }
111
+ else {
112
+ // fetch rejects without an answer for every network-level failure,
113
+ // a CORS refusal included, and does not say which.
114
+ failure = new CloudSimError("Couldn't reach the Cloud Sim API.", {
115
+ status: 0,
116
+ code: 'network_error',
117
+ });
118
+ }
119
+ response = undefined;
120
+ }
121
+ finally {
122
+ attempt.end();
123
+ }
124
+ if (response !== undefined) {
125
+ // Outside the try, as the credential is: what onUnauthorized
126
+ // throws is the caller's.
127
+ if (response.status === 401 && this.onUnauthorized && !refreshed) {
128
+ refreshed = true;
129
+ await response.body?.cancel().catch(() => { });
130
+ await this.onUnauthorized();
131
+ retries--;
132
+ continue;
133
+ }
134
+ failure = await errorFrom(response);
135
+ wait = retryAfter(response);
136
+ }
137
+ if (failure === undefined)
138
+ throw new Error('unreachable: an attempt ended with no answer');
139
+ if (!call.retry || !failure.transient || retries >= this.maxRetries)
140
+ throw failure;
141
+ if (wait !== null && wait > MAX_RETRY_AFTER_MS)
142
+ throw failure;
143
+ const info = { attempt: retries + 1, error: failure, delayMs: wait ?? backoff(retries) };
144
+ this.onRetry?.(info);
145
+ call.onRetry?.(info);
146
+ await sleep(info.delayMs, call.signal);
147
+ }
148
+ }
149
+ /** What one attempt at `call` sends, `body` already encoded. */
150
+ async init(call, body) {
151
+ const headers = { Accept: 'application/json', ...call.headers };
152
+ const key = typeof this.apiKey === 'function' ? await this.apiKey() : this.apiKey;
153
+ // The catalogs need no credential, so a client made without one can
154
+ // still read them.
155
+ if (key)
156
+ headers['Authorization'] = `Bearer ${key}`;
157
+ const version = call.version ?? this.version;
158
+ if (version)
159
+ headers['Cloud-Sim-Version'] = version;
160
+ if (typeof body === 'string')
161
+ headers['Content-Type'] = 'application/json';
162
+ // No `cache: 'no-store'`: some edge runtimes refuse the option, and the
163
+ // API's answers carry no validators a browser could cache them by.
164
+ return { method: call.method, headers, body };
165
+ }
166
+ url(path, query) {
167
+ const params = new URLSearchParams();
168
+ for (const [name, value] of Object.entries(query ?? {})) {
169
+ if (value !== undefined)
170
+ params.set(name, String(value));
171
+ }
172
+ const search = params.toString();
173
+ return `${this.baseUrl}${path}${search ? `?${search}` : ''}`;
174
+ }
175
+ }
176
+ /**
177
+ * One attempt's signal: aborted by the caller's signal, or by the time limit
178
+ * on the answer's headers.
179
+ */
180
+ class Attempt {
181
+ outer;
182
+ signal;
183
+ timeoutMs;
184
+ timedOut = false;
185
+ controller = new AbortController();
186
+ timer;
187
+ kept = false;
188
+ constructor(outer, timeoutMs) {
189
+ this.outer = outer;
190
+ this.signal = this.controller.signal;
191
+ this.timeoutMs = timeoutMs;
192
+ outer?.addEventListener('abort', this.onAbort, { once: true });
193
+ // 0 or Infinity: no limit.
194
+ if (timeoutMs > 0 && Number.isFinite(timeoutMs)) {
195
+ this.timer = setTimeout(() => {
196
+ this.timedOut = true;
197
+ this.controller.abort();
198
+ }, timeoutMs);
199
+ }
200
+ }
201
+ /** The headers arrived: the time limit is met. */
202
+ answered() {
203
+ clearTimeout(this.timer);
204
+ }
205
+ /** The body outlives the attempt: stay tied to the caller's signal. */
206
+ keep() {
207
+ this.kept = true;
208
+ }
209
+ end() {
210
+ clearTimeout(this.timer);
211
+ if (!this.kept)
212
+ this.outer?.removeEventListener('abort', this.onAbort);
213
+ }
214
+ onAbort = () => this.controller.abort(this.outer?.reason);
215
+ }
216
+ /** `id` as a path segment. */
217
+ export function segment(id) {
218
+ return encodeURIComponent(id);
219
+ }
220
+ function requestId(response) {
221
+ return response.headers.get('X-Request-Id');
222
+ }
223
+ /** The error a non-2xx answer stands for, from its body when it has one. */
224
+ async function errorFrom(response) {
225
+ const status = response.status;
226
+ let body;
227
+ try {
228
+ body = JSON.parse(await response.text());
229
+ }
230
+ catch {
231
+ body = undefined;
232
+ }
233
+ if (isRecord(body) && typeof body.error === 'string') {
234
+ return new CloudSimError(body.error, {
235
+ status,
236
+ code: typeof body.code === 'string' ? body.code : generalCode(status),
237
+ param: typeof body.param === 'string' ? body.param : null,
238
+ requestId: typeof body.request_id === 'string' ? body.request_id : requestId(response),
239
+ });
240
+ }
241
+ // Not the server's own answer: InstaCloud's Cloudflare edge swaps a 502
242
+ // or 504 for an HTML page of its own.
243
+ return new CloudSimError(`The Cloud Sim API answered HTTP ${status}.`, {
244
+ status,
245
+ code: generalCode(status),
246
+ requestId: requestId(response),
247
+ });
248
+ }
249
+ /** `Retry-After` in milliseconds: delta-seconds or an HTTP date. */
250
+ function retryAfter(response) {
251
+ const value = response.headers.get('Retry-After')?.trim();
252
+ if (!value)
253
+ return null;
254
+ if (/^\d+$/.test(value))
255
+ return Number(value) * 1000;
256
+ const at = Date.parse(value);
257
+ return Number.isNaN(at) ? null : Math.max(0, at - Date.now());
258
+ }
259
+ /** The wait before retry `retry` (0 for the first) when the server names none. */
260
+ export function backoff(retry) {
261
+ const ceiling = Math.min(MAX_BACKOFF_MS, FIRST_BACKOFF_MS * 2 ** retry);
262
+ return ceiling / 2 + (Math.random() * ceiling) / 2;
263
+ }
264
+ /** Waits `ms`, or rejects with the signal's reason once it aborts. */
265
+ export function sleep(ms, signal) {
266
+ return new Promise((resolve, reject) => {
267
+ if (signal?.aborted) {
268
+ reject(signal.reason);
269
+ return;
270
+ }
271
+ const onAbort = () => {
272
+ clearTimeout(timer);
273
+ reject(signal?.reason);
274
+ };
275
+ const timer = setTimeout(() => {
276
+ signal?.removeEventListener('abort', onAbort);
277
+ resolve();
278
+ }, ms);
279
+ signal?.addEventListener('abort', onAbort, { once: true });
280
+ });
281
+ }
282
+ export function isRecord(v) {
283
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
284
+ }
@@ -0,0 +1,9 @@
1
+ export { CloudSim, type CloudSimOptions } from './client.js';
2
+ export { CloudSimError, type CloudSimErrorInit, CompareError, isTransient, } from './errors.js';
3
+ export type { ApiKey, RetryInfo } from './http.js';
4
+ export { type FollowEvent, type FollowOptions, type FollowTransport, type Offsets, type StreamName, POLL_INTERVAL_MS, } from './follow.js';
5
+ export type { CallOptions, Comparison, CompareParams, CreateOptions, CreateRunParams, FirmwareInput, ListRunsParams, RunListing, RunOptionsInput, Runs, WaitOptions, } from './runs.js';
6
+ export type { McuFilter, McuListOptions, Mcus, Versions } from './catalog.js';
7
+ export { type Difference, type ResultComparison, compareResults } from './compare.js';
8
+ export { type Changes, type Engine, type EngineOption, type ErrorBody, type Firmware, type Lifecycle, type Live, type LiveStream, type Mcu, type McuEngine, type McuList, type Run, type RunEngine, type RunList, type RunOptions, type RunResult, type RunStatus, type UartEncoding, type Version, type VersionList, FINISHED, isFinished, } from './types.js';
9
+ export type { components, operations, paths } from './generated/v1.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // @simantic/cloud-sim: a client of the Cloud Sim API (openapi/v1.json in
2
+ // the cloud-sim repo), on fetch, with no dependencies. It runs wherever
3
+ // fetch does: Node 18 and later, browsers, Deno, Bun, edge runtimes.
4
+ // Anything that needs Node lives in '@simantic/cloud-sim/node'.
5
+ export { CloudSim } from './client.js';
6
+ export { CloudSimError, CompareError, isTransient, } from './errors.js';
7
+ export { POLL_INTERVAL_MS, } from './follow.js';
8
+ export { compareResults } from './compare.js';
9
+ export { FINISHED, isFinished, } from './types.js';
package/dist/node.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The file at `path` as a Blob, ready to be a run's firmware: it is sent as
3
+ * a multipart upload.
4
+ */
5
+ export declare function fromPath(path: string): Promise<Blob>;
package/dist/node.js ADDED
@@ -0,0 +1,10 @@
1
+ // '@simantic/cloud-sim/node': helpers that need Node, kept out of the main
2
+ // entry so it stays free of `node:` imports for browsers and edge runtimes.
3
+ import { readFile } from 'node:fs/promises';
4
+ /**
5
+ * The file at `path` as a Blob, ready to be a run's firmware: it is sent as
6
+ * a multipart upload.
7
+ */
8
+ export async function fromPath(path) {
9
+ return new Blob([await readFile(path)]);
10
+ }
package/dist/runs.d.ts ADDED
@@ -0,0 +1,152 @@
1
+ import { type FollowEvent, type FollowOptions } from './follow.js';
2
+ import { type Transport } from './http.js';
3
+ import { type Run, type RunList, type RunResult, type UartEncoding, type Version } from './types.js';
4
+ /**
5
+ * A run's firmware: the ELF's bytes (sent as a multipart upload), an https
6
+ * URL the server fetches it from, or the ELF already base64-encoded (sent
7
+ * in JSON).
8
+ */
9
+ export type FirmwareInput = Blob | Uint8Array | ArrayBuffer | {
10
+ url: string;
11
+ } | {
12
+ base64: string;
13
+ };
14
+ /** What every call takes. */
15
+ export interface CallOptions {
16
+ /** Ends the call, its retries and its waits between them, with the signal's reason. */
17
+ signal?: AbortSignal;
18
+ }
19
+ /** The engine-neutral run options. Each left out takes the engine's default. */
20
+ export interface RunOptionsInput {
21
+ /** Simulated seconds to run for: more than 0, at most 60. Default 10. */
22
+ simulatedSeconds?: number;
23
+ /** `hex` rows (the default) or `ascii` text. */
24
+ uartEncoding?: UartEncoding;
25
+ /** Each UART record without its timestamp and label. Default false. */
26
+ uartMessagesOnly?: boolean;
27
+ /** A bus access that reaches no device is a bus error, not a read of 0. */
28
+ trapUnmapped?: boolean;
29
+ /** USB device peripherals (such as `USB`) to put a CDC-ACM console behind. */
30
+ usbCdcConsoles?: string[];
31
+ }
32
+ /** What a create takes beyond the run itself. */
33
+ export interface CreateOptions extends CallOptions {
34
+ /** The API version for this run, in place of the client's. */
35
+ version?: string;
36
+ /**
37
+ * The time limit until the server answers, in place of the client's
38
+ * `timeoutMs`. It counts the upload, so give a large firmware more.
39
+ */
40
+ timeoutMs?: number;
41
+ }
42
+ export interface CreateRunParams extends RunOptionsInput, CreateOptions {
43
+ /** The MCU, by its canonical name (`client.mcus.list()`). */
44
+ mcu: string;
45
+ /** The engine; the version's default engine when left out. */
46
+ engine?: string;
47
+ firmware: FirmwareInput;
48
+ /**
49
+ * Makes the create safe to repeat: the same request again with the same
50
+ * key gets the run it made. One is generated when left out, so the SDK's
51
+ * own retries never make a second run.
52
+ */
53
+ idempotencyKey?: string;
54
+ }
55
+ export interface ListRunsParams extends CallOptions {
56
+ /** Runs per page, 1 to 100 (the server's default is 20). */
57
+ limit?: number;
58
+ /** Start after this run (a page's `next`). */
59
+ before?: string;
60
+ /** Only runs on this engine. */
61
+ engine?: string;
62
+ }
63
+ export interface WaitOptions extends CallOptions {
64
+ /** How long to wait between reads of the run. Default 1000. */
65
+ intervalMs?: number;
66
+ }
67
+ export interface CompareParams extends RunOptionsInput, CreateOptions {
68
+ /** The engines to run the firmware on, one run each. */
69
+ engines: string[];
70
+ mcu: string;
71
+ firmware: FirmwareInput;
72
+ }
73
+ export interface Comparison {
74
+ /** The version every run was made on. */
75
+ version: Version;
76
+ /** One run per engine, in the order the engines were given. */
77
+ runs: Run[];
78
+ }
79
+ export declare class Runs {
80
+ private readonly http;
81
+ constructor(http: Transport);
82
+ /**
83
+ * Makes a run. Answers with the run whether it was made now (201) or by an
84
+ * earlier request with the same idempotency key (200).
85
+ */
86
+ create(params: CreateRunParams): Promise<Run>;
87
+ /** The run. */
88
+ get(id: string, options?: CallOptions): Promise<Run>;
89
+ /**
90
+ * The caller's runs, newest first. Awaited, it is the first page
91
+ * (`{ runs, next }`); iterated with `for await`, it is every run, a page at
92
+ * a time.
93
+ */
94
+ list(params?: ListRunsParams): RunListing;
95
+ /** What a finished run produced. A run that has not finished is 409 `run_not_finished`. */
96
+ result(id: string, options?: CallOptions): Promise<RunResult>;
97
+ /** A finished run's UART output, as text. */
98
+ output(id: string, options?: CallOptions): Promise<string>;
99
+ /** A finished run's console, as text. */
100
+ log(id: string, options?: CallOptions): Promise<string>;
101
+ /**
102
+ * Cancels a run: a queued one at once, a running one within seconds. A run
103
+ * that already succeeded or failed is 409 `run_finished`. Retried like a
104
+ * read: the server answers a repeated cancel with the run as it is, and a
105
+ * run that finished meanwhile gets the 409, which is not retried.
106
+ */
107
+ cancel(id: string, options?: CallOptions): Promise<Run>;
108
+ /** The run once it has finished: succeeded, failed or canceled. */
109
+ wait(id: string, options?: WaitOptions): Promise<Run>;
110
+ /**
111
+ * The run as events, from wherever it is (or from `from`) to its end:
112
+ * `status` when its status changes, `log` and `output` text as it prints,
113
+ * `reset` when a stream starts over, `reconnecting` while a failure is
114
+ * being retried, and `end`. See `FollowEvent`.
115
+ */
116
+ follow(id: string, options?: FollowOptions): AsyncGenerator<FollowEvent, void, undefined>;
117
+ /**
118
+ * Runs the same firmware, MCU and options once on each engine, in
119
+ * parallel, and answers with the runs. It reads the version first (the
120
+ * call's, the client's, or the server's default), and an engine the
121
+ * version does not have fails the whole comparison
122
+ * (`engine_not_in_version`) before anything is uploaded. Every run is made
123
+ * on that version, whatever the server's default becomes meanwhile. Each
124
+ * run counts against the caller's limit of unfinished runs.
125
+ *
126
+ * The engines are checked before anything is asked of the server: none,
127
+ * or one named twice, is 400 `invalid_request` (`param: 'engines'`).
128
+ *
129
+ * Every engine's run is asked for even when another is refused. When
130
+ * some runs are made and others are not, it fails with a `CompareError`
131
+ * that carries the runs made, by engine, to follow or cancel.
132
+ */
133
+ compare(params: CompareParams): Promise<Comparison>;
134
+ private make;
135
+ /** Version `asked`, else the client's, else the server's default. */
136
+ private readVersion;
137
+ }
138
+ /**
139
+ * The caller's runs: a promise of the first page, and an async iterable of
140
+ * every run. Nothing is read until it is awaited or iterated.
141
+ */
142
+ export declare class RunListing implements PromiseLike<RunList>, AsyncIterable<Run> {
143
+ private readonly http;
144
+ private readonly params;
145
+ private first;
146
+ constructor(http: Transport, params: ListRunsParams);
147
+ /** The first page. */
148
+ firstPage(): Promise<RunList>;
149
+ then<A = RunList, B = never>(onFulfilled?: ((page: RunList) => A | PromiseLike<A>) | null, onRejected?: ((reason: unknown) => B | PromiseLike<B>) | null): Promise<A | B>;
150
+ [Symbol.asyncIterator](): AsyncGenerator<Run, void, undefined>;
151
+ private page;
152
+ }