@gigadrive/sdk 0.9.4 → 0.10.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 +52 -0
- package/dist/index.d.mts +1262 -4
- package/dist/index.d.ts +1262 -4
- package/dist/index.js +1549 -87
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1525 -87
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -466,6 +466,16 @@ var HttpClient = class {
|
|
|
466
466
|
}
|
|
467
467
|
return response;
|
|
468
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Fetch an arbitrary URL without authentication and without throwing on a
|
|
471
|
+
* non-2xx status, using the client's configured `fetch`. Used to probe
|
|
472
|
+
* public URLs such as microVM preview URLs.
|
|
473
|
+
*
|
|
474
|
+
* @internal
|
|
475
|
+
*/
|
|
476
|
+
async fetchPublic(url, init) {
|
|
477
|
+
return this.fetchFn(url, init);
|
|
478
|
+
}
|
|
469
479
|
async request(method, path, options) {
|
|
470
480
|
const response = await this.fetchWithAuth(method, path, options);
|
|
471
481
|
return this.handleResponse(response);
|
|
@@ -490,8 +500,9 @@ var HttpClient = class {
|
|
|
490
500
|
}
|
|
491
501
|
}
|
|
492
502
|
const signal = options?.signal;
|
|
493
|
-
const response = await this.fetchFn(url, { method, headers, body: requestBody, signal });
|
|
494
503
|
const bodyIsOneShot = typeof ReadableStream !== "undefined" && requestBody instanceof ReadableStream;
|
|
504
|
+
const init = bodyIsOneShot ? { method, headers, body: requestBody, signal, duplex: "half" } : { method, headers, body: requestBody, signal };
|
|
505
|
+
const response = await this.fetchFn(url, init);
|
|
495
506
|
if (response.status === 401 && !bodyIsOneShot) {
|
|
496
507
|
this.tokenManager.invalidate();
|
|
497
508
|
const retryToken = await this.tokenManager.getToken();
|
|
@@ -552,12 +563,265 @@ var HttpClient = class {
|
|
|
552
563
|
}
|
|
553
564
|
};
|
|
554
565
|
|
|
566
|
+
// src/resources/base-resource.ts
|
|
567
|
+
var BaseResource = class {
|
|
568
|
+
constructor(httpClient) {
|
|
569
|
+
this.httpClient = httpClient;
|
|
570
|
+
}
|
|
571
|
+
httpClient;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// src/microvms/duration.ts
|
|
575
|
+
var PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)$/;
|
|
576
|
+
var UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
|
|
577
|
+
var durationToMs = (value) => {
|
|
578
|
+
let ms;
|
|
579
|
+
if (typeof value === "number") {
|
|
580
|
+
ms = value;
|
|
581
|
+
} else {
|
|
582
|
+
const match = PATTERN.exec(value.trim());
|
|
583
|
+
if (!match)
|
|
584
|
+
throw new RangeError(`Invalid duration "${value}". Use milliseconds or a string such as 30s, 10m or 2h.`);
|
|
585
|
+
ms = Number(match[1]) * UNIT_MS[match[2]];
|
|
586
|
+
}
|
|
587
|
+
if (!Number.isFinite(ms) || ms < 0) throw new RangeError(`Invalid duration "${String(value)}"`);
|
|
588
|
+
return Math.ceil(ms);
|
|
589
|
+
};
|
|
590
|
+
var MICROVM_MAX_TIMEOUT_MS = 24 * 36e5;
|
|
591
|
+
var microvmTimeoutMs = (value) => {
|
|
592
|
+
const ms = durationToMs(value);
|
|
593
|
+
if (ms < 1 || ms > MICROVM_MAX_TIMEOUT_MS) {
|
|
594
|
+
throw new RangeError(`A microVM timeout must be between 1 ms and 24 hours (got ${String(value)}).`);
|
|
595
|
+
}
|
|
596
|
+
return ms;
|
|
597
|
+
};
|
|
598
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
599
|
+
if (signal?.aborted) {
|
|
600
|
+
reject(abortReason(signal));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const onAbort = () => {
|
|
604
|
+
clearTimeout(timer);
|
|
605
|
+
reject(abortReason(signal));
|
|
606
|
+
};
|
|
607
|
+
const timer = setTimeout(() => {
|
|
608
|
+
signal?.removeEventListener("abort", onAbort);
|
|
609
|
+
resolve();
|
|
610
|
+
}, ms);
|
|
611
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
612
|
+
});
|
|
613
|
+
var abortReason = (signal) => {
|
|
614
|
+
const reason = signal?.reason;
|
|
615
|
+
if (reason instanceof Error) return reason;
|
|
616
|
+
const error = new Error(typeof reason === "string" ? reason : "The operation was aborted");
|
|
617
|
+
error.name = "AbortError";
|
|
618
|
+
return error;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// src/microvms/errors.ts
|
|
622
|
+
var MicroVmNotRunningError = class extends ApiError {
|
|
623
|
+
constructor(message = "The microVM is not running", code = "microvm_not_running", reason) {
|
|
624
|
+
super(message, 409, code, reason);
|
|
625
|
+
this.name = "MicroVmNotRunningError";
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
var MicroVmNotFoundError = class extends ApiError {
|
|
629
|
+
constructor(message = "Not found", code, reason) {
|
|
630
|
+
super(message, 404, code, reason);
|
|
631
|
+
this.name = "MicroVmNotFoundError";
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
var MicroVmFileNotFoundError = class extends MicroVmNotFoundError {
|
|
635
|
+
/** The path that was not found. */
|
|
636
|
+
path;
|
|
637
|
+
constructor(path, message = `No such file or directory: ${path}`, code) {
|
|
638
|
+
super(message, code);
|
|
639
|
+
this.name = "MicroVmFileNotFoundError";
|
|
640
|
+
this.path = path;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
var MicroVmBusyError = class extends ApiError {
|
|
644
|
+
constructor(message, status, code = "microvm_busy", reason) {
|
|
645
|
+
super(message, status, code, reason);
|
|
646
|
+
this.name = "MicroVmBusyError";
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
var MicroVmUnreachableError = class extends ApiError {
|
|
650
|
+
constructor(message, code = "microvm_unreachable", reason) {
|
|
651
|
+
super(message, 502, code, reason);
|
|
652
|
+
this.name = "MicroVmUnreachableError";
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
var MicroVmImageNotReadyError = class extends ApiError {
|
|
656
|
+
constructor(message, code = "image_not_ready", reason) {
|
|
657
|
+
super(message, 409, code, reason);
|
|
658
|
+
this.name = "MicroVmImageNotReadyError";
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
var MicroVmQuotaExceededError = class extends ApiError {
|
|
662
|
+
constructor(message, status, code, reason) {
|
|
663
|
+
super(message, status, code, reason);
|
|
664
|
+
this.name = "MicroVmQuotaExceededError";
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
var CommandFailedError = class _CommandFailedError extends GigadriveError {
|
|
668
|
+
commandId;
|
|
669
|
+
/** Exit code, or `null` when a signal killed the process. */
|
|
670
|
+
exitCode;
|
|
671
|
+
signal;
|
|
672
|
+
stdout;
|
|
673
|
+
stderr;
|
|
674
|
+
constructor(output, label = "Command") {
|
|
675
|
+
const how = output.exitCode !== null ? `exited with code ${output.exitCode}` : `was killed by ${output.signal ?? "a signal"}`;
|
|
676
|
+
const tail = output.stderr.trim().split("\n").slice(-5).join("\n");
|
|
677
|
+
super(tail ? `${label} ${how}:
|
|
678
|
+
${tail}` : `${label} ${how}`);
|
|
679
|
+
this.name = "CommandFailedError";
|
|
680
|
+
this.commandId = output.commandId;
|
|
681
|
+
this.exitCode = output.exitCode;
|
|
682
|
+
this.signal = output.signal;
|
|
683
|
+
this.stdout = output.stdout;
|
|
684
|
+
this.stderr = output.stderr;
|
|
685
|
+
}
|
|
686
|
+
/** Builds the error from a finished command whose output was not collected. */
|
|
687
|
+
static fromInfo(info) {
|
|
688
|
+
return new _CommandFailedError({
|
|
689
|
+
commandId: info.id,
|
|
690
|
+
exitCode: info.exitCode,
|
|
691
|
+
signal: info.signal,
|
|
692
|
+
stdout: "",
|
|
693
|
+
stderr: "",
|
|
694
|
+
durationMs: 0
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
var MicroVmTimeoutError = class extends GigadriveError {
|
|
699
|
+
constructor(message) {
|
|
700
|
+
super(message);
|
|
701
|
+
this.name = "MicroVmTimeoutError";
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
var MicroVmPortNotExposedError = class extends GigadriveError {
|
|
705
|
+
port;
|
|
706
|
+
constructor(port, exposed) {
|
|
707
|
+
super(
|
|
708
|
+
exposed.length === 0 ? `Port ${port} is not exposed: this microVM exposes no ports. Pass \`ports: [${port}]\` when creating it.` : `Port ${port} is not exposed. Exposed ports: ${exposed.join(", ")}.`
|
|
709
|
+
);
|
|
710
|
+
this.name = "MicroVmPortNotExposedError";
|
|
711
|
+
this.port = port;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
var MicroVmStartError = class extends GigadriveError {
|
|
715
|
+
microvmId;
|
|
716
|
+
status;
|
|
717
|
+
stopReason;
|
|
718
|
+
constructor(microvmId, status, stopReason) {
|
|
719
|
+
super(`The microVM ${microvmId} is ${status}${stopReason ? ` (${stopReason})` : ""}`);
|
|
720
|
+
this.name = "MicroVmStartError";
|
|
721
|
+
this.microvmId = microvmId;
|
|
722
|
+
this.status = status;
|
|
723
|
+
this.stopReason = stopReason;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
var MicroVmCleanupError = class extends GigadriveError {
|
|
727
|
+
microvmId;
|
|
728
|
+
/** Why deleting the microVM failed. {@link Error.cause} is why creating it failed. */
|
|
729
|
+
cleanupError;
|
|
730
|
+
constructor(microvmId, cause, cleanupError) {
|
|
731
|
+
super(
|
|
732
|
+
`Creating the microVM ${microvmId} failed and deleting it failed too. Delete it with MicroVM.get('${microvmId}') and delete().`
|
|
733
|
+
);
|
|
734
|
+
this.name = "MicroVmCleanupError";
|
|
735
|
+
this.microvmId = microvmId;
|
|
736
|
+
this.cleanupError = cleanupError;
|
|
737
|
+
this.cause = cause;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
var MicroVmImageBuildError = class extends GigadriveError {
|
|
741
|
+
imageId;
|
|
742
|
+
constructor(imageId, message) {
|
|
743
|
+
super(`The image build failed${message ? `: ${message}` : ""}`);
|
|
744
|
+
this.name = "MicroVmImageBuildError";
|
|
745
|
+
this.imageId = imageId;
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
var NOT_RUNNING = /microvm[_ ]not[_ ]running|not running/i;
|
|
749
|
+
var toMicroVmError = (error, filePath) => {
|
|
750
|
+
if (!(error instanceof ApiError) || Object.getPrototypeOf(error) !== ApiError.prototype) return error;
|
|
751
|
+
const { message, status, code, reason } = error;
|
|
752
|
+
if (code === "microvm_busy") return new MicroVmBusyError(message, status, code, reason);
|
|
753
|
+
if (code === "microvm_unreachable") return new MicroVmUnreachableError(message, code, reason);
|
|
754
|
+
if (code === "image_not_ready") return new MicroVmImageNotReadyError(message, code, reason);
|
|
755
|
+
if (code === "quota_exceeded" || code === "plan_limit_exceeded") {
|
|
756
|
+
return new MicroVmQuotaExceededError(message, status, code, reason);
|
|
757
|
+
}
|
|
758
|
+
if (status === 409 && (code === "microvm_not_running" || code === void 0 && NOT_RUNNING.test(message))) {
|
|
759
|
+
return new MicroVmNotRunningError(message, code ?? "microvm_not_running", reason);
|
|
760
|
+
}
|
|
761
|
+
if (status === 404) {
|
|
762
|
+
const vmMissing = code === "not_found" || code === "microvm_not_found";
|
|
763
|
+
return filePath !== void 0 && !vmMissing ? new MicroVmFileNotFoundError(filePath, message, code) : new MicroVmNotFoundError(message, code, reason);
|
|
764
|
+
}
|
|
765
|
+
return error;
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// src/queues/encoding.ts
|
|
769
|
+
var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
|
|
770
|
+
var UNIT_SECONDS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
|
|
771
|
+
var durationToSeconds = (value) => {
|
|
772
|
+
let seconds;
|
|
773
|
+
if (typeof value === "number") {
|
|
774
|
+
seconds = value;
|
|
775
|
+
} else {
|
|
776
|
+
const match = DURATION_PATTERN.exec(value.trim());
|
|
777
|
+
if (!match) throw new Error(`Invalid duration "${value}". Use seconds or a string such as 30s, 10m, 12h or 7d.`);
|
|
778
|
+
seconds = Number(match[1]) * UNIT_SECONDS[match[2]];
|
|
779
|
+
}
|
|
780
|
+
if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid duration "${String(value)}"`);
|
|
781
|
+
return Math.ceil(seconds);
|
|
782
|
+
};
|
|
783
|
+
var textEncoder = new TextEncoder();
|
|
784
|
+
var textDecoder = new TextDecoder();
|
|
785
|
+
var utf8Encode = (value) => textEncoder.encode(value);
|
|
786
|
+
var utf8Decode = (bytes) => textDecoder.decode(bytes);
|
|
787
|
+
var bytesToBase64 = (bytes) => {
|
|
788
|
+
let binary = "";
|
|
789
|
+
for (let index = 0; index < bytes.length; index += 32768) {
|
|
790
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
|
|
791
|
+
}
|
|
792
|
+
return btoa(binary);
|
|
793
|
+
};
|
|
794
|
+
var base64ToBytes = (value) => {
|
|
795
|
+
const binary = atob(value);
|
|
796
|
+
const bytes = new Uint8Array(binary.length);
|
|
797
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
798
|
+
return bytes;
|
|
799
|
+
};
|
|
800
|
+
var toHex = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
801
|
+
var toArrayBuffer = (bytes) => {
|
|
802
|
+
const copy = new ArrayBuffer(bytes.byteLength);
|
|
803
|
+
new Uint8Array(copy).set(bytes);
|
|
804
|
+
return copy;
|
|
805
|
+
};
|
|
806
|
+
|
|
555
807
|
// src/streaming.ts
|
|
556
|
-
var
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
808
|
+
var parseBlock = (rawEvent) => {
|
|
809
|
+
let event = "message";
|
|
810
|
+
let id;
|
|
811
|
+
let retry;
|
|
812
|
+
const data = [];
|
|
813
|
+
for (const line of rawEvent.split("\n")) {
|
|
814
|
+
if (line === "" || line.startsWith(":")) continue;
|
|
815
|
+
const colon = line.indexOf(":");
|
|
816
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
817
|
+
const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
818
|
+
if (field === "data") data.push(value);
|
|
819
|
+
else if (field === "event") event = value || "message";
|
|
820
|
+
else if (field === "id" && !value.includes("\0")) id = value;
|
|
821
|
+
else if (field === "retry" && /^\d+$/.test(value)) retry = Number(value);
|
|
822
|
+
}
|
|
823
|
+
if (data.length === 0) return null;
|
|
824
|
+
return { event, data: data.join("\n"), id, retry };
|
|
561
825
|
};
|
|
562
826
|
var parseEvent = (data) => {
|
|
563
827
|
try {
|
|
@@ -567,7 +831,7 @@ var parseEvent = (data) => {
|
|
|
567
831
|
throw new Error(`Failed to parse streamed SSE data as JSON: ${message}`);
|
|
568
832
|
}
|
|
569
833
|
};
|
|
570
|
-
async function*
|
|
834
|
+
async function* readEventBlocks(response) {
|
|
571
835
|
const body = response.body;
|
|
572
836
|
if (!body) {
|
|
573
837
|
throw new Error("The response has no readable body to stream.");
|
|
@@ -575,44 +839,546 @@ async function* parseSSEStream(response) {
|
|
|
575
839
|
const reader = body.getReader();
|
|
576
840
|
const decoder = new TextDecoder();
|
|
577
841
|
let buffer = "";
|
|
578
|
-
const drain = function* (flush) {
|
|
579
|
-
let separator = buffer.indexOf("\n\n");
|
|
580
|
-
while (separator !== -1) {
|
|
581
|
-
const rawEvent = buffer.slice(0, separator);
|
|
582
|
-
buffer = buffer.slice(separator + 2);
|
|
583
|
-
const data = extractData(rawEvent);
|
|
584
|
-
if (data !== null && data !== "[DONE]") {
|
|
585
|
-
yield parseEvent(data);
|
|
586
|
-
}
|
|
587
|
-
separator = buffer.indexOf("\n\n");
|
|
588
|
-
}
|
|
589
|
-
if (flush) {
|
|
590
|
-
const data = extractData(buffer);
|
|
591
|
-
buffer = "";
|
|
592
|
-
if (data !== null && data !== "[DONE]") {
|
|
593
|
-
yield parseEvent(data);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
};
|
|
597
842
|
try {
|
|
598
843
|
for (; ; ) {
|
|
599
844
|
const { done, value } = await reader.read();
|
|
600
845
|
if (done) break;
|
|
601
846
|
buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
|
|
602
|
-
|
|
847
|
+
let separator = buffer.indexOf("\n\n");
|
|
848
|
+
while (separator !== -1) {
|
|
849
|
+
yield buffer.slice(0, separator);
|
|
850
|
+
buffer = buffer.slice(separator + 2);
|
|
851
|
+
separator = buffer.indexOf("\n\n");
|
|
852
|
+
}
|
|
603
853
|
}
|
|
604
|
-
|
|
854
|
+
buffer += decoder.decode();
|
|
855
|
+
if (buffer.length > 0) yield buffer;
|
|
605
856
|
} finally {
|
|
606
857
|
await reader.cancel().catch(() => void 0);
|
|
607
858
|
}
|
|
608
859
|
}
|
|
860
|
+
async function* parseSSEEvents(response) {
|
|
861
|
+
for await (const block of readEventBlocks(response)) {
|
|
862
|
+
const event = parseBlock(block);
|
|
863
|
+
if (event) yield event;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
async function* parseSSEStream(response) {
|
|
867
|
+
for await (const event of parseSSEEvents(response)) {
|
|
868
|
+
if (event.data.trim().length === 0 || event.data === "[DONE]") continue;
|
|
869
|
+
yield parseEvent(event.data);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
609
872
|
|
|
610
|
-
// src/
|
|
611
|
-
var
|
|
612
|
-
|
|
613
|
-
|
|
873
|
+
// src/microvms/logs.ts
|
|
874
|
+
var RECONNECT_DELAYS_MS = [100, 250, 500, 1e3, 2e3];
|
|
875
|
+
var isRetryable = (error) => {
|
|
876
|
+
if (error instanceof ApiError) return error.status === 408 || error.status === 429 || error.status >= 500;
|
|
877
|
+
if (error instanceof Error && error.name === "AbortError") return false;
|
|
878
|
+
return true;
|
|
879
|
+
};
|
|
880
|
+
var parseJson = (data) => {
|
|
881
|
+
try {
|
|
882
|
+
const value = JSON.parse(data);
|
|
883
|
+
return value !== null && typeof value === "object" ? value : null;
|
|
884
|
+
} catch {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
async function* streamCommandLogs(open, options = {}) {
|
|
889
|
+
const follow = options.follow ?? true;
|
|
890
|
+
const reconnect = options.reconnect ?? true;
|
|
891
|
+
const maxAttempts = options.maxReconnectAttempts ?? 5;
|
|
892
|
+
const next = {
|
|
893
|
+
stdout: options.stdoutOffset ?? 0,
|
|
894
|
+
stderr: options.stderrOffset ?? 0
|
|
895
|
+
};
|
|
896
|
+
let failures = 0;
|
|
897
|
+
for (; ; ) {
|
|
898
|
+
let progressed = false;
|
|
899
|
+
let serverError;
|
|
900
|
+
try {
|
|
901
|
+
const response = await open({
|
|
902
|
+
follow,
|
|
903
|
+
stdoutOffset: next.stdout,
|
|
904
|
+
stderrOffset: next.stderr,
|
|
905
|
+
signal: options.signal
|
|
906
|
+
});
|
|
907
|
+
for await (const event of parseSSEEvents(response)) {
|
|
908
|
+
if (event.event === "stdout" || event.event === "stderr") {
|
|
909
|
+
const payload = parseJson(event.data);
|
|
910
|
+
if (!payload || typeof payload.data !== "string") continue;
|
|
911
|
+
const stream = event.event;
|
|
912
|
+
let data = base64ToBytes(payload.data);
|
|
913
|
+
let offset = typeof payload.offset === "number" ? payload.offset : next[stream];
|
|
914
|
+
progressed = true;
|
|
915
|
+
if (offset < next[stream]) {
|
|
916
|
+
const skip = next[stream] - offset;
|
|
917
|
+
if (skip >= data.length) continue;
|
|
918
|
+
data = data.subarray(skip);
|
|
919
|
+
offset = next[stream];
|
|
920
|
+
}
|
|
921
|
+
next[stream] = offset + data.length;
|
|
922
|
+
if (data.length > 0) yield { type: "output", stream, data, offset };
|
|
923
|
+
} else if (event.event === "truncated") {
|
|
924
|
+
progressed = true;
|
|
925
|
+
yield { type: "truncated" };
|
|
926
|
+
} else if (event.event === "end") {
|
|
927
|
+
const payload = parseJson(event.data);
|
|
928
|
+
yield { type: "end", reason: typeof payload?.reason === "string" ? payload.reason : "snapshot" };
|
|
929
|
+
return;
|
|
930
|
+
} else if (event.event === "exit") {
|
|
931
|
+
const payload = parseJson(event.data) ?? {};
|
|
932
|
+
yield {
|
|
933
|
+
type: "exit",
|
|
934
|
+
exitCode: typeof payload.exitCode === "number" ? payload.exitCode : null,
|
|
935
|
+
signal: typeof payload.signal === "string" ? payload.signal : null,
|
|
936
|
+
...typeof payload.timedOut === "boolean" && { timedOut: payload.timedOut },
|
|
937
|
+
...(typeof payload.finishedAt === "string" || payload.finishedAt === null) && {
|
|
938
|
+
finishedAt: payload.finishedAt
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
return;
|
|
942
|
+
} else if (event.event === "error") {
|
|
943
|
+
const payload = parseJson(event.data);
|
|
944
|
+
const message = typeof payload?.message === "string" ? payload.message : typeof payload?.error === "string" ? payload.error : event.data;
|
|
945
|
+
const failure = new ApiError(message, 502, typeof payload?.code === "string" ? payload.code : void 0);
|
|
946
|
+
if (payload?.retryable !== true) serverError = failure;
|
|
947
|
+
throw failure;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (!follow || !reconnect) return;
|
|
951
|
+
const exit = progressed ? null : await options.resolveExit?.();
|
|
952
|
+
if (exit) {
|
|
953
|
+
yield { type: "exit", exitCode: exit.exitCode, signal: exit.signal };
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
} catch (error) {
|
|
957
|
+
if (error === serverError || options.signal?.aborted || !reconnect || !isRetryable(error)) throw error;
|
|
958
|
+
if (!progressed && failures >= maxAttempts) throw error;
|
|
959
|
+
}
|
|
960
|
+
failures = progressed ? 0 : failures + 1;
|
|
961
|
+
if (failures > maxAttempts) {
|
|
962
|
+
throw new ApiError("The command log stream kept closing without output", 502, "log_stream_closed");
|
|
963
|
+
}
|
|
964
|
+
await sleep(RECONNECT_DELAYS_MS[Math.min(failures, RECONNECT_DELAYS_MS.length - 1)], options.signal);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/microvms/resource.ts
|
|
969
|
+
var vmPath = (microvmId) => `/microvms/${encodeURIComponent(microvmId)}`;
|
|
970
|
+
var commandPath = (microvmId, commandId) => `${vmPath(microvmId)}/commands/${encodeURIComponent(commandId)}`;
|
|
971
|
+
var typed = (promise, filePath) => promise.catch((error) => {
|
|
972
|
+
throw toMicroVmError(error, filePath);
|
|
973
|
+
});
|
|
974
|
+
var BUSY_RETRY_DELAYS_MS = [250, 500, 1e3, 2e3, 4e3];
|
|
975
|
+
var read = async (run, signal, filePath) => {
|
|
976
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
977
|
+
try {
|
|
978
|
+
return await typed(run(), filePath);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
if (!(error instanceof MicroVmBusyError) || attempt >= BUSY_RETRY_DELAYS_MS.length || signal?.aborted) {
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
983
|
+
await sleep(BUSY_RETRY_DELAYS_MS[attempt], signal);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
var unwrapList = (body, key = "data") => {
|
|
988
|
+
if (Array.isArray(body)) return body;
|
|
989
|
+
if (body && typeof body === "object") {
|
|
990
|
+
const record = body;
|
|
991
|
+
const list = record[key] ?? record.data ?? record.items;
|
|
992
|
+
if (Array.isArray(list)) return list;
|
|
993
|
+
}
|
|
994
|
+
return [];
|
|
995
|
+
};
|
|
996
|
+
var normalizeCommand = (raw) => {
|
|
997
|
+
const withId = raw.id === void 0 && raw.commandId !== void 0 ? { ...raw, id: raw.commandId } : raw;
|
|
998
|
+
return typeof withId.timedOut === "boolean" ? withId : { ...withId, timedOut: false };
|
|
999
|
+
};
|
|
1000
|
+
var MicroVmCommandsResource = class extends BaseResource {
|
|
1001
|
+
/**
|
|
1002
|
+
* Run a command and wait for it to exit (`POST /microvms/:id/commands`, `wait: true`).
|
|
1003
|
+
*
|
|
1004
|
+
* @example
|
|
1005
|
+
* ```ts
|
|
1006
|
+
* const { exitCode, stdout } = await client.microvms.commands.run(id, { cmd: 'node', args: ['-v'] });
|
|
1007
|
+
* ```
|
|
1008
|
+
*/
|
|
1009
|
+
run(microvmId, input, options = {}) {
|
|
1010
|
+
return typed(
|
|
1011
|
+
this.httpClient.post(`${vmPath(microvmId)}/commands`, { ...input, wait: true }, options)
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Start a detached command and return at once (`POST /microvms/:id/commands`, `wait: false`).
|
|
1016
|
+
* Follow its output with {@link logs}.
|
|
1017
|
+
*/
|
|
1018
|
+
async start(microvmId, input, options = {}) {
|
|
1019
|
+
const raw = await typed(
|
|
1020
|
+
this.httpClient.post(`${vmPath(microvmId)}/commands`, { ...input, wait: false }, options)
|
|
1021
|
+
);
|
|
1022
|
+
return normalizeCommand(raw);
|
|
1023
|
+
}
|
|
1024
|
+
/** List the microVM's commands, newest first (`GET /microvms/:id/commands`). */
|
|
1025
|
+
async list(microvmId, options = {}) {
|
|
1026
|
+
const body = await read(
|
|
1027
|
+
() => this.httpClient.get(`${vmPath(microvmId)}/commands`, options),
|
|
1028
|
+
options.signal
|
|
1029
|
+
);
|
|
1030
|
+
return unwrapList(body).map(normalizeCommand);
|
|
1031
|
+
}
|
|
1032
|
+
/** Get one command (`GET /microvms/:id/commands/:cmdId`). */
|
|
1033
|
+
async get(microvmId, commandId, options = {}) {
|
|
1034
|
+
return normalizeCommand(
|
|
1035
|
+
await read(() => this.httpClient.get(commandPath(microvmId, commandId), options), options.signal)
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Stream a command's output (`GET /microvms/:id/commands/:cmdId/logs`, SSE).
|
|
1040
|
+
*
|
|
1041
|
+
* Yields `output` events with the raw bytes and a final `exit` event. A
|
|
1042
|
+
* dropped connection is reopened from the last received offsets, so no byte
|
|
1043
|
+
* is lost or repeated; pass `reconnect: false` to disable that.
|
|
1044
|
+
*
|
|
1045
|
+
* @example
|
|
1046
|
+
* ```ts
|
|
1047
|
+
* for await (const event of client.microvms.commands.logs(id, commandId)) {
|
|
1048
|
+
* if (event.type === 'output') process.stdout.write(event.data);
|
|
1049
|
+
* else console.log('exit', event.exitCode);
|
|
1050
|
+
* }
|
|
1051
|
+
* ```
|
|
1052
|
+
*/
|
|
1053
|
+
logs(microvmId, commandId, options = {}) {
|
|
1054
|
+
return streamCommandLogs(
|
|
1055
|
+
({ follow, stdoutOffset, stderrOffset, signal }) => typed(
|
|
1056
|
+
this.httpClient.requestStream("GET", `${commandPath(microvmId, commandId)}/logs`, {
|
|
1057
|
+
query: { follow, stdoutOffset, stderrOffset },
|
|
1058
|
+
headers: { Accept: "text/event-stream" },
|
|
1059
|
+
signal
|
|
1060
|
+
})
|
|
1061
|
+
),
|
|
1062
|
+
{
|
|
1063
|
+
...options,
|
|
1064
|
+
resolveExit: options.resolveExit ?? (async () => {
|
|
1065
|
+
const info = await this.get(microvmId, commandId);
|
|
1066
|
+
return info.status === "exited" ? { exitCode: info.exitCode, signal: info.signal } : null;
|
|
1067
|
+
})
|
|
1068
|
+
}
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Wait up to `timeoutMs` (at most 60000) for the command to exit
|
|
1073
|
+
* (`POST /microvms/:id/commands/:cmdId/wait`). The returned command is still
|
|
1074
|
+
* `running` when the wait timed out.
|
|
1075
|
+
*/
|
|
1076
|
+
async wait(microvmId, commandId, options = {}) {
|
|
1077
|
+
const { signal, timeoutMs } = options;
|
|
1078
|
+
const raw = await read(
|
|
1079
|
+
() => this.httpClient.post(
|
|
1080
|
+
`${commandPath(microvmId, commandId)}/wait`,
|
|
1081
|
+
timeoutMs === void 0 ? {} : { timeoutMs },
|
|
1082
|
+
{ signal }
|
|
1083
|
+
),
|
|
1084
|
+
signal
|
|
1085
|
+
);
|
|
1086
|
+
return normalizeCommand(raw);
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Write to the command's stdin (`POST /microvms/:id/commands/:cmdId/stdin`).
|
|
1090
|
+
* The command must have been started with `stdin: true` or a `pty`.
|
|
1091
|
+
*
|
|
1092
|
+
* @param options.eof - Close stdin after writing.
|
|
1093
|
+
*/
|
|
1094
|
+
async stdin(microvmId, commandId, data, options = {}) {
|
|
1095
|
+
await typed(
|
|
1096
|
+
this.httpClient.post(`${commandPath(microvmId, commandId)}/stdin`, data, {
|
|
1097
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
1098
|
+
query: { eof: options.eof ? true : void 0 },
|
|
1099
|
+
signal: options.signal
|
|
1100
|
+
})
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
/** Send a signal to the command's process group (`POST /microvms/:id/commands/:cmdId/signal`). */
|
|
1104
|
+
async signal(microvmId, commandId, signal, options = {}) {
|
|
1105
|
+
await typed(this.httpClient.post(`${commandPath(microvmId, commandId)}/signal`, { signal }, options));
|
|
1106
|
+
}
|
|
1107
|
+
/** Resize a PTY command's terminal (`POST /microvms/:id/commands/:cmdId/resize`). */
|
|
1108
|
+
async resize(microvmId, commandId, size, options = {}) {
|
|
1109
|
+
await typed(
|
|
1110
|
+
this.httpClient.post(
|
|
1111
|
+
`${commandPath(microvmId, commandId)}/resize`,
|
|
1112
|
+
{ cols: size.cols, rows: size.rows },
|
|
1113
|
+
options
|
|
1114
|
+
)
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
var modeString = (mode) => typeof mode === "number" ? mode.toString(8).padStart(4, "0") : mode;
|
|
1119
|
+
var MicroVmFilesResource = class extends BaseResource {
|
|
1120
|
+
/** Read a file as bytes (`GET /microvms/:id/files/content`). */
|
|
1121
|
+
async read(microvmId, path, options = {}) {
|
|
1122
|
+
const response = await this.open(microvmId, path, options);
|
|
1123
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1124
|
+
}
|
|
1125
|
+
/** Read a file as a stream of bytes, without buffering it (`GET /microvms/:id/files/content`). */
|
|
1126
|
+
async readStream(microvmId, path, options = {}) {
|
|
1127
|
+
const response = await this.open(microvmId, path, options);
|
|
1128
|
+
if (!response.body) return new Blob([]).stream();
|
|
1129
|
+
return response.body;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Write a file, replacing it atomically (`PUT /microvms/:id/files/content`).
|
|
1133
|
+
* Missing parent directories are created unless `mkdirs: false`.
|
|
1134
|
+
*/
|
|
1135
|
+
write(microvmId, path, content, options = {}) {
|
|
1136
|
+
return typed(
|
|
1137
|
+
this.httpClient.put(`${vmPath(microvmId)}/files/content`, content, {
|
|
1138
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
1139
|
+
query: { path, mode: modeString(options.mode), mkdirs: options.mkdirs === false ? false : void 0 },
|
|
1140
|
+
signal: options.signal
|
|
1141
|
+
}),
|
|
1142
|
+
path
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* List a directory (`GET /microvms/:id/files`). `truncated` is set when the
|
|
1147
|
+
* listing hit its size limit; list a narrower directory then.
|
|
1148
|
+
*/
|
|
1149
|
+
async list(microvmId, path, options = {}) {
|
|
1150
|
+
const body = await read(
|
|
1151
|
+
() => this.httpClient.get(`${vmPath(microvmId)}/files`, {
|
|
1152
|
+
query: { path, recursive: options.recursive ? true : void 0, depth: options.depth },
|
|
1153
|
+
signal: options.signal
|
|
1154
|
+
}),
|
|
1155
|
+
options.signal,
|
|
1156
|
+
path
|
|
1157
|
+
);
|
|
1158
|
+
const truncated = body?.truncated === true;
|
|
1159
|
+
return { entries: unwrapList(body, "entries"), truncated };
|
|
1160
|
+
}
|
|
1161
|
+
/** Describe a file or directory without following a final symlink (`GET /microvms/:id/files/stat`). */
|
|
1162
|
+
stat(microvmId, path, options = {}) {
|
|
1163
|
+
return read(
|
|
1164
|
+
() => this.httpClient.get(`${vmPath(microvmId)}/files/stat`, { query: { path }, signal: options.signal }),
|
|
1165
|
+
options.signal,
|
|
1166
|
+
path
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
/** Create a directory, with its parents unless `parents: false` (`POST /microvms/:id/files/mkdir`). */
|
|
1170
|
+
mkdir(microvmId, path, options = {}) {
|
|
1171
|
+
const body = options.parents === void 0 ? { path } : { path, parents: options.parents };
|
|
1172
|
+
return typed(this.httpClient.post(`${vmPath(microvmId)}/files/mkdir`, body, options), path);
|
|
1173
|
+
}
|
|
1174
|
+
/** Move or rename a file or directory (`POST /microvms/:id/files/move`). */
|
|
1175
|
+
move(microvmId, from, to, options = {}) {
|
|
1176
|
+
return typed(this.httpClient.post(`${vmPath(microvmId)}/files/move`, { from, to }, options), from);
|
|
1177
|
+
}
|
|
1178
|
+
/** Delete a file, or a directory (non-empty ones need `recursive`) (`DELETE /microvms/:id/files`). */
|
|
1179
|
+
async remove(microvmId, path, options = {}) {
|
|
1180
|
+
await typed(
|
|
1181
|
+
this.httpClient.delete(`${vmPath(microvmId)}/files`, {
|
|
1182
|
+
query: { path, recursive: options.recursive ? true : void 0 },
|
|
1183
|
+
signal: options.signal
|
|
1184
|
+
}),
|
|
1185
|
+
path
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
open(microvmId, path, options) {
|
|
1189
|
+
return read(
|
|
1190
|
+
() => this.httpClient.requestStream("GET", `${vmPath(microvmId)}/files/content`, {
|
|
1191
|
+
query: { path, offset: options.offset, length: options.length },
|
|
1192
|
+
signal: options.signal
|
|
1193
|
+
}),
|
|
1194
|
+
options.signal,
|
|
1195
|
+
path
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
var toImageSource = (input) => {
|
|
1200
|
+
if (input.source) return input.source;
|
|
1201
|
+
const from = input.from;
|
|
1202
|
+
if (typeof from === "string") return { type: "registry", ref: from };
|
|
1203
|
+
if (from && "dockerfile" in from) return { type: "dockerfile", dockerfile: from.dockerfile };
|
|
1204
|
+
if (from && "registry" in from) return { type: "registry", ref: from.registry };
|
|
1205
|
+
throw new TypeError("Pass `from` (a registry reference or { dockerfile }) or `source` to create an image.");
|
|
1206
|
+
};
|
|
1207
|
+
var MicroVmImagesResource = class extends BaseResource {
|
|
1208
|
+
defaultApplicationId;
|
|
1209
|
+
constructor(httpClient, defaultApplicationId) {
|
|
1210
|
+
super(httpClient);
|
|
1211
|
+
this.defaultApplicationId = defaultApplicationId || void 0;
|
|
1212
|
+
}
|
|
1213
|
+
/** List the built-in images and the application's custom images (`GET /microvm-images`). */
|
|
1214
|
+
async list(query = {}, options = {}) {
|
|
1215
|
+
const body = await read(
|
|
1216
|
+
() => this.httpClient.get("/microvm-images", {
|
|
1217
|
+
query: { applicationId: query.applicationId ?? this.defaultApplicationId },
|
|
1218
|
+
signal: options.signal
|
|
1219
|
+
}),
|
|
1220
|
+
options.signal
|
|
1221
|
+
);
|
|
1222
|
+
return unwrapList(body);
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Start building a custom image (`POST /microvm-images`). The image is
|
|
1226
|
+
* `building` until {@link waitUntilReady} resolves.
|
|
1227
|
+
*
|
|
1228
|
+
* @example
|
|
1229
|
+
* ```ts
|
|
1230
|
+
* const image = await client.microvms.images.create({ name: 'py-ml', from: 'python:3.13' });
|
|
1231
|
+
* await client.microvms.images.waitUntilReady(image.id);
|
|
1232
|
+
* const vm = await MicroVM.create({ image: 'py-ml' });
|
|
1233
|
+
* ```
|
|
1234
|
+
*/
|
|
1235
|
+
async create(input, options = {}) {
|
|
1236
|
+
const applicationId = input.applicationId ?? this.defaultApplicationId;
|
|
1237
|
+
const source = toImageSource(input);
|
|
1238
|
+
return await typed(
|
|
1239
|
+
this.httpClient.post(
|
|
1240
|
+
"/microvm-images",
|
|
1241
|
+
{ ...applicationId ? { applicationId } : {}, name: input.name, source },
|
|
1242
|
+
options
|
|
1243
|
+
)
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
/** Get one image (`GET /microvm-images/:id`). */
|
|
1247
|
+
get(imageId, options = {}) {
|
|
1248
|
+
return read(
|
|
1249
|
+
() => this.httpClient.get(`/microvm-images/${encodeURIComponent(imageId)}`, options),
|
|
1250
|
+
options.signal
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
/** Delete a custom image (`DELETE /microvm-images/:id`). */
|
|
1254
|
+
async delete(imageId, options = {}) {
|
|
1255
|
+
await typed(this.httpClient.delete(`/microvm-images/${encodeURIComponent(imageId)}`, options));
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Poll an image until its build finished.
|
|
1259
|
+
*
|
|
1260
|
+
* @throws {@link MicroVmImageBuildError} when the build failed.
|
|
1261
|
+
* @throws {@link MicroVmTimeoutError} when it is still building after `timeout`.
|
|
1262
|
+
*/
|
|
1263
|
+
async waitUntilReady(imageId, options = {}) {
|
|
1264
|
+
const deadline = Date.now() + durationToMs(options.timeout ?? "15m");
|
|
1265
|
+
const interval = durationToMs(options.interval ?? "2s");
|
|
1266
|
+
let lastStatus;
|
|
1267
|
+
for (; ; ) {
|
|
1268
|
+
if (options.signal?.aborted) throw abortReason(options.signal);
|
|
1269
|
+
const image = await this.get(imageId, { signal: options.signal });
|
|
1270
|
+
if (image.status !== lastStatus) {
|
|
1271
|
+
lastStatus = image.status;
|
|
1272
|
+
options.onStatus?.(image);
|
|
1273
|
+
}
|
|
1274
|
+
if (image.status === "ready") return image;
|
|
1275
|
+
if (image.status === "failed") throw new MicroVmImageBuildError(image.id, image.error);
|
|
1276
|
+
if (Date.now() + interval > deadline) {
|
|
1277
|
+
throw new MicroVmTimeoutError(`The image ${image.name} is still ${image.status}; gave up waiting.`);
|
|
1278
|
+
}
|
|
1279
|
+
await sleep(interval, options.signal);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
var MicroVmsResource = class extends BaseResource {
|
|
1284
|
+
/** Commands: run, start, logs, wait, stdin, signal, resize. */
|
|
1285
|
+
commands;
|
|
1286
|
+
/** Files: read, write, list, stat, mkdir, move, remove. */
|
|
1287
|
+
files;
|
|
1288
|
+
/** Built-in and custom images. */
|
|
1289
|
+
images;
|
|
1290
|
+
defaultApplicationId;
|
|
1291
|
+
constructor(httpClient, defaultApplicationId) {
|
|
1292
|
+
super(httpClient);
|
|
1293
|
+
this.defaultApplicationId = defaultApplicationId || void 0;
|
|
1294
|
+
this.commands = new MicroVmCommandsResource(httpClient);
|
|
1295
|
+
this.files = new MicroVmFilesResource(httpClient);
|
|
1296
|
+
this.images = new MicroVmImagesResource(httpClient, defaultApplicationId);
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Create a microVM (`POST /microvms`). Resolves once it is `running`; with
|
|
1300
|
+
* `wait: false` it resolves at once with a `starting` microVM.
|
|
1301
|
+
*
|
|
1302
|
+
* `applicationId` defaults to the client's `applicationId` or
|
|
1303
|
+
* `GIGADRIVE_APPLICATION_ID`, and may be omitted entirely with application
|
|
1304
|
+
* or deployment credentials.
|
|
1305
|
+
*/
|
|
1306
|
+
create(input = {}, options = {}) {
|
|
1307
|
+
const applicationId = input.applicationId ?? this.defaultApplicationId;
|
|
1308
|
+
return typed(
|
|
1309
|
+
this.httpClient.post("/microvms", applicationId ? { ...input, applicationId } : input, {
|
|
1310
|
+
query: { wait: options.wait === false ? false : void 0 },
|
|
1311
|
+
signal: options.signal
|
|
1312
|
+
})
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
/** List one page of microVMs, newest first (`GET /microvms`). */
|
|
1316
|
+
async list(query = {}, options = {}) {
|
|
1317
|
+
const body = await read(
|
|
1318
|
+
() => this.httpClient.get("/microvms", {
|
|
1319
|
+
query: {
|
|
1320
|
+
applicationId: query.applicationId ?? this.defaultApplicationId,
|
|
1321
|
+
status: query.status,
|
|
1322
|
+
cursor: query.cursor,
|
|
1323
|
+
limit: query.limit
|
|
1324
|
+
},
|
|
1325
|
+
signal: options.signal
|
|
1326
|
+
}),
|
|
1327
|
+
options.signal
|
|
1328
|
+
);
|
|
1329
|
+
const nextCursor = body.pagination?.nextCursor ?? body.nextCursor ?? void 0;
|
|
1330
|
+
const hasMore = body.pagination?.hasMore ?? nextCursor !== void 0;
|
|
1331
|
+
return {
|
|
1332
|
+
items: body.data ?? body.items ?? [],
|
|
1333
|
+
...nextCursor && hasMore ? { nextCursor } : {},
|
|
1334
|
+
hasMore: hasMore && nextCursor !== void 0
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Iterate every microVM across all pages.
|
|
1339
|
+
*
|
|
1340
|
+
* @example
|
|
1341
|
+
* ```ts
|
|
1342
|
+
* for await (const vm of client.microvms.listAll({ status: 'running' })) console.log(vm.id);
|
|
1343
|
+
* ```
|
|
1344
|
+
*/
|
|
1345
|
+
async *listAll(query = {}, options = {}) {
|
|
1346
|
+
let cursor;
|
|
1347
|
+
do {
|
|
1348
|
+
const page = await this.list({ ...query, cursor }, options);
|
|
1349
|
+
yield* page.items;
|
|
1350
|
+
cursor = page.nextCursor;
|
|
1351
|
+
} while (cursor);
|
|
1352
|
+
}
|
|
1353
|
+
/** Get one microVM (`GET /microvms/:id`). */
|
|
1354
|
+
get(microvmId, options = {}) {
|
|
1355
|
+
return read(() => this.httpClient.get(vmPath(microvmId), options), options.signal);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Set the remaining lifetime to `timeoutMs` from now (`POST /microvms/:id/timeout`),
|
|
1359
|
+
* up to the plan's maximum lifetime.
|
|
1360
|
+
*/
|
|
1361
|
+
extendTimeout(microvmId, timeoutMs, options = {}) {
|
|
1362
|
+
return typed(this.httpClient.post(`${vmPath(microvmId)}/timeout`, { timeoutMs }, options));
|
|
1363
|
+
}
|
|
1364
|
+
/** Stop a microVM (`POST /microvms/:id/stop`). Its disk and memory are discarded. */
|
|
1365
|
+
stop(microvmId, options = {}) {
|
|
1366
|
+
return typed(this.httpClient.post(`${vmPath(microvmId)}/stop`, void 0, options));
|
|
1367
|
+
}
|
|
1368
|
+
/** Delete a microVM, stopping it first if needed (`DELETE /microvms/:id`). */
|
|
1369
|
+
async delete(microvmId, options = {}) {
|
|
1370
|
+
await typed(this.httpClient.delete(vmPath(microvmId), options));
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Request a public preview URL with the client's `fetch`, without
|
|
1374
|
+
* credentials and without throwing on error statuses. Used by
|
|
1375
|
+
* `MicroVM.waitForPort`.
|
|
1376
|
+
*
|
|
1377
|
+
* @internal
|
|
1378
|
+
*/
|
|
1379
|
+
fetchPreview(url, init) {
|
|
1380
|
+
return this.httpClient.fetchPublic(url, init);
|
|
614
1381
|
}
|
|
615
|
-
httpClient;
|
|
616
1382
|
};
|
|
617
1383
|
|
|
618
1384
|
// src/resources/ai-gateway/index.ts
|
|
@@ -897,7 +1663,7 @@ var ApplicationRequestsResource = class extends BaseResource {
|
|
|
897
1663
|
|
|
898
1664
|
// src/upload/checksum.ts
|
|
899
1665
|
var EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
900
|
-
var
|
|
1666
|
+
var toHex2 = (buffer) => {
|
|
901
1667
|
const bytes = new Uint8Array(buffer);
|
|
902
1668
|
let hex = "";
|
|
903
1669
|
for (let i = 0; i < bytes.length; i++) {
|
|
@@ -905,7 +1671,7 @@ var toHex = (buffer) => {
|
|
|
905
1671
|
}
|
|
906
1672
|
return hex;
|
|
907
1673
|
};
|
|
908
|
-
var subtleDigest = async (algorithm, bytes) =>
|
|
1674
|
+
var subtleDigest = async (algorithm, bytes) => toHex2(await crypto.subtle.digest(algorithm, bytes));
|
|
909
1675
|
var importNodeCrypto = async () => {
|
|
910
1676
|
if (typeof process === "undefined" || !process.versions?.node) return null;
|
|
911
1677
|
try {
|
|
@@ -1714,7 +2480,7 @@ var pollSignal = (ms, signal) => {
|
|
|
1714
2480
|
}
|
|
1715
2481
|
};
|
|
1716
2482
|
};
|
|
1717
|
-
var
|
|
2483
|
+
var sleep2 = (ms, signal) => new Promise((resolve, reject) => {
|
|
1718
2484
|
if (signal?.aborted) {
|
|
1719
2485
|
reject(signal.reason instanceof Error ? signal.reason : new GigadriveError("Aborted"));
|
|
1720
2486
|
return;
|
|
@@ -1838,7 +2604,7 @@ var ApplicationDomainsResource = class extends BaseResource {
|
|
|
1838
2604
|
if (last) throw new DomainNotActiveError(last, "timeout");
|
|
1839
2605
|
throw new GigadriveError("Timed out before the domain could be read");
|
|
1840
2606
|
}
|
|
1841
|
-
await
|
|
2607
|
+
await sleep2(Math.min(intervalMs, untilDeadline), signal);
|
|
1842
2608
|
}
|
|
1843
2609
|
}
|
|
1844
2610
|
};
|
|
@@ -2624,48 +3390,9 @@ var OrganizationsResource = class extends BaseResource {
|
|
|
2624
3390
|
* console.log(org.id, org.slug);
|
|
2625
3391
|
* ```
|
|
2626
3392
|
*/
|
|
2627
|
-
async create(input) {
|
|
2628
|
-
return this.httpClient.post("/organizations", input);
|
|
2629
|
-
}
|
|
2630
|
-
};
|
|
2631
|
-
|
|
2632
|
-
// src/queues/encoding.ts
|
|
2633
|
-
var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
|
|
2634
|
-
var UNIT_SECONDS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
|
|
2635
|
-
var durationToSeconds = (value) => {
|
|
2636
|
-
let seconds;
|
|
2637
|
-
if (typeof value === "number") {
|
|
2638
|
-
seconds = value;
|
|
2639
|
-
} else {
|
|
2640
|
-
const match = DURATION_PATTERN.exec(value.trim());
|
|
2641
|
-
if (!match) throw new Error(`Invalid duration "${value}". Use seconds or a string such as 30s, 10m, 12h or 7d.`);
|
|
2642
|
-
seconds = Number(match[1]) * UNIT_SECONDS[match[2]];
|
|
2643
|
-
}
|
|
2644
|
-
if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid duration "${String(value)}"`);
|
|
2645
|
-
return Math.ceil(seconds);
|
|
2646
|
-
};
|
|
2647
|
-
var textEncoder = new TextEncoder();
|
|
2648
|
-
var textDecoder = new TextDecoder();
|
|
2649
|
-
var utf8Encode = (value) => textEncoder.encode(value);
|
|
2650
|
-
var utf8Decode = (bytes) => textDecoder.decode(bytes);
|
|
2651
|
-
var bytesToBase64 = (bytes) => {
|
|
2652
|
-
let binary = "";
|
|
2653
|
-
for (let index = 0; index < bytes.length; index += 32768) {
|
|
2654
|
-
binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
|
|
3393
|
+
async create(input) {
|
|
3394
|
+
return this.httpClient.post("/organizations", input);
|
|
2655
3395
|
}
|
|
2656
|
-
return btoa(binary);
|
|
2657
|
-
};
|
|
2658
|
-
var base64ToBytes = (value) => {
|
|
2659
|
-
const binary = atob(value);
|
|
2660
|
-
const bytes = new Uint8Array(binary.length);
|
|
2661
|
-
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
2662
|
-
return bytes;
|
|
2663
|
-
};
|
|
2664
|
-
var toHex2 = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2665
|
-
var toArrayBuffer = (bytes) => {
|
|
2666
|
-
const copy = new ArrayBuffer(bytes.byteLength);
|
|
2667
|
-
new Uint8Array(copy).set(bytes);
|
|
2668
|
-
return copy;
|
|
2669
3396
|
};
|
|
2670
3397
|
|
|
2671
3398
|
// src/queues/errors.ts
|
|
@@ -2697,11 +3424,11 @@ var hmacSha256Hex = async (secret, payload) => {
|
|
|
2697
3424
|
false,
|
|
2698
3425
|
["sign"]
|
|
2699
3426
|
);
|
|
2700
|
-
return
|
|
3427
|
+
return toHex(await crypto.subtle.sign("HMAC", key, toArrayBuffer(utf8Encode(payload))));
|
|
2701
3428
|
};
|
|
2702
3429
|
var signedPayload = async (input, timestamp) => {
|
|
2703
3430
|
const body = typeof input.body === "string" ? utf8Encode(input.body) : input.body;
|
|
2704
|
-
const bodyHash =
|
|
3431
|
+
const bodyHash = toHex(await crypto.subtle.digest("SHA-256", toArrayBuffer(body)));
|
|
2705
3432
|
return `${String(timestamp)}.${input.queue}.${input.messageId}.${String(input.attempt)}.${bodyHash}`;
|
|
2706
3433
|
};
|
|
2707
3434
|
var safeEqual = (left, right) => {
|
|
@@ -2784,7 +3511,7 @@ var ACK_FLUSH_MS = 25;
|
|
|
2784
3511
|
var ACK_RETRY_DELAYS_MS = [200, 1e3];
|
|
2785
3512
|
var EMPTY_POLL_MIN_MS = 1e3;
|
|
2786
3513
|
var EMPTY_POLL_MAX_MS = 5e3;
|
|
2787
|
-
var
|
|
3514
|
+
var sleep3 = (ms, signal) => new Promise((resolve) => {
|
|
2788
3515
|
if (signal?.aborted) return resolve();
|
|
2789
3516
|
const timer = setTimeout(done, ms);
|
|
2790
3517
|
function done() {
|
|
@@ -2979,7 +3706,7 @@ var Queue = class {
|
|
|
2979
3706
|
const retryable = !(error instanceof ApiError) || error.status >= 500 || error.status === 429;
|
|
2980
3707
|
const delay2 = ACK_RETRY_DELAYS_MS[attempt];
|
|
2981
3708
|
if (!retryable || delay2 === void 0) throw error;
|
|
2982
|
-
await
|
|
3709
|
+
await sleep3(delay2, void 0);
|
|
2983
3710
|
}
|
|
2984
3711
|
}
|
|
2985
3712
|
};
|
|
@@ -3019,7 +3746,7 @@ var Queue = class {
|
|
|
3019
3746
|
if (messages.length === 0) {
|
|
3020
3747
|
if (options.stopWhenEmpty) return;
|
|
3021
3748
|
const remaining = Math.max(waitMs, EMPTY_POLL_MIN_MS) - (Date.now() - started);
|
|
3022
|
-
if (remaining > 0) await
|
|
3749
|
+
if (remaining > 0) await sleep3(Math.min(remaining, EMPTY_POLL_MAX_MS), options.signal);
|
|
3023
3750
|
continue;
|
|
3024
3751
|
}
|
|
3025
3752
|
await Promise.all(
|
|
@@ -3455,6 +4182,12 @@ var GigadriveClient = class {
|
|
|
3455
4182
|
* handle bound to one queue.
|
|
3456
4183
|
*/
|
|
3457
4184
|
queues;
|
|
4185
|
+
/**
|
|
4186
|
+
* microVMs: isolated Linux machines to run commands in, move files to and
|
|
4187
|
+
* from, and expose ports from. This is the low-level REST surface; the
|
|
4188
|
+
* {@link MicroVM} class (`MicroVM.create({ client })`) is the friendlier API.
|
|
4189
|
+
*/
|
|
4190
|
+
microvms;
|
|
3458
4191
|
constructor(config = {}) {
|
|
3459
4192
|
const baseUrl = config.baseUrl ?? readEnv("GIGADRIVE_API_BASE_URL") ?? DEFAULT_BASE_URL2;
|
|
3460
4193
|
const applicationId = config.applicationId ?? readEnv("GIGADRIVE_APPLICATION_ID");
|
|
@@ -3471,6 +4204,687 @@ var GigadriveClient = class {
|
|
|
3471
4204
|
this.stickySessions = new StickySessionsResource(httpClient);
|
|
3472
4205
|
this.imageOptimization = new ImageOptimizationResource(httpClient);
|
|
3473
4206
|
this.queues = new QueuesResource(httpClient, applicationId);
|
|
4207
|
+
this.microvms = new MicroVmsResource(httpClient, applicationId);
|
|
4208
|
+
}
|
|
4209
|
+
};
|
|
4210
|
+
|
|
4211
|
+
// src/microvms/command.ts
|
|
4212
|
+
var MAX_WAIT_MS = 6e4;
|
|
4213
|
+
var toMs = (value) => {
|
|
4214
|
+
if (value === null || value === void 0) return null;
|
|
4215
|
+
const ms = typeof value === "number" ? value : Date.parse(value);
|
|
4216
|
+
return Number.isFinite(ms) ? ms : null;
|
|
4217
|
+
};
|
|
4218
|
+
var commandDurationMs = (info) => {
|
|
4219
|
+
const started = toMs(info.startedAt);
|
|
4220
|
+
const finished = toMs(info.finishedAt);
|
|
4221
|
+
return started !== null && finished !== null ? Math.max(0, finished - started) : null;
|
|
4222
|
+
};
|
|
4223
|
+
var Command = class {
|
|
4224
|
+
/** @internal Use {@link MicroVM.spawn} or `vm.commands.get(id)`. */
|
|
4225
|
+
constructor(resource, microvmId, info) {
|
|
4226
|
+
this.resource = resource;
|
|
4227
|
+
this.microvmId = microvmId;
|
|
4228
|
+
this.current = info;
|
|
4229
|
+
}
|
|
4230
|
+
resource;
|
|
4231
|
+
microvmId;
|
|
4232
|
+
current;
|
|
4233
|
+
pump;
|
|
4234
|
+
pumpError;
|
|
4235
|
+
skippedOutput = false;
|
|
4236
|
+
/** Command id (`cmd_...`). Pass it to `vm.commands.get(id)` to reattach later. */
|
|
4237
|
+
get id() {
|
|
4238
|
+
return this.current.id;
|
|
4239
|
+
}
|
|
4240
|
+
/** The latest known state. Updated by {@link refresh}, {@link wait} and the end of {@link logs}. */
|
|
4241
|
+
get info() {
|
|
4242
|
+
return this.current;
|
|
4243
|
+
}
|
|
4244
|
+
get status() {
|
|
4245
|
+
return this.current.status;
|
|
4246
|
+
}
|
|
4247
|
+
/** Exit code once exited; `null` while running or when a signal killed it. */
|
|
4248
|
+
get exitCode() {
|
|
4249
|
+
return this.current.exitCode;
|
|
4250
|
+
}
|
|
4251
|
+
/** Signal that killed the process, if any. */
|
|
4252
|
+
get signal() {
|
|
4253
|
+
return this.current.signal;
|
|
4254
|
+
}
|
|
4255
|
+
get pid() {
|
|
4256
|
+
return this.current.pid;
|
|
4257
|
+
}
|
|
4258
|
+
/** Whether the command was killed because it ran past its `timeout`. */
|
|
4259
|
+
get timedOut() {
|
|
4260
|
+
return this.current.timedOut;
|
|
4261
|
+
}
|
|
4262
|
+
/**
|
|
4263
|
+
* Whether a {@link logs} read found older output already dropped (each
|
|
4264
|
+
* stream keeps its last 8 MiB), so what it delivered starts mid-stream.
|
|
4265
|
+
*/
|
|
4266
|
+
get truncated() {
|
|
4267
|
+
return this.skippedOutput;
|
|
4268
|
+
}
|
|
4269
|
+
/** Fetch the latest state from the API. */
|
|
4270
|
+
async refresh() {
|
|
4271
|
+
this.current = await this.resource.get(this.microvmId, this.id);
|
|
4272
|
+
return this.current;
|
|
4273
|
+
}
|
|
4274
|
+
/**
|
|
4275
|
+
* Stream the command's output, from the beginning of the retained output by
|
|
4276
|
+
* default, until it exits. With `follow: false` it delivers what is
|
|
4277
|
+
* buffered and ends, even while the command keeps running. Reconnects transparently after a dropped
|
|
4278
|
+
* connection. Breaking out of the loop closes the stream; the command keeps
|
|
4279
|
+
* running.
|
|
4280
|
+
*
|
|
4281
|
+
* @example
|
|
4282
|
+
* ```ts
|
|
4283
|
+
* for await (const { stream, text } of command.logs()) {
|
|
4284
|
+
* (stream === 'stdout' ? process.stdout : process.stderr).write(text);
|
|
4285
|
+
* }
|
|
4286
|
+
* ```
|
|
4287
|
+
*/
|
|
4288
|
+
async *logs(options = {}) {
|
|
4289
|
+
const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() };
|
|
4290
|
+
for await (const event of this.resource.logs(this.microvmId, this.id, options)) {
|
|
4291
|
+
if (event.type === "exit") {
|
|
4292
|
+
this.current = {
|
|
4293
|
+
...this.current,
|
|
4294
|
+
status: "exited",
|
|
4295
|
+
exitCode: event.exitCode,
|
|
4296
|
+
signal: event.signal,
|
|
4297
|
+
...event.timedOut !== void 0 && { timedOut: event.timedOut },
|
|
4298
|
+
...event.finishedAt !== void 0 && { finishedAt: event.finishedAt }
|
|
4299
|
+
};
|
|
4300
|
+
continue;
|
|
4301
|
+
}
|
|
4302
|
+
if (event.type === "truncated") {
|
|
4303
|
+
this.skippedOutput = true;
|
|
4304
|
+
continue;
|
|
4305
|
+
}
|
|
4306
|
+
if (event.type === "end") continue;
|
|
4307
|
+
const text = decoders[event.stream].decode(event.data, { stream: true });
|
|
4308
|
+
yield { stream: event.stream, data: event.data, text, offset: event.offset };
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
/**
|
|
4312
|
+
* Collect the output from the start until the command exits.
|
|
4313
|
+
*
|
|
4314
|
+
* @example
|
|
4315
|
+
* ```ts
|
|
4316
|
+
* const { stdout } = await (await vm.spawn('ls', ['-la'])).output();
|
|
4317
|
+
* ```
|
|
4318
|
+
*/
|
|
4319
|
+
async output(options = {}) {
|
|
4320
|
+
let stdout = "";
|
|
4321
|
+
let stderr = "";
|
|
4322
|
+
for await (const chunk of this.logs(options)) {
|
|
4323
|
+
if (chunk.stream === "stdout") stdout += chunk.text;
|
|
4324
|
+
else stderr += chunk.text;
|
|
4325
|
+
}
|
|
4326
|
+
return { stdout, stderr };
|
|
4327
|
+
}
|
|
4328
|
+
/** The complete stdout as text, once the command exited. */
|
|
4329
|
+
async stdout(options = {}) {
|
|
4330
|
+
return (await this.output(options)).stdout;
|
|
4331
|
+
}
|
|
4332
|
+
/** The complete stderr as text, once the command exited. */
|
|
4333
|
+
async stderr(options = {}) {
|
|
4334
|
+
return (await this.output(options)).stderr;
|
|
4335
|
+
}
|
|
4336
|
+
/**
|
|
4337
|
+
* Wait for the command to exit and return its final state. Output callbacks
|
|
4338
|
+
* given to `spawn` have received all output when this resolves.
|
|
4339
|
+
*
|
|
4340
|
+
* @throws {@link MicroVmTimeoutError} when `timeout` elapses first (the command keeps running).
|
|
4341
|
+
* @throws {@link CommandFailedError} with `throwOnError` when it exits non-zero or is killed.
|
|
4342
|
+
*/
|
|
4343
|
+
async wait(options = {}) {
|
|
4344
|
+
const deadline = options.timeout === void 0 ? Infinity : Date.now() + durationToMs(options.timeout);
|
|
4345
|
+
while (this.current.status !== "exited") {
|
|
4346
|
+
const remaining = deadline - Date.now();
|
|
4347
|
+
if (remaining <= 0) {
|
|
4348
|
+
throw new MicroVmTimeoutError(`The command ${this.id} is still running; gave up waiting.`);
|
|
4349
|
+
}
|
|
4350
|
+
this.current = await this.resource.wait(this.microvmId, this.id, {
|
|
4351
|
+
timeoutMs: Math.min(MAX_WAIT_MS, remaining),
|
|
4352
|
+
signal: options.signal
|
|
4353
|
+
});
|
|
4354
|
+
}
|
|
4355
|
+
if (this.pump) {
|
|
4356
|
+
await this.pump;
|
|
4357
|
+
if (this.pumpError) throw this.pumpError;
|
|
4358
|
+
}
|
|
4359
|
+
if (options.throwOnError && this.current.exitCode !== 0) {
|
|
4360
|
+
throw CommandFailedError.fromInfo(this.current);
|
|
4361
|
+
}
|
|
4362
|
+
return this.current;
|
|
4363
|
+
}
|
|
4364
|
+
/**
|
|
4365
|
+
* Send a signal to the command's process group. Default `SIGTERM`; use
|
|
4366
|
+
* `SIGKILL` to stop it unconditionally.
|
|
4367
|
+
*/
|
|
4368
|
+
async kill(signal = "SIGTERM") {
|
|
4369
|
+
await this.resource.signal(this.microvmId, this.id, signal);
|
|
4370
|
+
}
|
|
4371
|
+
/**
|
|
4372
|
+
* Write to stdin. The command must have been spawned with `stdin: true` or a `pty`.
|
|
4373
|
+
*
|
|
4374
|
+
* @example
|
|
4375
|
+
* ```ts
|
|
4376
|
+
* const cat = await vm.spawn('cat', [], { stdin: true });
|
|
4377
|
+
* await cat.write('hello\n');
|
|
4378
|
+
* await cat.end();
|
|
4379
|
+
* console.log(await cat.stdout()); // "hello\n"
|
|
4380
|
+
* ```
|
|
4381
|
+
*/
|
|
4382
|
+
async write(data) {
|
|
4383
|
+
await this.resource.stdin(this.microvmId, this.id, data);
|
|
4384
|
+
}
|
|
4385
|
+
/** Close stdin, optionally writing a last piece of data first. */
|
|
4386
|
+
async end(data = new Uint8Array(0)) {
|
|
4387
|
+
await this.resource.stdin(this.microvmId, this.id, data, { eof: true });
|
|
4388
|
+
}
|
|
4389
|
+
/** Resize the terminal of a PTY command. */
|
|
4390
|
+
async resize(cols, rows) {
|
|
4391
|
+
await this.resource.resize(this.microvmId, this.id, { cols, rows });
|
|
4392
|
+
}
|
|
4393
|
+
/** JSON form: the command's latest state. */
|
|
4394
|
+
toJSON() {
|
|
4395
|
+
return this.current;
|
|
4396
|
+
}
|
|
4397
|
+
/**
|
|
4398
|
+
* Deliver output to callbacks in the background, for `spawn` with
|
|
4399
|
+
* `onStdout`/`onStderr`. {@link wait} waits for it and rethrows its error.
|
|
4400
|
+
*
|
|
4401
|
+
* @internal
|
|
4402
|
+
*/
|
|
4403
|
+
startOutputPump(onStdout, onStderr) {
|
|
4404
|
+
this.pump = (async () => {
|
|
4405
|
+
for await (const chunk of this.logs()) {
|
|
4406
|
+
await (chunk.stream === "stdout" ? onStdout : onStderr)?.(chunk.text, chunk);
|
|
4407
|
+
}
|
|
4408
|
+
})().catch((error) => {
|
|
4409
|
+
this.pumpError = error instanceof Error ? error : new Error(String(error));
|
|
4410
|
+
});
|
|
4411
|
+
}
|
|
4412
|
+
};
|
|
4413
|
+
|
|
4414
|
+
// src/microvms/microvm.ts
|
|
4415
|
+
var defaultClient;
|
|
4416
|
+
var resolveClient = (client) => {
|
|
4417
|
+
if (client instanceof GigadriveClient) return client;
|
|
4418
|
+
if (client) return new GigadriveClient(client);
|
|
4419
|
+
defaultClient ??= new GigadriveClient();
|
|
4420
|
+
return defaultClient;
|
|
4421
|
+
};
|
|
4422
|
+
var CREATE_WAIT_TIMEOUT = "10m";
|
|
4423
|
+
var NOT_LISTENING = /* @__PURE__ */ new Set([502, 503, 504]);
|
|
4424
|
+
var PROBE_TIMEOUT_MS = 5e3;
|
|
4425
|
+
var TERMINAL = ["stopping", "stopped", "failed"];
|
|
4426
|
+
var toStartInput = (cmd, args, options) => {
|
|
4427
|
+
const user = options.sudo ? "root" : options.user;
|
|
4428
|
+
return {
|
|
4429
|
+
cmd,
|
|
4430
|
+
...args ? { args } : {},
|
|
4431
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
4432
|
+
...options.env ? { env: options.env } : {},
|
|
4433
|
+
...user !== void 0 ? { user } : {},
|
|
4434
|
+
...options.timeout !== void 0 ? { timeoutMs: durationToMs(options.timeout) } : {}
|
|
4435
|
+
};
|
|
4436
|
+
};
|
|
4437
|
+
var failIfRequested = (result, throwOnError) => {
|
|
4438
|
+
if (throwOnError && result.exitCode !== 0) throw new CommandFailedError(result);
|
|
4439
|
+
return result;
|
|
4440
|
+
};
|
|
4441
|
+
var MicroVmFiles = class {
|
|
4442
|
+
/** @internal */
|
|
4443
|
+
constructor(resource, microvmId) {
|
|
4444
|
+
this.resource = resource;
|
|
4445
|
+
this.microvmId = microvmId;
|
|
4446
|
+
}
|
|
4447
|
+
resource;
|
|
4448
|
+
microvmId;
|
|
4449
|
+
/** Read a file as bytes. @throws {@link MicroVmFileNotFoundError} */
|
|
4450
|
+
read(path, options) {
|
|
4451
|
+
return this.resource.read(this.microvmId, path, options);
|
|
4452
|
+
}
|
|
4453
|
+
/** Read a file as UTF-8 text. */
|
|
4454
|
+
async readText(path, options) {
|
|
4455
|
+
return new TextDecoder().decode(await this.read(path, options));
|
|
4456
|
+
}
|
|
4457
|
+
/** Read a file as a stream, for files too large to buffer. */
|
|
4458
|
+
readStream(path, options) {
|
|
4459
|
+
return this.resource.readStream(this.microvmId, path, options);
|
|
4460
|
+
}
|
|
4461
|
+
/**
|
|
4462
|
+
* Write a file atomically, creating parent directories.
|
|
4463
|
+
*
|
|
4464
|
+
* @example
|
|
4465
|
+
* ```ts
|
|
4466
|
+
* await vm.files.write('/app/index.js', 'console.log("hi")');
|
|
4467
|
+
* await vm.files.write('/app/logo.png', await (await fetch(url)).blob());
|
|
4468
|
+
* ```
|
|
4469
|
+
*/
|
|
4470
|
+
write(path, content, options) {
|
|
4471
|
+
return this.resource.write(this.microvmId, path, content, options);
|
|
4472
|
+
}
|
|
4473
|
+
/**
|
|
4474
|
+
* Write several files, a few at a time. Resolves in input order once all
|
|
4475
|
+
* succeeded; rejects with the first failure.
|
|
4476
|
+
*
|
|
4477
|
+
* @example
|
|
4478
|
+
* ```ts
|
|
4479
|
+
* await vm.files.writeMany([
|
|
4480
|
+
* { path: 'app/package.json', content: JSON.stringify(pkg) },
|
|
4481
|
+
* { path: 'app/run.sh', content: '#!/bin/sh\nnode .', mode: '0755' },
|
|
4482
|
+
* ]);
|
|
4483
|
+
* ```
|
|
4484
|
+
*/
|
|
4485
|
+
async writeMany(files, options = {}) {
|
|
4486
|
+
const results = new Array(files.length);
|
|
4487
|
+
let next = 0;
|
|
4488
|
+
const worker = async () => {
|
|
4489
|
+
while (next < files.length) {
|
|
4490
|
+
const index = next++;
|
|
4491
|
+
const file = files[index];
|
|
4492
|
+
results[index] = await this.write(file.path, file.content, { mode: file.mode, signal: options.signal });
|
|
4493
|
+
}
|
|
4494
|
+
};
|
|
4495
|
+
const workers = Math.max(1, Math.min(options.concurrency ?? 4, files.length));
|
|
4496
|
+
await Promise.all(Array.from({ length: workers }, worker));
|
|
4497
|
+
return results;
|
|
4498
|
+
}
|
|
4499
|
+
/**
|
|
4500
|
+
* List a directory. `recursive` descends into subdirectories (up to `depth`
|
|
4501
|
+
* levels). Very large listings are cut off; `client.microvms.files.list`
|
|
4502
|
+
* reports that as `truncated`.
|
|
4503
|
+
*/
|
|
4504
|
+
async list(path = ".", options) {
|
|
4505
|
+
return (await this.resource.list(this.microvmId, path, options)).entries;
|
|
4506
|
+
}
|
|
4507
|
+
/** Describe a file or directory. @throws {@link MicroVmFileNotFoundError} */
|
|
4508
|
+
stat(path) {
|
|
4509
|
+
return this.resource.stat(this.microvmId, path);
|
|
4510
|
+
}
|
|
4511
|
+
/** Whether a file or directory exists. */
|
|
4512
|
+
async exists(path) {
|
|
4513
|
+
try {
|
|
4514
|
+
await this.stat(path);
|
|
4515
|
+
return true;
|
|
4516
|
+
} catch (error) {
|
|
4517
|
+
if (error instanceof MicroVmFileNotFoundError) return false;
|
|
4518
|
+
throw error;
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
/** Create a directory and its parents (pass `parents: false` to require the parent). */
|
|
4522
|
+
mkdir(path, options) {
|
|
4523
|
+
return this.resource.mkdir(this.microvmId, path, options);
|
|
4524
|
+
}
|
|
4525
|
+
/** Delete a file, or a directory with `recursive: true`. */
|
|
4526
|
+
remove(path, options) {
|
|
4527
|
+
return this.resource.remove(this.microvmId, path, options);
|
|
4528
|
+
}
|
|
4529
|
+
/** Move or rename a file or directory. */
|
|
4530
|
+
move(from, to) {
|
|
4531
|
+
return this.resource.move(this.microvmId, from, to);
|
|
4532
|
+
}
|
|
4533
|
+
};
|
|
4534
|
+
var MicroVmCommands = class {
|
|
4535
|
+
/** @internal */
|
|
4536
|
+
constructor(resource, microvmId) {
|
|
4537
|
+
this.resource = resource;
|
|
4538
|
+
this.microvmId = microvmId;
|
|
4539
|
+
}
|
|
4540
|
+
resource;
|
|
4541
|
+
microvmId;
|
|
4542
|
+
/**
|
|
4543
|
+
* Reattach to a command, for example one spawned by another process.
|
|
4544
|
+
*
|
|
4545
|
+
* @example
|
|
4546
|
+
* ```ts
|
|
4547
|
+
* const command = await vm.commands.get(savedCommandId);
|
|
4548
|
+
* for await (const chunk of command.logs()) process.stdout.write(chunk.data);
|
|
4549
|
+
* ```
|
|
4550
|
+
*/
|
|
4551
|
+
async get(commandId) {
|
|
4552
|
+
return new Command(this.resource, this.microvmId, await this.resource.get(this.microvmId, commandId));
|
|
4553
|
+
}
|
|
4554
|
+
/** All commands the microVM still remembers, newest first. */
|
|
4555
|
+
async list() {
|
|
4556
|
+
const infos = await this.resource.list(this.microvmId);
|
|
4557
|
+
return infos.map((info) => new Command(this.resource, this.microvmId, info));
|
|
4558
|
+
}
|
|
4559
|
+
};
|
|
4560
|
+
var MicroVM = class _MicroVM {
|
|
4561
|
+
/** @internal Use {@link MicroVM.create} or {@link MicroVM.get}. */
|
|
4562
|
+
constructor(resource, info) {
|
|
4563
|
+
this.resource = resource;
|
|
4564
|
+
this.current = info;
|
|
4565
|
+
this.files = new MicroVmFiles(resource.files, info.id);
|
|
4566
|
+
this.commands = new MicroVmCommands(resource.commands, info.id);
|
|
4567
|
+
}
|
|
4568
|
+
resource;
|
|
4569
|
+
/** Files inside the microVM. */
|
|
4570
|
+
files;
|
|
4571
|
+
/** Reattach to commands by id. */
|
|
4572
|
+
commands;
|
|
4573
|
+
current;
|
|
4574
|
+
/**
|
|
4575
|
+
* Create a microVM and wait until it runs.
|
|
4576
|
+
*
|
|
4577
|
+
* @throws {RangeError} When `timeout` is longer than 24 hours.
|
|
4578
|
+
*
|
|
4579
|
+
* The application is taken from `applicationId`, the client's
|
|
4580
|
+
* `applicationId` or `GIGADRIVE_APPLICATION_ID`; inside a Gigadrive
|
|
4581
|
+
* deployment or Function nothing needs configuring.
|
|
4582
|
+
*
|
|
4583
|
+
* @example
|
|
4584
|
+
* ```ts
|
|
4585
|
+
* const vm = await MicroVM.create({
|
|
4586
|
+
* image: 'python3.13',
|
|
4587
|
+
* vcpus: 2,
|
|
4588
|
+
* memoryMb: 4096,
|
|
4589
|
+
* timeout: '15m',
|
|
4590
|
+
* ports: [8000],
|
|
4591
|
+
* env: { PYTHONUNBUFFERED: '1' },
|
|
4592
|
+
* metadata: { user: 'u_123' },
|
|
4593
|
+
* network: 'deny',
|
|
4594
|
+
* });
|
|
4595
|
+
* ```
|
|
4596
|
+
*/
|
|
4597
|
+
static async create(options = {}) {
|
|
4598
|
+
const { client, timeout, timeoutMs, network, wait, signal, ...input } = options;
|
|
4599
|
+
const resource = resolveClient(client).microvms;
|
|
4600
|
+
const info = await resource.create(
|
|
4601
|
+
{
|
|
4602
|
+
...input,
|
|
4603
|
+
...timeout !== void 0 ? { timeoutMs: microvmTimeoutMs(timeout) } : timeoutMs !== void 0 ? { timeoutMs } : {},
|
|
4604
|
+
...network !== void 0 ? { network: typeof network === "string" ? { egress: network } : network } : {}
|
|
4605
|
+
},
|
|
4606
|
+
// Never hold one request open for the whole boot: a create can wait
|
|
4607
|
+
// minutes for capacity, longer than proxies in front of the API keep a
|
|
4608
|
+
// response open. Start it, then poll.
|
|
4609
|
+
{ wait: false, signal }
|
|
4610
|
+
);
|
|
4611
|
+
const vm = new _MicroVM(resource, info);
|
|
4612
|
+
if (wait === false) return vm;
|
|
4613
|
+
try {
|
|
4614
|
+
return await vm.waitUntilRunning({ timeout: CREATE_WAIT_TIMEOUT, signal });
|
|
4615
|
+
} catch (error) {
|
|
4616
|
+
if (error instanceof MicroVmStartError) throw error;
|
|
4617
|
+
try {
|
|
4618
|
+
await vm.delete();
|
|
4619
|
+
} catch (cleanupError) {
|
|
4620
|
+
throw new MicroVmCleanupError(vm.id, error, cleanupError);
|
|
4621
|
+
}
|
|
4622
|
+
throw error;
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
/**
|
|
4626
|
+
* Connect to an existing microVM by id.
|
|
4627
|
+
*
|
|
4628
|
+
* @throws {@link MicroVmNotFoundError} when it does not exist.
|
|
4629
|
+
*/
|
|
4630
|
+
static async get(microvmId, options = {}) {
|
|
4631
|
+
const resource = resolveClient(options.client).microvms;
|
|
4632
|
+
return new _MicroVM(resource, await resource.get(microvmId));
|
|
4633
|
+
}
|
|
4634
|
+
/** Alias of {@link MicroVM.get}. */
|
|
4635
|
+
static connect(microvmId, options = {}) {
|
|
4636
|
+
return _MicroVM.get(microvmId, options);
|
|
4637
|
+
}
|
|
4638
|
+
/**
|
|
4639
|
+
* Iterate microVMs, newest first, across all pages.
|
|
4640
|
+
*
|
|
4641
|
+
* @example
|
|
4642
|
+
* ```ts
|
|
4643
|
+
* for await (const vm of MicroVM.list({ status: 'running' })) {
|
|
4644
|
+
* console.log(vm.id, vm.metadata);
|
|
4645
|
+
* }
|
|
4646
|
+
* ```
|
|
4647
|
+
*/
|
|
4648
|
+
static async *list(options = {}) {
|
|
4649
|
+
const { client, ...query } = options;
|
|
4650
|
+
const resource = resolveClient(client).microvms;
|
|
4651
|
+
for await (const info of resource.listAll(query)) yield new _MicroVM(resource, info);
|
|
4652
|
+
}
|
|
4653
|
+
/** Stable UUID. */
|
|
4654
|
+
get id() {
|
|
4655
|
+
return this.current.id;
|
|
4656
|
+
}
|
|
4657
|
+
/** Label used in preview URLs. */
|
|
4658
|
+
get slug() {
|
|
4659
|
+
return this.current.slug;
|
|
4660
|
+
}
|
|
4661
|
+
get applicationId() {
|
|
4662
|
+
return this.current.applicationId;
|
|
4663
|
+
}
|
|
4664
|
+
/** Status as of the last API response; call {@link refresh} for the current one. */
|
|
4665
|
+
get status() {
|
|
4666
|
+
return this.current.status;
|
|
4667
|
+
}
|
|
4668
|
+
/** Exposed ports. */
|
|
4669
|
+
get ports() {
|
|
4670
|
+
return this.current.ports.map((entry) => entry.port);
|
|
4671
|
+
}
|
|
4672
|
+
get metadata() {
|
|
4673
|
+
return this.current.metadata;
|
|
4674
|
+
}
|
|
4675
|
+
/** When the microVM stops automatically. */
|
|
4676
|
+
get expiresAt() {
|
|
4677
|
+
return this.current.expiresAt ? new Date(this.current.expiresAt) : null;
|
|
4678
|
+
}
|
|
4679
|
+
/** The full API object as of the last response. */
|
|
4680
|
+
get info() {
|
|
4681
|
+
return this.current;
|
|
4682
|
+
}
|
|
4683
|
+
/**
|
|
4684
|
+
* The public preview URL of an exposed port.
|
|
4685
|
+
*
|
|
4686
|
+
* @throws {@link MicroVmPortNotExposedError} when the port was not in `ports` at create.
|
|
4687
|
+
*
|
|
4688
|
+
* @example
|
|
4689
|
+
* ```ts
|
|
4690
|
+
* vm.url(3000); // "https://3000-k3j9x2m1q8zt.vm.gigadrive.app"
|
|
4691
|
+
* ```
|
|
4692
|
+
*/
|
|
4693
|
+
url(port) {
|
|
4694
|
+
const entry = this.current.ports.find((candidate) => candidate.port === port);
|
|
4695
|
+
if (!entry) throw new MicroVmPortNotExposedError(port, this.ports);
|
|
4696
|
+
return entry.url;
|
|
4697
|
+
}
|
|
4698
|
+
/** Fetch the latest state. */
|
|
4699
|
+
async refresh(options = {}) {
|
|
4700
|
+
this.current = await this.resource.get(this.id, { signal: options.signal });
|
|
4701
|
+
return this.current;
|
|
4702
|
+
}
|
|
4703
|
+
async run(cmd, argsOrOptions, maybeOptions) {
|
|
4704
|
+
const args = Array.isArray(argsOrOptions) ? argsOrOptions : void 0;
|
|
4705
|
+
const options = (Array.isArray(argsOrOptions) ? maybeOptions : argsOrOptions) ?? {};
|
|
4706
|
+
const input = toStartInput(cmd, args, options);
|
|
4707
|
+
const streaming = options.onStdout !== void 0 || options.onStderr !== void 0 || options.stdin !== void 0 || options.signal !== void 0;
|
|
4708
|
+
if (!streaming) {
|
|
4709
|
+
const raw = await this.resource.commands.run(this.id, input);
|
|
4710
|
+
return failIfRequested(
|
|
4711
|
+
{
|
|
4712
|
+
commandId: raw.id,
|
|
4713
|
+
exitCode: raw.exitCode,
|
|
4714
|
+
signal: raw.signal ?? null,
|
|
4715
|
+
timedOut: raw.timedOut === true,
|
|
4716
|
+
stdout: raw.stdout,
|
|
4717
|
+
stderr: raw.stderr,
|
|
4718
|
+
truncated: raw.truncated,
|
|
4719
|
+
durationMs: raw.durationMs
|
|
4720
|
+
},
|
|
4721
|
+
options.throwOnError
|
|
4722
|
+
);
|
|
4723
|
+
}
|
|
4724
|
+
const { signal } = options;
|
|
4725
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
4726
|
+
const startedAt = Date.now();
|
|
4727
|
+
const info = await this.resource.commands.start(
|
|
4728
|
+
this.id,
|
|
4729
|
+
{ ...input, ...options.stdin !== void 0 ? { stdin: true } : {} },
|
|
4730
|
+
{ signal }
|
|
4731
|
+
);
|
|
4732
|
+
const command = new Command(this.resource.commands, this.id, info);
|
|
4733
|
+
const kill = () => void command.kill("SIGKILL").catch(() => void 0);
|
|
4734
|
+
signal?.addEventListener("abort", kill, { once: true });
|
|
4735
|
+
try {
|
|
4736
|
+
if (options.stdin !== void 0) await command.end(options.stdin);
|
|
4737
|
+
const output = { stdout: "", stderr: "" };
|
|
4738
|
+
const expected = { stdout: 0, stderr: 0 };
|
|
4739
|
+
let truncated = false;
|
|
4740
|
+
for await (const chunk of command.logs({ signal })) {
|
|
4741
|
+
if (chunk.offset > expected[chunk.stream]) truncated = true;
|
|
4742
|
+
expected[chunk.stream] = chunk.offset + chunk.data.length;
|
|
4743
|
+
output[chunk.stream] += chunk.text;
|
|
4744
|
+
await (chunk.stream === "stdout" ? options.onStdout : options.onStderr)?.(chunk.text, chunk);
|
|
4745
|
+
}
|
|
4746
|
+
const final = command.status === "exited" ? command.info : await command.wait({ signal });
|
|
4747
|
+
return failIfRequested(
|
|
4748
|
+
{
|
|
4749
|
+
commandId: command.id,
|
|
4750
|
+
exitCode: final.exitCode,
|
|
4751
|
+
signal: final.signal,
|
|
4752
|
+
timedOut: final.timedOut === true,
|
|
4753
|
+
...output,
|
|
4754
|
+
truncated: truncated || command.truncated,
|
|
4755
|
+
durationMs: commandDurationMs(final) ?? Date.now() - startedAt
|
|
4756
|
+
},
|
|
4757
|
+
options.throwOnError
|
|
4758
|
+
);
|
|
4759
|
+
} finally {
|
|
4760
|
+
signal?.removeEventListener("abort", kill);
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
async spawn(cmd, argsOrOptions, maybeOptions) {
|
|
4764
|
+
const args = Array.isArray(argsOrOptions) ? argsOrOptions : void 0;
|
|
4765
|
+
const options = (Array.isArray(argsOrOptions) ? maybeOptions : argsOrOptions) ?? {};
|
|
4766
|
+
const pty = options.pty === true ? { cols: 80, rows: 24 } : options.pty || void 0;
|
|
4767
|
+
const info = await this.resource.commands.start(this.id, {
|
|
4768
|
+
...toStartInput(cmd, args, options),
|
|
4769
|
+
...options.stdin ? { stdin: true } : {},
|
|
4770
|
+
...pty ? { pty } : {}
|
|
4771
|
+
});
|
|
4772
|
+
const command = new Command(this.resource.commands, this.id, info);
|
|
4773
|
+
if (options.onStdout || options.onStderr) command.startOutputPump(options.onStdout, options.onStderr);
|
|
4774
|
+
return command;
|
|
4775
|
+
}
|
|
4776
|
+
/**
|
|
4777
|
+
* Set the remaining lifetime to `duration` from now (up to the plan's
|
|
4778
|
+
* maximum lifetime).
|
|
4779
|
+
*
|
|
4780
|
+
* @example
|
|
4781
|
+
* ```ts
|
|
4782
|
+
* await vm.extendTimeout('30m');
|
|
4783
|
+
* ```
|
|
4784
|
+
*/
|
|
4785
|
+
async extendTimeout(duration) {
|
|
4786
|
+
this.current = await this.resource.extendTimeout(this.id, microvmTimeoutMs(duration));
|
|
4787
|
+
return this.current;
|
|
4788
|
+
}
|
|
4789
|
+
/** Stop the microVM. Its disk, memory and running commands are discarded. */
|
|
4790
|
+
async stop() {
|
|
4791
|
+
this.current = await this.resource.stop(this.id);
|
|
4792
|
+
return this.current;
|
|
4793
|
+
}
|
|
4794
|
+
/** Delete the microVM, stopping it first if needed. */
|
|
4795
|
+
async delete() {
|
|
4796
|
+
await this.resource.delete(this.id);
|
|
4797
|
+
}
|
|
4798
|
+
/**
|
|
4799
|
+
* Poll until the microVM is running, for microVMs created with `wait: false`.
|
|
4800
|
+
*
|
|
4801
|
+
* @throws {@link MicroVmStartError} when it failed or stopped instead.
|
|
4802
|
+
* @throws {@link MicroVmTimeoutError} when it is still starting after `timeout`.
|
|
4803
|
+
*/
|
|
4804
|
+
async waitUntilRunning(options = {}) {
|
|
4805
|
+
const deadline = Date.now() + durationToMs(options.timeout ?? "2m");
|
|
4806
|
+
const interval = durationToMs(options.interval ?? "500ms");
|
|
4807
|
+
while (this.current.status !== "running") {
|
|
4808
|
+
if (TERMINAL.includes(this.current.status)) {
|
|
4809
|
+
throw new MicroVmStartError(this.id, this.current.status, this.current.stopReason);
|
|
4810
|
+
}
|
|
4811
|
+
if (Date.now() + interval > deadline) {
|
|
4812
|
+
throw new MicroVmTimeoutError(`The microVM ${this.id} is still ${this.current.status}; gave up waiting.`);
|
|
4813
|
+
}
|
|
4814
|
+
await sleep(interval, options.signal);
|
|
4815
|
+
await this.refresh({ signal: options.signal });
|
|
4816
|
+
if (options.signal?.aborted) throw abortReason(options.signal);
|
|
4817
|
+
}
|
|
4818
|
+
return this;
|
|
4819
|
+
}
|
|
4820
|
+
/**
|
|
4821
|
+
* Poll an exposed port's preview URL until a server answers (any status
|
|
4822
|
+
* other than 502, 503 or 504, which the gateway returns while nothing
|
|
4823
|
+
* listens).
|
|
4824
|
+
*
|
|
4825
|
+
* @returns The preview URL.
|
|
4826
|
+
* @throws {@link MicroVmPortNotExposedError} when the port is not exposed.
|
|
4827
|
+
* @throws {@link MicroVmNotRunningError} when the microVM has stopped.
|
|
4828
|
+
* @throws {@link MicroVmTimeoutError} when nothing answers within `timeout`.
|
|
4829
|
+
*
|
|
4830
|
+
* @example
|
|
4831
|
+
* ```ts
|
|
4832
|
+
* await vm.spawn('python3', ['-m', 'http.server', '8000']);
|
|
4833
|
+
* const url = await vm.waitForPort(8000);
|
|
4834
|
+
* ```
|
|
4835
|
+
*/
|
|
4836
|
+
async waitForPort(port, options = {}) {
|
|
4837
|
+
const base = this.url(port);
|
|
4838
|
+
const path = options.path ?? "/";
|
|
4839
|
+
const target = `${base.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
4840
|
+
const deadline = Date.now() + durationToMs(options.timeout ?? "60s");
|
|
4841
|
+
const interval = durationToMs(options.interval ?? "500ms");
|
|
4842
|
+
for (; ; ) {
|
|
4843
|
+
if (options.signal?.aborted) throw abortReason(options.signal);
|
|
4844
|
+
const controller = new AbortController();
|
|
4845
|
+
const timer = setTimeout(
|
|
4846
|
+
() => controller.abort(),
|
|
4847
|
+
Math.min(PROBE_TIMEOUT_MS, Math.max(1, deadline - Date.now()))
|
|
4848
|
+
);
|
|
4849
|
+
const forward = () => controller.abort();
|
|
4850
|
+
options.signal?.addEventListener("abort", forward, { once: true });
|
|
4851
|
+
let stopped = false;
|
|
4852
|
+
try {
|
|
4853
|
+
const response = await this.resource.fetchPreview(target, {
|
|
4854
|
+
method: "GET",
|
|
4855
|
+
redirect: "manual",
|
|
4856
|
+
signal: controller.signal
|
|
4857
|
+
});
|
|
4858
|
+
await response.body?.cancel().catch(() => void 0);
|
|
4859
|
+
stopped = response.status === 410 && response.headers.get("x-gigadrive-error") === "EDGE_MICROVM_STOPPED";
|
|
4860
|
+
if (!stopped && !NOT_LISTENING.has(response.status)) return base;
|
|
4861
|
+
} catch {
|
|
4862
|
+
if (options.signal?.aborted) throw abortReason(options.signal);
|
|
4863
|
+
} finally {
|
|
4864
|
+
clearTimeout(timer);
|
|
4865
|
+
options.signal?.removeEventListener("abort", forward);
|
|
4866
|
+
}
|
|
4867
|
+
if (stopped) {
|
|
4868
|
+
throw new MicroVmNotRunningError(`The microVM ${this.id} has stopped; nothing will answer on port ${port}.`);
|
|
4869
|
+
}
|
|
4870
|
+
if (Date.now() + interval > deadline) {
|
|
4871
|
+
throw new MicroVmTimeoutError(`Nothing answered on port ${port} of the microVM ${this.id}.`);
|
|
4872
|
+
}
|
|
4873
|
+
await sleep(interval, options.signal);
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
/** Stops the microVM at the end of an `await using` block. Already stopped microVMs are fine. */
|
|
4877
|
+
async [Symbol.asyncDispose]() {
|
|
4878
|
+
try {
|
|
4879
|
+
await this.stop();
|
|
4880
|
+
} catch (error) {
|
|
4881
|
+
if (error instanceof MicroVmNotRunningError || error instanceof MicroVmNotFoundError) return;
|
|
4882
|
+
throw error;
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
/** JSON form: the API object. */
|
|
4886
|
+
toJSON() {
|
|
4887
|
+
return this.current;
|
|
3474
4888
|
}
|
|
3475
4889
|
};
|
|
3476
4890
|
|
|
@@ -3487,10 +4901,10 @@ async function* paginate(fetchPage) {
|
|
|
3487
4901
|
}
|
|
3488
4902
|
|
|
3489
4903
|
// src/queues/default-client.ts
|
|
3490
|
-
var
|
|
4904
|
+
var defaultClient2;
|
|
3491
4905
|
var defaultQueuesResource = () => {
|
|
3492
|
-
|
|
3493
|
-
return
|
|
4906
|
+
defaultClient2 ??= new GigadriveClient();
|
|
4907
|
+
return defaultClient2.queues;
|
|
3494
4908
|
};
|
|
3495
4909
|
function queue(name, options = {}) {
|
|
3496
4910
|
const { client, ...queueOptions } = options;
|
|
@@ -3514,7 +4928,7 @@ var decodeWorkflowMessage = (text) => JSON.parse(text, (_key, value) => {
|
|
|
3514
4928
|
return value;
|
|
3515
4929
|
});
|
|
3516
4930
|
var readEnv2 = (name) => globalThis.process?.env?.[name] || void 0;
|
|
3517
|
-
var deduplicationKeyFor = async (idempotencyKey) => idempotencyKey.length <= 256 ? idempotencyKey : `wkf:sha256:${
|
|
4931
|
+
var deduplicationKeyFor = async (idempotencyKey) => idempotencyKey.length <= 256 ? idempotencyKey : `wkf:sha256:${toHex(await crypto.subtle.digest("SHA-256", toArrayBuffer(utf8Encode(idempotencyKey))))}`;
|
|
3518
4932
|
function createWorkflowQueue(options = {}) {
|
|
3519
4933
|
const resource = () => options.client?.queues ?? defaultQueuesResource();
|
|
3520
4934
|
const scope = { applicationId: options.applicationId, environment: options.environment };
|
|
@@ -3655,12 +5069,34 @@ export {
|
|
|
3655
5069
|
ApplicationStorageResource,
|
|
3656
5070
|
ApplicationsResource,
|
|
3657
5071
|
AuthenticationError,
|
|
5072
|
+
Command,
|
|
5073
|
+
CommandFailedError,
|
|
3658
5074
|
ConfigurationError,
|
|
3659
5075
|
DeploymentsResource,
|
|
3660
5076
|
DomainNotActiveError,
|
|
3661
5077
|
GigadriveClient,
|
|
3662
5078
|
GigadriveError,
|
|
3663
5079
|
ImageOptimizationResource,
|
|
5080
|
+
MICROVM_MAX_TIMEOUT_MS,
|
|
5081
|
+
MicroVM,
|
|
5082
|
+
MicroVmBusyError,
|
|
5083
|
+
MicroVmCleanupError,
|
|
5084
|
+
MicroVmCommands,
|
|
5085
|
+
MicroVmCommandsResource,
|
|
5086
|
+
MicroVmFileNotFoundError,
|
|
5087
|
+
MicroVmFiles,
|
|
5088
|
+
MicroVmFilesResource,
|
|
5089
|
+
MicroVmImageBuildError,
|
|
5090
|
+
MicroVmImageNotReadyError,
|
|
5091
|
+
MicroVmImagesResource,
|
|
5092
|
+
MicroVmNotFoundError,
|
|
5093
|
+
MicroVmNotRunningError,
|
|
5094
|
+
MicroVmPortNotExposedError,
|
|
5095
|
+
MicroVmQuotaExceededError,
|
|
5096
|
+
MicroVmStartError,
|
|
5097
|
+
MicroVmTimeoutError,
|
|
5098
|
+
MicroVmUnreachableError,
|
|
5099
|
+
MicroVmsResource,
|
|
3664
5100
|
NonRetryableError,
|
|
3665
5101
|
OrganizationAiGatewayResource,
|
|
3666
5102
|
OrganizationDomainsResource,
|
|
@@ -3688,8 +5124,10 @@ export {
|
|
|
3688
5124
|
createManagedImageUrl,
|
|
3689
5125
|
createWorkflowQueue,
|
|
3690
5126
|
defaultQueueCodec,
|
|
5127
|
+
durationToMs,
|
|
3691
5128
|
durationToSeconds,
|
|
3692
5129
|
paginate,
|
|
5130
|
+
parseSSEEvents,
|
|
3693
5131
|
parseSSEStream,
|
|
3694
5132
|
queue,
|
|
3695
5133
|
signQueueDelivery,
|