@gjermundgaraba/clankerbox-sdk 0.15.6 → 0.18.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,708 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
3
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
4
+ import { Context, Duration, Effect, Fiber, Layer, Match, Predicate, Record, Result, Schema, SchemaIssue } from "effect";
5
+ import { HttpClient, HttpClientRequest } from "effect/http";
6
+ //#region src/errors.ts
7
+ const located = {
8
+ host: Schema.optionalKey(Schema.String),
9
+ action: Schema.optionalKey(Schema.String),
10
+ id: Schema.optionalKey(Schema.String),
11
+ made: Schema.optionalKey(Schema.String)
12
+ };
13
+ /**
14
+ * The request is malformed, or the host list is one a client can't route by, or a config,
15
+ * profile or setup file couldn't be read or doesn't hold what it should: then `file` names it.
16
+ */
17
+ var Invalid = class extends Schema.TaggedError()("Invalid", {
18
+ message: Schema.String,
19
+ ...located,
20
+ file: Schema.optionalKey(Schema.String)
21
+ }, { httpApiStatus: 400 }) {};
22
+ /** The machine or checkpoint doesn't exist. A `delete` that meets it is done. */
23
+ var NotFound = class extends Schema.TaggedError()("NotFound", {
24
+ message: Schema.String,
25
+ ...located
26
+ }, { httpApiStatus: 404 }) {};
27
+ const ConflictKind = Schema.Literals(["exists", "busy"]);
28
+ /** The name is taken (`exists`), or another action holds the resource (`busy`). */
29
+ var Conflict = class extends Schema.TaggedError()("Conflict", {
30
+ message: Schema.String,
31
+ kind: ConflictKind,
32
+ ...located
33
+ }, { httpApiStatus: 409 }) {};
34
+ /** No host offers `base`: the host a create names, or any in the client's list. */
35
+ var NotOffered = class extends Schema.TaggedError()("NotOffered", {
36
+ message: Schema.String,
37
+ ...located,
38
+ base: Schema.String
39
+ }, { httpApiStatus: 412 }) {};
40
+ /** `machine`'s create, fork or restore failed before it was made: it can only be deleted. */
41
+ var NotMade = class extends Schema.TaggedError()("NotMade", {
42
+ message: Schema.String,
43
+ ...located,
44
+ machine: Schema.String
45
+ }, { httpApiStatus: 412 }) {};
46
+ /** The runtime no longer has `machine`: it can only be deleted. */
47
+ var Missing = class extends Schema.TaggedError()("Missing", {
48
+ message: Schema.String,
49
+ ...located,
50
+ machine: Schema.String
51
+ }, { httpApiStatus: 412 }) {};
52
+ /** `checkpoint`'s capture failed, so nothing restores from it: it can only be deleted. */
53
+ var NotReady = class extends Schema.TaggedError()("NotReady", {
54
+ message: Schema.String,
55
+ ...located,
56
+ checkpoint: Schema.String
57
+ }, { httpApiStatus: 412 }) {};
58
+ /** `checkpoint` holds RAM state saved under another runtime pin than the one the host runs. */
59
+ var PinMismatch = class extends Schema.TaggedError()("PinMismatch", {
60
+ message: Schema.String,
61
+ ...located,
62
+ checkpoint: Schema.String
63
+ }, { httpApiStatus: 412 }) {};
64
+ /** `machine` is in `state`, which the action doesn't act from. */
65
+ var WrongState = class extends Schema.TaggedError()("WrongState", {
66
+ message: Schema.String,
67
+ ...located,
68
+ machine: Schema.String,
69
+ state: Schema.String
70
+ }, { httpApiStatus: 412 }) {};
71
+ /** The runtime can't give the requested sizes, such as a disk below the base's. */
72
+ var Unsizable = class extends Schema.TaggedError()("Unsizable", {
73
+ message: Schema.String,
74
+ ...located
75
+ }, { httpApiStatus: 422 }) {};
76
+ /**
77
+ * A guest script exited non-zero or ran past its timeout; the message says which, with its
78
+ * output's tail. A failed `setup` leaves the machine unmade, a failed `preparation` made.
79
+ */
80
+ var ScriptFailed = class extends Schema.TaggedError()("ScriptFailed", {
81
+ message: Schema.String,
82
+ ...located,
83
+ script: Schema.Literals(["setup", "preparation"])
84
+ }, { httpApiStatus: 422 }) {};
85
+ /** The provider under the runtime refused the call for good, such as for its plan or API key. */
86
+ var Refused = class extends Schema.TaggedError()("Refused", {
87
+ message: Schema.String,
88
+ ...located
89
+ }, { httpApiStatus: 403 }) {};
90
+ /** The host has no room now; room can free up, so a later call may succeed. */
91
+ var Capacity = class extends Schema.TaggedError()("Capacity", {
92
+ message: Schema.String,
93
+ ...located
94
+ }, { httpApiStatus: 503 }) {};
95
+ /**
96
+ * No reply: only the client library makes it. A mutation's request may have reached the host
97
+ * and run, so it is retryable only when `readOnly`.
98
+ */
99
+ var Unavailable = class extends Schema.TaggedError()("Unavailable", {
100
+ message: Schema.String,
101
+ readOnly: Schema.Boolean,
102
+ ...located
103
+ }) {};
104
+ var Internal = class extends Schema.TaggedError()("Internal", {
105
+ message: Schema.String,
106
+ ...located
107
+ }, { httpApiStatus: 500 }) {};
108
+ /** What a host may answer with; each action declares its own share. */
109
+ const hostErrors = [
110
+ Invalid,
111
+ NotFound,
112
+ Conflict,
113
+ NotOffered,
114
+ NotMade,
115
+ Missing,
116
+ NotReady,
117
+ PinMismatch,
118
+ WrongState,
119
+ Unsizable,
120
+ ScriptFailed,
121
+ Refused,
122
+ Capacity,
123
+ Internal
124
+ ];
125
+ const HostError = Schema.Union(hostErrors);
126
+ /**
127
+ * Each carries, where known: `host` and `action`, which the client sets; `id`, the resource the
128
+ * error is about, which a host sets on `NotFound` and `Conflict` and the client sets to the call's
129
+ * subject otherwise; and `made`, the resource a create, fork, restore or capture makes, which a
130
+ * failure may leave behind.
131
+ */
132
+ const ClankerboxError = Schema.Union([...hostErrors, Unavailable]);
133
+ /**
134
+ * Whether the same call may succeed later: a busy resource or a full host may free up, and a read
135
+ * that got no reply may get one.
136
+ */
137
+ const retryable = (error) => Match.value(error).pipe(Match.tagsExhaustive({
138
+ Invalid: () => false,
139
+ NotFound: () => false,
140
+ Conflict: ({ kind }) => kind === "busy",
141
+ NotOffered: () => false,
142
+ NotMade: () => false,
143
+ Missing: () => false,
144
+ NotReady: () => false,
145
+ PinMismatch: () => false,
146
+ WrongState: () => false,
147
+ Unsizable: () => false,
148
+ ScriptFailed: () => false,
149
+ Refused: () => false,
150
+ Capacity: () => true,
151
+ Unavailable: ({ readOnly }) => readOnly,
152
+ Internal: () => false
153
+ }));
154
+ //#endregion
155
+ //#region src/ids.ts
156
+ const separator = "_";
157
+ const maxIdLength = 62;
158
+ const idPattern = /^[A-Za-z0-9_-]{1,62}$/u;
159
+ const hostIdPattern = /^[a-z][a-z0-9-]{0,31}$/u;
160
+ const hostIdRule = "a host ID is 1 to 32 lowercase letters, digits and '-', starting with a letter";
161
+ const namePattern = /^[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*$/u;
162
+ const nameRule = "a name starts with a letter, then letters, digits and '-', with no '--' and no '-' at the end";
163
+ const HostId = Schema.String.check(Schema.isPattern(hostIdPattern, { message: hostIdRule }));
164
+ /** Unique per host and per resource type. */
165
+ const Name = Schema.String.check(Schema.isPattern(namePattern, { message: nameRule }));
166
+ const describeId = (id) => {
167
+ if (!idPattern.test(id)) return `ID ${JSON.stringify(id)} must be 1 to ${maxIdLength} letters, digits, '_' or '-'`;
168
+ const at = id.indexOf(separator);
169
+ if (at <= 0 || at === id.length - 1) return `ID ${JSON.stringify(id)} must be <host>_<name>`;
170
+ if (!hostIdPattern.test(id.slice(0, at))) return `ID ${JSON.stringify(id)} has an invalid host: ${hostIdRule}`;
171
+ if (!namePattern.test(id.slice(at + 1))) return `ID ${JSON.stringify(id)} has an invalid name: ${nameRule}`;
172
+ };
173
+ /** `<host>_<name>`: neither part contains `_`, so an ID has exactly one. */
174
+ const Id = Schema.String.check(Schema.makeFilter((id) => describeId(id) ?? true));
175
+ const parseId = (id) => {
176
+ const problem = describeId(id);
177
+ if (problem !== void 0) return Effect.fail(new Invalid({ message: problem }));
178
+ const at = id.indexOf(separator);
179
+ return Effect.succeed({
180
+ host: id.slice(0, at),
181
+ name: id.slice(at + 1)
182
+ });
183
+ };
184
+ const parseName = (name) => namePattern.test(name) ? Effect.succeed(name) : Effect.fail(new Invalid({ message: `name ${JSON.stringify(name)}: ${nameRule}` }));
185
+ /** Checks both parts and the whole. */
186
+ const formatId = (host, name) => {
187
+ if (!hostIdPattern.test(host)) return Effect.fail(new Invalid({ message: `host ID ${JSON.stringify(host)}: ${hostIdRule}` }));
188
+ const id = `${host}${separator}${name}`;
189
+ return parseName(name).pipe(Effect.andThen(parseId(id)), Effect.as(id));
190
+ };
191
+ //#endregion
192
+ //#region src/resources.ts
193
+ const Runtime = Schema.Literals([
194
+ "smolvm",
195
+ "tart",
196
+ "boat"
197
+ ]);
198
+ const Size = Schema.Int.check(Schema.isGreaterThan(0));
199
+ /** The contract's mutations, which record themselves on a resource's `action`. */
200
+ const ActionName = Schema.Literals([
201
+ "createMachine",
202
+ "startMachine",
203
+ "stopMachine",
204
+ "deleteMachine",
205
+ "forkMachine",
206
+ "restoreMachine",
207
+ "captureCheckpoint",
208
+ "deleteCheckpoint"
209
+ ]);
210
+ const ActionStatus = Schema.Literals([
211
+ "running",
212
+ "failed",
213
+ "done"
214
+ ]);
215
+ /** A resource's last action; the next action replaces it. */
216
+ const ActionRecord = Schema.Union([Schema.Struct({
217
+ name: ActionName,
218
+ status: Schema.Literals(["running", "done"])
219
+ }), Schema.Struct({
220
+ name: ActionName,
221
+ status: Schema.Literal("failed"),
222
+ error: HostError
223
+ })]);
224
+ const SshEndpoint = Schema.Struct({
225
+ host: Schema.String,
226
+ port: Schema.Int.check(Schema.isBetween({
227
+ minimum: 1,
228
+ maximum: 65535
229
+ }))
230
+ });
231
+ const SshLogin = Schema.Struct({
232
+ user: Schema.String,
233
+ ...SshEndpoint.fields
234
+ });
235
+ /**
236
+ * `state` is `missing` when the runtime doesn't have the machine, and `unknown` when it couldn't
237
+ * be read. Until its create, fork or restore has `made` it, a machine can only be read, stopped or
238
+ * deleted. `ssh` is only on a running machine whose runtime gives it an endpoint, and `hostKey`,
239
+ * `<type> <base64>`, once preparation has recorded it.
240
+ */
241
+ const Machine = Schema.Struct({
242
+ id: Id,
243
+ runtime: Runtime,
244
+ createdAt: Schema.DateTimeUtcFromString,
245
+ base: Schema.String,
246
+ profile: Schema.optionalKey(Schema.String),
247
+ cpu: Size,
248
+ ramMib: Size,
249
+ diskGib: Size,
250
+ state: Schema.Literals([
251
+ "running",
252
+ "stopped",
253
+ "missing",
254
+ "unknown"
255
+ ]),
256
+ made: Schema.Boolean,
257
+ action: ActionRecord,
258
+ ssh: Schema.optionalKey(SshLogin),
259
+ hostKey: Schema.optionalKey(Schema.String)
260
+ }).annotate({ identifier: "Machine" });
261
+ /** The state as people read it: `running`, or `stopped (unmade)` for one never made. */
262
+ const stateLabel = ({ state, made }) => made ? state : `${state} (unmade)`;
263
+ /** `kind` is `ram` (RAM and disks) on smolvm, and `disk` on Tart and boat. */
264
+ const Checkpoint = Schema.Struct({
265
+ id: Id,
266
+ createdAt: Schema.DateTimeUtcFromString,
267
+ machine: Id,
268
+ kind: Schema.Literals(["ram", "disk"]),
269
+ base: Schema.String,
270
+ profile: Schema.optionalKey(Schema.String),
271
+ cpu: Size,
272
+ ramMib: Size,
273
+ diskGib: Size,
274
+ action: ActionRecord
275
+ }).annotate({ identifier: "Checkpoint" });
276
+ /** A checkpoint restores once its capture is done; it never changes after. */
277
+ const isReady = (checkpoint) => checkpoint.action.status === "done";
278
+ /**
279
+ * `runtimeVersion` is boat's API version on boat. `captureFrom` is the state a machine must be in
280
+ * to be captured: Tart's is `stopped`.
281
+ */
282
+ const Host = Schema.Struct({
283
+ id: HostId,
284
+ runtime: Runtime,
285
+ version: Schema.String,
286
+ runtimeVersion: Schema.String,
287
+ captureFrom: Schema.Literals(["running", "stopped"]),
288
+ bases: Schema.Array(Schema.String)
289
+ }).annotate({ identifier: "Host" });
290
+ const usageFields = {
291
+ host: HostId,
292
+ resource: Schema.Literals([
293
+ "cpu",
294
+ "ramMib",
295
+ "runningVms",
296
+ "activeSandboxes",
297
+ "startsPerMinute",
298
+ "startsPerHour",
299
+ "startsPerDay"
300
+ ]),
301
+ limit: Schema.optionalKey(Schema.Int)
302
+ };
303
+ /**
304
+ * A resource of a host and how much of it is in use, counted as a refusal counts it, so it
305
+ * needn't match the host's own machines. When a machine the sum counts couldn't be read, the
306
+ * sum would be a guess, so the row names those machines instead.
307
+ *
308
+ * `cpu` and `ramMib` are what the host's running machines were given, `runningVms` Apple's two
309
+ * VMs per Mac, and `activeSandboxes` and the `starts` of each rolling window the boat account's.
310
+ * `limit` is there only where the host refuses an action that would pass it.
311
+ */
312
+ const Usage = Schema.Union([Schema.Struct({
313
+ ...usageFields,
314
+ used: Schema.Int
315
+ }), Schema.Struct({
316
+ ...usageFields,
317
+ unread: Schema.Array(Id)
318
+ })]).annotate({ identifier: "Usage" });
319
+ /** Nothing logs the script: it can carry secrets. */
320
+ const Setup = Schema.Struct({
321
+ script: Schema.String,
322
+ timeoutSeconds: Schema.optionalKey(Size)
323
+ });
324
+ const specFields = {
325
+ base: Schema.String,
326
+ cpu: Size,
327
+ ramMib: Size,
328
+ diskGib: Size,
329
+ setup: Schema.optionalKey(Setup),
330
+ profile: Schema.optionalKey(Schema.String)
331
+ };
332
+ /** A profile file fills these in; the host sees only the `profile` label. */
333
+ const MachineSpec = Schema.Struct(specFields);
334
+ /** The host checks that `id` names it, so a create never lands under another host's ID. */
335
+ const CreateRequest = Schema.Struct({
336
+ id: Id,
337
+ ...specFields
338
+ });
339
+ //#endregion
340
+ //#region src/api.ts
341
+ /** A host bounds its own reads of runtime state under it, so a list answers in time. */
342
+ const readTimeout = Duration.seconds(10);
343
+ const ById = Schema.Struct({ id: Id });
344
+ const FromMachine = Schema.Struct({
345
+ machine: Id,
346
+ name: Name
347
+ });
348
+ const read = {
349
+ caller: Action.Anyone,
350
+ readOnly: true
351
+ };
352
+ const write = {
353
+ caller: Action.Anyone,
354
+ readOnly: false
355
+ };
356
+ const listing = [Invalid, Internal];
357
+ const reading = [
358
+ Invalid,
359
+ NotFound,
360
+ Internal
361
+ ];
362
+ const changing = [
363
+ Invalid,
364
+ NotFound,
365
+ Conflict,
366
+ Refused,
367
+ Internal
368
+ ];
369
+ const booting = [
370
+ ...changing,
371
+ NotMade,
372
+ Missing,
373
+ WrongState,
374
+ Capacity
375
+ ];
376
+ const Http = ActionHttp.make([
377
+ Action.make("listMachines", {
378
+ ...read,
379
+ error: listing,
380
+ description: "List the host's machines.",
381
+ success: Schema.Array(Machine)
382
+ }),
383
+ Action.make("getMachine", {
384
+ ...read,
385
+ error: reading,
386
+ description: "Read one machine.",
387
+ input: ById,
388
+ success: Machine
389
+ }),
390
+ Action.make("createMachine", {
391
+ ...write,
392
+ error: [
393
+ Invalid,
394
+ Conflict,
395
+ NotOffered,
396
+ Unsizable,
397
+ ScriptFailed,
398
+ Refused,
399
+ Capacity,
400
+ Internal
401
+ ],
402
+ description: "Create a machine from a base, run its setup once, and prepare it.",
403
+ input: CreateRequest,
404
+ success: Machine
405
+ }),
406
+ Action.make("startMachine", {
407
+ ...write,
408
+ error: [...booting, ScriptFailed],
409
+ description: "Start a machine. On a running machine, run preparation again.",
410
+ input: ById,
411
+ success: Machine
412
+ }),
413
+ Action.make("stopMachine", {
414
+ ...write,
415
+ error: changing,
416
+ description: "Stop a machine: a power-off that keeps its disk and ends its processes. A stopped machine stays stopped, and the stop is still its last action.",
417
+ input: ById,
418
+ success: Machine
419
+ }),
420
+ Action.make("deleteMachine", {
421
+ ...write,
422
+ error: changing,
423
+ description: "Delete a machine and everything native it owns.",
424
+ input: ById
425
+ }),
426
+ Action.make("forkMachine", {
427
+ ...write,
428
+ error: [
429
+ ...booting,
430
+ Unsizable,
431
+ ScriptFailed
432
+ ],
433
+ description: "Copy a machine to a new name on the same host.",
434
+ input: FromMachine,
435
+ success: Machine
436
+ }),
437
+ Action.make("restoreMachine", {
438
+ ...write,
439
+ error: [
440
+ ...changing,
441
+ NotReady,
442
+ PinMismatch,
443
+ Unsizable,
444
+ ScriptFailed,
445
+ Capacity
446
+ ],
447
+ description: "Create a machine on the checkpoint's host from that checkpoint.",
448
+ input: Schema.Struct({
449
+ checkpoint: Id,
450
+ name: Name
451
+ }),
452
+ success: Machine
453
+ }),
454
+ Action.make("listCheckpoints", {
455
+ ...read,
456
+ error: listing,
457
+ description: "List the host's checkpoints.",
458
+ success: Schema.Array(Checkpoint)
459
+ }),
460
+ Action.make("getCheckpoint", {
461
+ ...read,
462
+ error: reading,
463
+ description: "Read one checkpoint.",
464
+ input: ById,
465
+ success: Checkpoint
466
+ }),
467
+ Action.make("captureCheckpoint", {
468
+ ...write,
469
+ error: booting,
470
+ description: "Capture a checkpoint of a machine. Its kind follows the runtime: ram on smolvm, which captures only a running machine, and disk on Tart and boat.",
471
+ input: FromMachine,
472
+ success: Checkpoint
473
+ }),
474
+ Action.make("deleteCheckpoint", {
475
+ ...write,
476
+ error: changing,
477
+ description: "Delete a checkpoint.",
478
+ input: ById
479
+ }),
480
+ Action.make("getHost", {
481
+ ...read,
482
+ error: [Invalid],
483
+ description: "Read the host: its runtime, versions and bases.",
484
+ success: Host
485
+ }),
486
+ Action.make("getCapacity", {
487
+ ...read,
488
+ error: [
489
+ Invalid,
490
+ Refused,
491
+ Internal
492
+ ],
493
+ description: "Read the host's resources, how much of each is in use, and their limits. A machine whose state can't be read leaves the rows that count it unread; it fails when the runtime can't be read at all.",
494
+ success: Schema.Array(Usage)
495
+ })
496
+ ], { prefix: "/api" });
497
+ const actionsByName = Action.byName(Http.actions);
498
+ //#endregion
499
+ //#region src/release.ts
500
+ /** Also the version of the clankerbox binaries it ships with. */
501
+ const version = "0.18.0";
502
+ /** major.minor: releases that share it speak the same API. */
503
+ const release = (of) => of.split(".").slice(0, 2).join(".");
504
+ /** A host refuses a request whose `release` differs from its own, or that has none. */
505
+ const versionHeader = "clankerbox-version";
506
+ //#endregion
507
+ //#region src/describe.ts
508
+ const jsonPath = (path, names) => path.map((key, index) => {
509
+ if (Number.isInteger(key)) return `[${String(key)}]`;
510
+ return index === 0 ? names?.get(key) ?? String(key) : `.${String(key)}`;
511
+ }).join("");
512
+ const flatten = SchemaIssue.makeFormatterStandardSchemaV1({ leafHook: (issue) => Match.value(issue).pipe(Match.tag("MissingKey", () => "missing"), Match.tag("UnexpectedKey", () => "not a known field"), Match.orElse(SchemaIssue.defaultLeafHook)) });
513
+ /** On one line: `cpu: missing; ram: not a known field`. `names` renames top-level fields. */
514
+ const describeIssues = (error, names) => flatten(error.issue).issues.map(({ path = [], message }) => {
515
+ const keys = path.map((segment) => Predicate.isPropertyKey(segment) ? segment : segment.key);
516
+ return keys.length === 0 ? message : `${jsonPath(keys, names)}: ${message}`;
517
+ }).join("; ");
518
+ /**
519
+ * Node describes a failure only in its cause, so what has no wording here reads as the system's
520
+ * own.
521
+ */
522
+ const describeFileError = ({ reason }) => {
523
+ if (Predicate.isTagged(reason, "NotFound")) return "no such file or directory";
524
+ if (Predicate.isTagged(reason, "PermissionDenied")) return "permission denied";
525
+ return reason.description ?? (reason.cause instanceof Error ? reason.cause.message : reason._tag);
526
+ };
527
+ //#endregion
528
+ //#region src/client.ts
529
+ var client_exports = /* @__PURE__ */ __exportAll({
530
+ Client: () => Client,
531
+ HostEntry: () => HostEntry,
532
+ layer: () => layer,
533
+ make: () => make
534
+ });
535
+ /** `id` is the host part of every ID the host holds. */
536
+ const HostEntry = Schema.Struct({
537
+ id: HostId,
538
+ url: Schema.String.check(Schema.makeFilter((url) => URL.canParse(url) || "a URL, such as http://linux:8484"))
539
+ });
540
+ /**
541
+ * Nothing retries: a mutation whose reply is lost or times out is `Unavailable`, and the caller
542
+ * reads the resource to see what happened.
543
+ */
544
+ var Client = class extends Context.Service()("@gjermundgaraba/clankerbox-sdk/Client") {};
545
+ const hostApi = (entry) => ActionHttp.client(Http, {
546
+ baseUrl: entry.url,
547
+ transformClient: HttpClient.mapRequest(HttpClientRequest.setHeader(versionHeader, version))
548
+ });
549
+ const causeDetail = (error) => error.cause instanceof Error ? error.cause.message : error.message;
550
+ const unanswered = (context, detail) => context.readOnly ? new Unavailable({
551
+ message: `host ${context.host.id} (${context.host.url}) didn't answer ${context.what}: ${detail}`,
552
+ readOnly: true
553
+ }) : new Unavailable({
554
+ message: `no reply from host ${context.host.id} to ${context.what} (${detail}); the action may have run: read ${context.target ?? "the resource"} to see`,
555
+ readOnly: false
556
+ });
557
+ const bounded = (call, context) => {
558
+ const { timeout } = context;
559
+ return timeout === void 0 ? call : Effect.timeoutOrElse(call, {
560
+ duration: timeout,
561
+ orElse: () => Effect.fail(unanswered(context, `timed out after ${Duration.format(timeout)}`))
562
+ });
563
+ };
564
+ const unexpected = (context, error) => new Internal({ message: `host ${context.host.id} answered ${context.what} unexpectedly: ${error.message}` });
565
+ const settle = (call, context) => bounded(call, context).pipe(Effect.catchTags({
566
+ HttpClientError: (error) => Effect.fail(Match.value(error.reason).pipe(Match.tag("TransportError", () => unanswered(context, causeDetail(error))), Match.when({
567
+ _tag: "DecodeError",
568
+ cause: (cause) => cause !== void 0 && !(cause instanceof SyntaxError)
569
+ }, () => unanswered(context, causeDetail(error))), Match.orElse(() => unexpected(context, error)))),
570
+ InvalidInput: (error) => Effect.fail(new Invalid({ message: `${context.what}: the request doesn't match the action's input: ${error.message}` })),
571
+ SchemaError: (error) => Effect.fail(new Internal({ message: `host ${context.host.id}'s reply didn't decode: ${error.message}` })),
572
+ Unauthenticated: (error) => Effect.fail(unexpected(context, error)),
573
+ Forbidden: (error) => Effect.fail(unexpected(context, error))
574
+ }));
575
+ const encodeError = Schema.encodeSync(ClankerboxError);
576
+ const decodeError = Schema.decodeSync(ClankerboxError);
577
+ const locate = ({ action, host, id, made }) => (error) => decodeError({
578
+ ...Record.filter({
579
+ action,
580
+ host,
581
+ id,
582
+ made
583
+ }, Predicate.isNotUndefined),
584
+ ...encodeError(error)
585
+ });
586
+ const checkHost = (entry, host) => host.id === entry.id ? Effect.succeed(host) : Effect.fail(new Invalid({ message: `host ${entry.id} (${entry.url}) calls itself ${host.id}; its entry in the host list must use that ID` }));
587
+ const decodeHosts = Schema.decodeUnknownEffect(Schema.Array(HostEntry));
588
+ const subjectOf = (input) => "id" in input ? input.id : "machine" in input ? input.machine : input.checkpoint;
589
+ /**
590
+ * A client over `hosts`, in placement order. It sends nothing until called. An empty list, a
591
+ * malformed entry or a host ID that appears twice is Invalid.
592
+ */
593
+ const make = (hosts, options) => Effect.gen(function* () {
594
+ if (hosts.length === 0) return yield* new Invalid({ message: "the host list is empty: a client needs a host" });
595
+ yield* decodeHosts(hosts, { errors: "all" }).pipe(Effect.mapError((error) => new Invalid({ message: `host list: ${describeIssues(error)}` })));
596
+ const twice = hosts.find(({ id }, index) => hosts.findIndex((other) => other.id === id) < index);
597
+ if (twice !== void 0) return yield* new Invalid({ message: `host ${twice.id} appears twice in the host list` });
598
+ const routed = yield* Effect.forEach(hosts, (entry) => Effect.map(hostApi(entry), (api) => ({
599
+ entry,
600
+ api
601
+ })));
602
+ const routes = new Map(routed.map((route) => [route.entry.id, route]));
603
+ const route = (host) => {
604
+ const found = routes.get(host);
605
+ return found === void 0 ? Effect.fail(new Invalid({ message: `host ${host} isn't in the host list` })) : Effect.succeed(found);
606
+ };
607
+ const send = ({ entry, api }, name, input, subject, made) => {
608
+ const { readOnly } = actionsByName[name];
609
+ const method = api[name];
610
+ return settle(method(input), {
611
+ host: entry,
612
+ timeout: readOnly ? options?.readTimeout ?? readTimeout : options?.timeout,
613
+ readOnly,
614
+ what: subject === void 0 ? name : `${name} ${subject}`,
615
+ target: made ?? subject
616
+ }).pipe(Effect.mapError(locate({
617
+ action: name,
618
+ host: entry.id,
619
+ id: subject,
620
+ made
621
+ })));
622
+ };
623
+ const naming = (name) => (effect) => Effect.mapError(effect, (error) => error.action === void 0 ? locate({ action: name })(error) : error);
624
+ const gather = (call) => Effect.forEach(routed, (route) => Effect.map(Effect.result(call(route)), (result) => ({
625
+ host: route.entry.id,
626
+ result
627
+ })), { concurrency: "unbounded" }).pipe(Effect.map((results) => {
628
+ const answers = [];
629
+ const unreachable = [];
630
+ for (const { host, result } of results) if (Result.isSuccess(result)) answers.push(...result.success);
631
+ else unreachable.push({
632
+ host,
633
+ error: result.failure
634
+ });
635
+ return {
636
+ answers,
637
+ unreachable
638
+ };
639
+ }));
640
+ const readHost = (route) => Effect.flatMap(send(route, "getHost", {}), (host) => checkHost(route.entry, host)).pipe(Effect.mapError(locate({
641
+ action: "getHost",
642
+ host: route.entry.id
643
+ })));
644
+ const onResource = (name, input, makes) => naming(name)(Effect.gen(function* () {
645
+ const subject = subjectOf(input);
646
+ const { host } = yield* parseId(subject);
647
+ const made = makes === void 0 ? void 0 : yield* formatId(host, makes);
648
+ return yield* send(yield* route(host), name, input, subject, made);
649
+ }));
650
+ const createOn = (host, name, spec) => Effect.flatMap(formatId(host, name), (id) => onResource("createMachine", {
651
+ ...spec,
652
+ id
653
+ }, name));
654
+ const offering = (base) => Effect.gen(function* () {
655
+ const asked = yield* Effect.forEach(routed, (route) => Effect.map(Effect.forkScoped(Effect.result(readHost(route))), (fiber) => ({
656
+ host: route.entry.id,
657
+ fiber
658
+ })));
659
+ const misses = [];
660
+ let noReply = false;
661
+ for (const { host, fiber } of asked) {
662
+ const result = yield* Fiber.join(fiber);
663
+ if (Result.isFailure(result)) {
664
+ noReply ||= Predicate.isTagged(result.failure, "Unavailable");
665
+ misses.push(`${host} failed: ${result.failure.message}`);
666
+ } else if (result.success.bases.includes(base)) return result.success;
667
+ else {
668
+ const { bases } = result.success;
669
+ misses.push(`${host} offers ${bases.length > 0 ? bases.join(", ") : "no bases"}`);
670
+ }
671
+ }
672
+ const message = `no host offers base ${base}: ${misses.join("; ")}`;
673
+ return yield* noReply ? new Unavailable({
674
+ message,
675
+ readOnly: true
676
+ }) : new NotOffered({
677
+ message,
678
+ base
679
+ });
680
+ }).pipe(Effect.scoped);
681
+ const create = (name, spec, options) => Effect.gen(function* () {
682
+ yield* parseName(name);
683
+ if (options?.host !== void 0) return yield* createOn(options.host, name, spec);
684
+ return yield* createOn((yield* offering(spec.base)).id, name, spec);
685
+ });
686
+ const place = (base, options) => options?.host === void 0 ? offering(base) : Effect.flatMap(route(options.host), readHost);
687
+ const deleting = (removal) => removal.pipe(Effect.as("deleted"), Effect.catchTag("NotFound", () => Effect.succeed("absent")));
688
+ return {
689
+ hosts: gather((route) => Effect.map(readHost(route), (host) => [host])),
690
+ machines: gather((route) => send(route, "listMachines", {})),
691
+ checkpoints: gather((route) => send(route, "listCheckpoints", {})),
692
+ capacity: gather((route) => send(route, "getCapacity", {})),
693
+ place: (base, options) => naming("getHost")(place(base, options)),
694
+ create: (name, spec, options) => naming("createMachine")(create(name, spec, options)),
695
+ getMachine: (input) => onResource("getMachine", input),
696
+ startMachine: (input) => onResource("startMachine", input),
697
+ stopMachine: (input) => onResource("stopMachine", input),
698
+ deleteMachine: (input) => deleting(onResource("deleteMachine", input)),
699
+ forkMachine: (input) => onResource("forkMachine", input, input.name),
700
+ restoreMachine: (input) => onResource("restoreMachine", input, input.name),
701
+ getCheckpoint: (input) => onResource("getCheckpoint", input),
702
+ captureCheckpoint: (input) => onResource("captureCheckpoint", input, input.name),
703
+ deleteCheckpoint: (input) => deleting(onResource("deleteCheckpoint", input))
704
+ };
705
+ });
706
+ const layer = (hosts, options) => Layer.effect(Client, make(hosts, options));
707
+ //#endregion
708
+ export { parseId as A, NotFound as B, Usage as C, Id as D, HostId as E, ConflictKind as F, Refused as G, NotOffered as H, HostError as I, Unsizable as J, ScriptFailed as K, Internal as L, Capacity as M, ClankerboxError as N, Name as O, Conflict as P, Invalid as R, SshLogin as S, stateLabel as T, NotReady as U, NotMade as V, PinMismatch as W, hostErrors as X, WrongState as Y, retryable as Z, Machine as _, release as a, Setup as b, Http as c, ActionName as d, ActionRecord as f, Host as g, CreateRequest as h, describeIssues as i, parseName as j, formatId as k, actionsByName as l, Checkpoint as m, layer as n, version as o, ActionStatus as p, Unavailable as q, describeFileError as r, versionHeader as s, client_exports as t, readTimeout as u, MachineSpec as v, isReady as w, SshEndpoint as x, Runtime as y, Missing as z };