@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.
- package/README.md +298 -0
- package/dist/catalog.d.ts +32 -0
- package/dist/catalog.js +58 -0
- package/dist/client.d.ts +53 -0
- package/dist/client.js +15 -0
- package/dist/compare.d.ts +26 -0
- package/dist/compare.js +53 -0
- package/dist/errors.d.ts +61 -0
- package/dist/errors.js +111 -0
- package/dist/follow.d.ts +61 -0
- package/dist/follow.js +277 -0
- package/dist/generated/v1.d.ts +1959 -0
- package/dist/generated/v1.js +5 -0
- package/dist/http.d.ts +89 -0
- package/dist/http.js +284 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.js +10 -0
- package/dist/runs.d.ts +152 -0
- package/dist/runs.js +284 -0
- package/dist/sse.d.ts +30 -0
- package/dist/sse.js +109 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.js +11 -0
- package/package.json +59 -0
package/dist/errors.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Every failure the SDK reports, other than an aborted signal (which throws
|
|
2
|
+
// the signal's own reason, as fetch does).
|
|
3
|
+
/**
|
|
4
|
+
* A request the Cloud Sim API refused or never answered.
|
|
5
|
+
*
|
|
6
|
+
* `status` is the HTTP status, or 0 when no answer arrived (offline, DNS, a
|
|
7
|
+
* CORS refusal, `timeoutMs` passed). `code` is a stable name for programs,
|
|
8
|
+
* such as `mcu_not_supported`, `network_error` or `timeout`; `param` names
|
|
9
|
+
* the request field at fault, when one is; `requestId` is the request's
|
|
10
|
+
* `X-Request-Id`, worth quoting in a bug report. `transient` says whether
|
|
11
|
+
* the same request may work later.
|
|
12
|
+
*/
|
|
13
|
+
export class CloudSimError extends Error {
|
|
14
|
+
status;
|
|
15
|
+
code;
|
|
16
|
+
param;
|
|
17
|
+
requestId;
|
|
18
|
+
transient;
|
|
19
|
+
constructor(message, init) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'CloudSimError';
|
|
22
|
+
this.status = init.status;
|
|
23
|
+
this.code = init.code;
|
|
24
|
+
this.param = init.param ?? null;
|
|
25
|
+
this.requestId = init.requestId ?? null;
|
|
26
|
+
this.transient = init.transient ?? transientStatus(init.status);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `runs.compare` made some of its runs but not all. The runs it made go on
|
|
31
|
+
* on the server and count against the caller's limit: follow them, or
|
|
32
|
+
* cancel them. Its status, code, param and request id are the first
|
|
33
|
+
* failure's, and it is `transient` only if every failure is. The Python
|
|
34
|
+
* SDK's `CompareError` has the same fields.
|
|
35
|
+
*/
|
|
36
|
+
export class CompareError extends CloudSimError {
|
|
37
|
+
/** The version the comparison was made on. */
|
|
38
|
+
version;
|
|
39
|
+
/** The runs made, by engine, in the order the engines were given. */
|
|
40
|
+
runs;
|
|
41
|
+
/**
|
|
42
|
+
* Why each other engine's run was not made, by engine, in the same order:
|
|
43
|
+
* a `CloudSimError`, or whatever else the create threw.
|
|
44
|
+
*/
|
|
45
|
+
errors;
|
|
46
|
+
constructor(version, runs, errors) {
|
|
47
|
+
const failures = Object.entries(errors);
|
|
48
|
+
const first = failures[0]?.[1];
|
|
49
|
+
const why = failures
|
|
50
|
+
.map(([engine, e]) => `${engine}: ${e instanceof Error ? e.message : String(e)}`)
|
|
51
|
+
.join('; ');
|
|
52
|
+
const made = Object.keys(runs);
|
|
53
|
+
let message = `${failures.length} of ${failures.length + made.length} runs could not be made (${why})`;
|
|
54
|
+
if (made.length > 0)
|
|
55
|
+
message += `; the runs on ${made.join(', ')} were made and run on`;
|
|
56
|
+
super(message, {
|
|
57
|
+
status: first instanceof CloudSimError ? first.status : 0,
|
|
58
|
+
code: first instanceof CloudSimError ? first.code : 'compare_failed',
|
|
59
|
+
param: first instanceof CloudSimError ? first.param : null,
|
|
60
|
+
requestId: first instanceof CloudSimError ? first.requestId : null,
|
|
61
|
+
transient: failures.every(([, e]) => isTransient(e)),
|
|
62
|
+
});
|
|
63
|
+
this.name = 'CompareError';
|
|
64
|
+
this.version = version;
|
|
65
|
+
this.runs = runs;
|
|
66
|
+
this.errors = errors;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether `error` is a failure that may go away if the same request is made
|
|
71
|
+
* again: a `CloudSimError` that is `transient`.
|
|
72
|
+
*/
|
|
73
|
+
export function isTransient(error) {
|
|
74
|
+
return error instanceof CloudSimError && error.transient;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether an answer with `status` may change if the request is made again:
|
|
78
|
+
* none arrived (0), the caller is at their run limit (429), or the server or
|
|
79
|
+
* the edge in front of it could not answer (502, 503, 504). Any other status
|
|
80
|
+
* is the server's answer to this request.
|
|
81
|
+
*/
|
|
82
|
+
export function transientStatus(status) {
|
|
83
|
+
return status === 0 || status === 429 || status === 502 || status === 503 || status === 504;
|
|
84
|
+
}
|
|
85
|
+
/** The code the server gives an error of `status` that names none of its own. */
|
|
86
|
+
export function generalCode(status) {
|
|
87
|
+
switch (status) {
|
|
88
|
+
case 400:
|
|
89
|
+
return 'invalid_request';
|
|
90
|
+
case 401:
|
|
91
|
+
return 'unauthorized';
|
|
92
|
+
case 403:
|
|
93
|
+
return 'forbidden';
|
|
94
|
+
case 404:
|
|
95
|
+
return 'not_found';
|
|
96
|
+
case 409:
|
|
97
|
+
return 'conflict';
|
|
98
|
+
case 410:
|
|
99
|
+
return 'gone';
|
|
100
|
+
case 413:
|
|
101
|
+
return 'payload_too_large';
|
|
102
|
+
case 415:
|
|
103
|
+
return 'unsupported_media_type';
|
|
104
|
+
case 429:
|
|
105
|
+
return 'too_many_runs';
|
|
106
|
+
case 503:
|
|
107
|
+
return 'unavailable';
|
|
108
|
+
default:
|
|
109
|
+
return status >= 500 ? 'internal_error' : 'http_error';
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/follow.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CloudSimError } from './errors.js';
|
|
2
|
+
import { type Transport } from './http.js';
|
|
3
|
+
import { type Run } from './types.js';
|
|
4
|
+
/** One of a run's two streams: the engine's console, or its UART output. */
|
|
5
|
+
export type StreamName = 'log' | 'output';
|
|
6
|
+
/** Where each stream has been read to, in bytes, as `log`/`output` events' `next` say. */
|
|
7
|
+
export interface Offsets {
|
|
8
|
+
log: number;
|
|
9
|
+
output: number;
|
|
10
|
+
}
|
|
11
|
+
export type FollowEvent =
|
|
12
|
+
/** The run: first, each time its status changes, and after a recovery. */
|
|
13
|
+
{
|
|
14
|
+
type: 'status';
|
|
15
|
+
run: Run;
|
|
16
|
+
}
|
|
17
|
+
/** More of the engine's console; `next` is where the console has been read to. */
|
|
18
|
+
| {
|
|
19
|
+
type: 'log';
|
|
20
|
+
text: string;
|
|
21
|
+
next: number;
|
|
22
|
+
}
|
|
23
|
+
/** More of the UART output; `next` is where it has been read to. */
|
|
24
|
+
| {
|
|
25
|
+
type: 'output';
|
|
26
|
+
text: string;
|
|
27
|
+
next: number;
|
|
28
|
+
}
|
|
29
|
+
/** Drop what this stream has had so far: it starts over. */
|
|
30
|
+
| {
|
|
31
|
+
type: 'reset';
|
|
32
|
+
stream: StreamName;
|
|
33
|
+
}
|
|
34
|
+
/** A failure is being retried; a `status` follows once it has recovered. */
|
|
35
|
+
| {
|
|
36
|
+
type: 'reconnecting';
|
|
37
|
+
error: CloudSimError;
|
|
38
|
+
}
|
|
39
|
+
/** The run has finished and both streams are complete. The last event. */
|
|
40
|
+
| {
|
|
41
|
+
type: 'end';
|
|
42
|
+
run: Run;
|
|
43
|
+
};
|
|
44
|
+
export type FollowTransport = 'auto' | 'sse' | 'poll';
|
|
45
|
+
export interface FollowOptions {
|
|
46
|
+
/** Ends the iteration: it throws the signal's reason. */
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
/** `auto` (the default: `sse`, falling back to `poll`), `sse` or `poll`. */
|
|
49
|
+
transport?: FollowTransport;
|
|
50
|
+
/** How long `poll` waits between pages while nothing more is waiting. */
|
|
51
|
+
intervalMs?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Where to start each stream: the `next` of the last `log` and `output`
|
|
54
|
+
* events of an earlier follow (0 for a stream that had none). Left out,
|
|
55
|
+
* both start at 0.
|
|
56
|
+
*/
|
|
57
|
+
from?: Offsets;
|
|
58
|
+
}
|
|
59
|
+
/** The wait between pages while nothing more is waiting, as the dashboard has it. */
|
|
60
|
+
export declare const POLL_INTERVAL_MS = 750;
|
|
61
|
+
export declare function follow(http: Transport, id: string, options?: FollowOptions): AsyncGenerator<FollowEvent, void, undefined>;
|
package/dist/follow.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// `runs.follow`: a run's status and output as events, from wherever it is
|
|
2
|
+
// (or from the offsets given) to its end. The text of the `log` events,
|
|
3
|
+
// concatenated, is the run's whole console, and that of the `output` events
|
|
4
|
+
// its whole UART output, with one exception: a `reset` says the run's final
|
|
5
|
+
// result differs from what it shipped while it ran, and that stream starts
|
|
6
|
+
// over, so whatever was kept of it is to be dropped. Each `log` and
|
|
7
|
+
// `output` event says where its stream has been read to (`next`), so a
|
|
8
|
+
// caller that stops can follow again `from` there.
|
|
9
|
+
//
|
|
10
|
+
// Two transports give the same events:
|
|
11
|
+
// - `sse` reads GET /v1/runs/{id}/events, and when a stream closes before
|
|
12
|
+
// `end` (the server ends each connection after at most 10 minutes) it
|
|
13
|
+
// reconnects with Last-Event-ID, which resumes after the last whole
|
|
14
|
+
// event. The server's `error` event (it could not read the run, as in a
|
|
15
|
+
// store outage) and a connection lost mid-stream are transient failures:
|
|
16
|
+
// the stream waits (the event's `retry`, 5 s) and reconnects, and fails
|
|
17
|
+
// only after `maxRetries` of them with nothing new in between;
|
|
18
|
+
// - `poll` pages GET /v1/runs/{id}/live, sending back each stream's `next`.
|
|
19
|
+
// This ports the homepage dashboard's polling loop; unlike the legacy
|
|
20
|
+
// /jobs/{id}/live, /v1's keeps serving pages after the run ends, from its
|
|
21
|
+
// result, so the pages alone make up the whole output.
|
|
22
|
+
// `auto`, the default, is `sse` that falls back to `poll` when the events
|
|
23
|
+
// request fails before its first event: a server without the route (404,
|
|
24
|
+
// 405), something in between that will not pass an event stream, or a
|
|
25
|
+
// network failure. Once an event has arrived it stays on `sse`. SSE through
|
|
26
|
+
// the edge (Cloudflare, then InstaCloud) was checked in the plan's spike
|
|
27
|
+
// S1: unbuffered, open past 10 minutes, idle streams kept by keep-alives.
|
|
28
|
+
//
|
|
29
|
+
// While a failure is being retried, either transport says so with a
|
|
30
|
+
// `reconnecting` event, and the first event once it has recovered is a
|
|
31
|
+
// `status`, even if the status is the same, so a UI can show
|
|
32
|
+
// "Reconnecting…" between the two.
|
|
33
|
+
import { CloudSimError } from './errors.js';
|
|
34
|
+
import { isRecord, segment, sleep } from './http.js';
|
|
35
|
+
import { SseParser, readEvents } from './sse.js';
|
|
36
|
+
import { isFinished } from './types.js';
|
|
37
|
+
/** The wait between pages while nothing more is waiting, as the dashboard has it. */
|
|
38
|
+
export const POLL_INTERVAL_MS = 750;
|
|
39
|
+
// Until the server names one: the API's own `retry` is 1 s.
|
|
40
|
+
const RECONNECT_MS = 1_000;
|
|
41
|
+
export async function* follow(http, id, options = {}) {
|
|
42
|
+
// A signal of the follow's own, so a caller that stops iterating also
|
|
43
|
+
// stops whatever request or retry is still under way.
|
|
44
|
+
const outer = options.signal;
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const onAbort = () => controller.abort(outer?.reason);
|
|
47
|
+
if (outer?.aborted)
|
|
48
|
+
onAbort();
|
|
49
|
+
else
|
|
50
|
+
outer?.addEventListener('abort', onAbort, { once: true });
|
|
51
|
+
const inner = { ...options, signal: controller.signal };
|
|
52
|
+
try {
|
|
53
|
+
switch (options.transport ?? 'auto') {
|
|
54
|
+
case 'poll':
|
|
55
|
+
return yield* followPoll(http, id, inner);
|
|
56
|
+
case 'sse':
|
|
57
|
+
return yield* followSse(http, id, inner, false);
|
|
58
|
+
default:
|
|
59
|
+
return yield* followSse(http, id, inner, true);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
outer?.removeEventListener('abort', onAbort);
|
|
64
|
+
controller.abort();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function* followPoll(http, id, { signal, intervalMs = POLL_INTERVAL_MS, from }) {
|
|
68
|
+
const at = { log: from?.log ?? 0, output: from?.output ?? 0 };
|
|
69
|
+
let status;
|
|
70
|
+
// A failure was retried: the next status is news whatever it is.
|
|
71
|
+
let recovered = false;
|
|
72
|
+
for (;;) {
|
|
73
|
+
const page = yield* surfacing((onRetry) => http.json({
|
|
74
|
+
method: 'GET',
|
|
75
|
+
path: `/v1/runs/${segment(id)}/live`,
|
|
76
|
+
query: { log: at.log, output: at.output },
|
|
77
|
+
signal,
|
|
78
|
+
onRetry,
|
|
79
|
+
retry: true,
|
|
80
|
+
}), () => {
|
|
81
|
+
recovered = true;
|
|
82
|
+
});
|
|
83
|
+
if (page.run.status !== status || recovered) {
|
|
84
|
+
status = page.run.status;
|
|
85
|
+
recovered = false;
|
|
86
|
+
yield { type: 'status', run: page.run };
|
|
87
|
+
}
|
|
88
|
+
for (const stream of ['log', 'output']) {
|
|
89
|
+
const part = page[stream];
|
|
90
|
+
// Sent back as it came: it counts bytes, and a character cut off at
|
|
91
|
+
// the end of what has arrived waits for the next page.
|
|
92
|
+
at[stream] = part.next;
|
|
93
|
+
if (part.reset)
|
|
94
|
+
yield { type: 'reset', stream };
|
|
95
|
+
if (part.text)
|
|
96
|
+
yield { type: stream, text: part.text, next: part.next };
|
|
97
|
+
}
|
|
98
|
+
const more = page.log.more || page.output.more;
|
|
99
|
+
if (isFinished(page.run) && !more) {
|
|
100
|
+
yield { type: 'end', run: page.run };
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// What is already waiting is read at once.
|
|
104
|
+
if (!more)
|
|
105
|
+
await sleep(intervalMs, signal);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The run's event stream. With `fallback`, a stream that fails before its
|
|
110
|
+
* first event gives way to polling, from where it was asked to start: no
|
|
111
|
+
* event has been passed on yet, so none is repeated.
|
|
112
|
+
*/
|
|
113
|
+
async function* followSse(http, id, options, fallback) {
|
|
114
|
+
const { signal, from } = options;
|
|
115
|
+
// `from` is resumed from as an event id would be.
|
|
116
|
+
const parser = new SseParser(from ? `${from.log}:${from.output}` : undefined);
|
|
117
|
+
let status;
|
|
118
|
+
// Whether an event has arrived: from then on, this is the transport.
|
|
119
|
+
let received = false;
|
|
120
|
+
// Failures of the stream since it last had news for the caller.
|
|
121
|
+
let failures = 0;
|
|
122
|
+
const fallingBack = (e) => fallback && !received && !signal?.aborted && !refusesCaller(e);
|
|
123
|
+
// A failure was retried: the next status is passed on whatever it is
|
|
124
|
+
// (but is news, which resets `failures`, only if it changed).
|
|
125
|
+
let recovered = false;
|
|
126
|
+
const recovering = () => {
|
|
127
|
+
recovered = true;
|
|
128
|
+
};
|
|
129
|
+
for (;;) {
|
|
130
|
+
const headers = { Accept: 'text/event-stream' };
|
|
131
|
+
// Only an id the server gave (or `from`): it refuses any other (400).
|
|
132
|
+
if (parser.lastId)
|
|
133
|
+
headers['Last-Event-ID'] = parser.lastId;
|
|
134
|
+
let body;
|
|
135
|
+
try {
|
|
136
|
+
const response = yield* surfacing((onRetry) => http.stream({
|
|
137
|
+
method: 'GET',
|
|
138
|
+
path: `/v1/runs/${segment(id)}/events`,
|
|
139
|
+
headers,
|
|
140
|
+
signal,
|
|
141
|
+
onRetry,
|
|
142
|
+
// Before the first event, falling back to polling is the retry.
|
|
143
|
+
retry: received || !fallback,
|
|
144
|
+
}), recovering);
|
|
145
|
+
body = eventStream(response);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
if (fallingBack(e))
|
|
149
|
+
return yield* followPoll(http, id, options);
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
let failure;
|
|
153
|
+
try {
|
|
154
|
+
read: for await (const event of readEvents(body, parser)) {
|
|
155
|
+
const data = JSON.parse(event.data);
|
|
156
|
+
received = true;
|
|
157
|
+
switch (event.event) {
|
|
158
|
+
case 'status': {
|
|
159
|
+
const run = data;
|
|
160
|
+
// Each connection starts with the run again: it is passed on
|
|
161
|
+
// when its status changed, or after a failure.
|
|
162
|
+
if (run.status !== status || recovered) {
|
|
163
|
+
if (run.status !== status)
|
|
164
|
+
failures = 0;
|
|
165
|
+
status = run.status;
|
|
166
|
+
recovered = false;
|
|
167
|
+
yield { type: 'status', run };
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 'log':
|
|
172
|
+
case 'output': {
|
|
173
|
+
if (!isRecord(data) || typeof data.next !== 'number')
|
|
174
|
+
break;
|
|
175
|
+
failures = 0;
|
|
176
|
+
if (data.reset === true)
|
|
177
|
+
yield { type: 'reset', stream: event.event };
|
|
178
|
+
if (typeof data.text === 'string' && data.text) {
|
|
179
|
+
yield { type: event.event, text: data.text, next: data.next };
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case 'end':
|
|
184
|
+
yield { type: 'end', run: data };
|
|
185
|
+
return;
|
|
186
|
+
case 'error':
|
|
187
|
+
// The server could not read the run, and closes the stream.
|
|
188
|
+
failure = streamError(data);
|
|
189
|
+
break read;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
if (signal?.aborted)
|
|
195
|
+
throw signal.reason;
|
|
196
|
+
if (fallingBack(e))
|
|
197
|
+
return yield* followPoll(http, id, options);
|
|
198
|
+
// A connection lost mid-stream is picked up again below; anything
|
|
199
|
+
// else is the caller's.
|
|
200
|
+
if (!(e instanceof TypeError))
|
|
201
|
+
throw e;
|
|
202
|
+
failure = new CloudSimError('The connection to the Cloud Sim API was lost.', {
|
|
203
|
+
status: 0,
|
|
204
|
+
code: 'network_error',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Closed without a single event: as good as refused.
|
|
208
|
+
if (fallingBack(undefined))
|
|
209
|
+
return yield* followPoll(http, id, options);
|
|
210
|
+
if (failure) {
|
|
211
|
+
if (++failures > http.maxRetries)
|
|
212
|
+
throw failure;
|
|
213
|
+
recovering();
|
|
214
|
+
yield { type: 'reconnecting', error: failure };
|
|
215
|
+
}
|
|
216
|
+
// The stream closed before `end` (the server ends each connection after
|
|
217
|
+
// at most 10 minutes), or failed: pick it up where it was, as soon as
|
|
218
|
+
// the server asked (5 s after an `error`).
|
|
219
|
+
await sleep(parser.retry ?? RECONNECT_MS, signal);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The answer of `request`, passing on each retry it makes on the way as a
|
|
224
|
+
* `reconnecting` event (after calling `onRecovering`), as the retries
|
|
225
|
+
* happen rather than once the answer is in.
|
|
226
|
+
*/
|
|
227
|
+
async function* surfacing(request, onRecovering) {
|
|
228
|
+
const retries = [];
|
|
229
|
+
let settled;
|
|
230
|
+
let wake;
|
|
231
|
+
request((info) => {
|
|
232
|
+
retries.push(info.error);
|
|
233
|
+
wake?.();
|
|
234
|
+
}).then((value) => {
|
|
235
|
+
settled = { value };
|
|
236
|
+
wake?.();
|
|
237
|
+
}, (error) => {
|
|
238
|
+
settled = { error };
|
|
239
|
+
wake?.();
|
|
240
|
+
});
|
|
241
|
+
for (;;) {
|
|
242
|
+
const error = retries.shift();
|
|
243
|
+
if (error) {
|
|
244
|
+
onRecovering();
|
|
245
|
+
yield { type: 'reconnecting', error };
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (settled) {
|
|
249
|
+
if ('error' in settled)
|
|
250
|
+
throw settled.error;
|
|
251
|
+
return settled.value;
|
|
252
|
+
}
|
|
253
|
+
// Nothing can arrive between the checks above and this: callbacks run
|
|
254
|
+
// only once this function awaits.
|
|
255
|
+
await new Promise((resolve) => {
|
|
256
|
+
wake = resolve;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** The body of an answer that is an event stream, or why it is not one. */
|
|
261
|
+
function eventStream(response) {
|
|
262
|
+
const type = response.headers.get('Content-Type') ?? '';
|
|
263
|
+
if (!response.body || !type.toLowerCase().startsWith('text/event-stream')) {
|
|
264
|
+
response.body?.cancel().catch(() => { });
|
|
265
|
+
throw new CloudSimError(`The Cloud Sim API answered the event stream with ${type || 'no Content-Type'}.`, { status: response.status, code: 'invalid_response' });
|
|
266
|
+
}
|
|
267
|
+
return response.body;
|
|
268
|
+
}
|
|
269
|
+
/** A refusal of the caller, which polling would get as well. */
|
|
270
|
+
function refusesCaller(e) {
|
|
271
|
+
return e instanceof CloudSimError && (e.status === 401 || e.status === 403);
|
|
272
|
+
}
|
|
273
|
+
/** An `error` event's `{"error", "code"}`, as the failure it stands for. */
|
|
274
|
+
function streamError(data) {
|
|
275
|
+
const body = isRecord(data) ? data : {};
|
|
276
|
+
return new CloudSimError(typeof body.error === 'string' ? body.error : 'The Cloud Sim API could not read the run.', { status: 503, code: typeof body.code === 'string' ? body.code : 'unavailable' });
|
|
277
|
+
}
|