@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.js
CHANGED
|
@@ -44,12 +44,34 @@ __export(src_exports, {
|
|
|
44
44
|
ApplicationStorageResource: () => ApplicationStorageResource,
|
|
45
45
|
ApplicationsResource: () => ApplicationsResource,
|
|
46
46
|
AuthenticationError: () => AuthenticationError,
|
|
47
|
+
Command: () => Command,
|
|
48
|
+
CommandFailedError: () => CommandFailedError,
|
|
47
49
|
ConfigurationError: () => ConfigurationError,
|
|
48
50
|
DeploymentsResource: () => DeploymentsResource,
|
|
49
51
|
DomainNotActiveError: () => DomainNotActiveError,
|
|
50
52
|
GigadriveClient: () => GigadriveClient,
|
|
51
53
|
GigadriveError: () => GigadriveError,
|
|
52
54
|
ImageOptimizationResource: () => ImageOptimizationResource,
|
|
55
|
+
MICROVM_MAX_TIMEOUT_MS: () => MICROVM_MAX_TIMEOUT_MS,
|
|
56
|
+
MicroVM: () => MicroVM,
|
|
57
|
+
MicroVmBusyError: () => MicroVmBusyError,
|
|
58
|
+
MicroVmCleanupError: () => MicroVmCleanupError,
|
|
59
|
+
MicroVmCommands: () => MicroVmCommands,
|
|
60
|
+
MicroVmCommandsResource: () => MicroVmCommandsResource,
|
|
61
|
+
MicroVmFileNotFoundError: () => MicroVmFileNotFoundError,
|
|
62
|
+
MicroVmFiles: () => MicroVmFiles,
|
|
63
|
+
MicroVmFilesResource: () => MicroVmFilesResource,
|
|
64
|
+
MicroVmImageBuildError: () => MicroVmImageBuildError,
|
|
65
|
+
MicroVmImageNotReadyError: () => MicroVmImageNotReadyError,
|
|
66
|
+
MicroVmImagesResource: () => MicroVmImagesResource,
|
|
67
|
+
MicroVmNotFoundError: () => MicroVmNotFoundError,
|
|
68
|
+
MicroVmNotRunningError: () => MicroVmNotRunningError,
|
|
69
|
+
MicroVmPortNotExposedError: () => MicroVmPortNotExposedError,
|
|
70
|
+
MicroVmQuotaExceededError: () => MicroVmQuotaExceededError,
|
|
71
|
+
MicroVmStartError: () => MicroVmStartError,
|
|
72
|
+
MicroVmTimeoutError: () => MicroVmTimeoutError,
|
|
73
|
+
MicroVmUnreachableError: () => MicroVmUnreachableError,
|
|
74
|
+
MicroVmsResource: () => MicroVmsResource,
|
|
53
75
|
NonRetryableError: () => NonRetryableError,
|
|
54
76
|
OrganizationAiGatewayResource: () => OrganizationAiGatewayResource,
|
|
55
77
|
OrganizationDomainsResource: () => OrganizationDomainsResource,
|
|
@@ -77,8 +99,10 @@ __export(src_exports, {
|
|
|
77
99
|
createManagedImageUrl: () => createManagedImageUrl,
|
|
78
100
|
createWorkflowQueue: () => createWorkflowQueue,
|
|
79
101
|
defaultQueueCodec: () => defaultQueueCodec,
|
|
102
|
+
durationToMs: () => durationToMs,
|
|
80
103
|
durationToSeconds: () => durationToSeconds,
|
|
81
104
|
paginate: () => paginate,
|
|
105
|
+
parseSSEEvents: () => parseSSEEvents,
|
|
82
106
|
parseSSEStream: () => parseSSEStream,
|
|
83
107
|
queue: () => queue,
|
|
84
108
|
signQueueDelivery: () => signQueueDelivery,
|
|
@@ -554,6 +578,16 @@ var HttpClient = class {
|
|
|
554
578
|
}
|
|
555
579
|
return response;
|
|
556
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Fetch an arbitrary URL without authentication and without throwing on a
|
|
583
|
+
* non-2xx status, using the client's configured `fetch`. Used to probe
|
|
584
|
+
* public URLs such as microVM preview URLs.
|
|
585
|
+
*
|
|
586
|
+
* @internal
|
|
587
|
+
*/
|
|
588
|
+
async fetchPublic(url, init) {
|
|
589
|
+
return this.fetchFn(url, init);
|
|
590
|
+
}
|
|
557
591
|
async request(method, path, options) {
|
|
558
592
|
const response = await this.fetchWithAuth(method, path, options);
|
|
559
593
|
return this.handleResponse(response);
|
|
@@ -578,8 +612,9 @@ var HttpClient = class {
|
|
|
578
612
|
}
|
|
579
613
|
}
|
|
580
614
|
const signal = options?.signal;
|
|
581
|
-
const response = await this.fetchFn(url, { method, headers, body: requestBody, signal });
|
|
582
615
|
const bodyIsOneShot = typeof ReadableStream !== "undefined" && requestBody instanceof ReadableStream;
|
|
616
|
+
const init = bodyIsOneShot ? { method, headers, body: requestBody, signal, duplex: "half" } : { method, headers, body: requestBody, signal };
|
|
617
|
+
const response = await this.fetchFn(url, init);
|
|
583
618
|
if (response.status === 401 && !bodyIsOneShot) {
|
|
584
619
|
this.tokenManager.invalidate();
|
|
585
620
|
const retryToken = await this.tokenManager.getToken();
|
|
@@ -640,12 +675,265 @@ var HttpClient = class {
|
|
|
640
675
|
}
|
|
641
676
|
};
|
|
642
677
|
|
|
678
|
+
// src/resources/base-resource.ts
|
|
679
|
+
var BaseResource = class {
|
|
680
|
+
constructor(httpClient) {
|
|
681
|
+
this.httpClient = httpClient;
|
|
682
|
+
}
|
|
683
|
+
httpClient;
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// src/microvms/duration.ts
|
|
687
|
+
var PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)$/;
|
|
688
|
+
var UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
|
|
689
|
+
var durationToMs = (value) => {
|
|
690
|
+
let ms;
|
|
691
|
+
if (typeof value === "number") {
|
|
692
|
+
ms = value;
|
|
693
|
+
} else {
|
|
694
|
+
const match = PATTERN.exec(value.trim());
|
|
695
|
+
if (!match)
|
|
696
|
+
throw new RangeError(`Invalid duration "${value}". Use milliseconds or a string such as 30s, 10m or 2h.`);
|
|
697
|
+
ms = Number(match[1]) * UNIT_MS[match[2]];
|
|
698
|
+
}
|
|
699
|
+
if (!Number.isFinite(ms) || ms < 0) throw new RangeError(`Invalid duration "${String(value)}"`);
|
|
700
|
+
return Math.ceil(ms);
|
|
701
|
+
};
|
|
702
|
+
var MICROVM_MAX_TIMEOUT_MS = 24 * 36e5;
|
|
703
|
+
var microvmTimeoutMs = (value) => {
|
|
704
|
+
const ms = durationToMs(value);
|
|
705
|
+
if (ms < 1 || ms > MICROVM_MAX_TIMEOUT_MS) {
|
|
706
|
+
throw new RangeError(`A microVM timeout must be between 1 ms and 24 hours (got ${String(value)}).`);
|
|
707
|
+
}
|
|
708
|
+
return ms;
|
|
709
|
+
};
|
|
710
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
711
|
+
if (signal?.aborted) {
|
|
712
|
+
reject(abortReason(signal));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const onAbort = () => {
|
|
716
|
+
clearTimeout(timer);
|
|
717
|
+
reject(abortReason(signal));
|
|
718
|
+
};
|
|
719
|
+
const timer = setTimeout(() => {
|
|
720
|
+
signal?.removeEventListener("abort", onAbort);
|
|
721
|
+
resolve();
|
|
722
|
+
}, ms);
|
|
723
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
724
|
+
});
|
|
725
|
+
var abortReason = (signal) => {
|
|
726
|
+
const reason = signal?.reason;
|
|
727
|
+
if (reason instanceof Error) return reason;
|
|
728
|
+
const error = new Error(typeof reason === "string" ? reason : "The operation was aborted");
|
|
729
|
+
error.name = "AbortError";
|
|
730
|
+
return error;
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// src/microvms/errors.ts
|
|
734
|
+
var MicroVmNotRunningError = class extends ApiError {
|
|
735
|
+
constructor(message = "The microVM is not running", code = "microvm_not_running", reason) {
|
|
736
|
+
super(message, 409, code, reason);
|
|
737
|
+
this.name = "MicroVmNotRunningError";
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
var MicroVmNotFoundError = class extends ApiError {
|
|
741
|
+
constructor(message = "Not found", code, reason) {
|
|
742
|
+
super(message, 404, code, reason);
|
|
743
|
+
this.name = "MicroVmNotFoundError";
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
var MicroVmFileNotFoundError = class extends MicroVmNotFoundError {
|
|
747
|
+
/** The path that was not found. */
|
|
748
|
+
path;
|
|
749
|
+
constructor(path, message = `No such file or directory: ${path}`, code) {
|
|
750
|
+
super(message, code);
|
|
751
|
+
this.name = "MicroVmFileNotFoundError";
|
|
752
|
+
this.path = path;
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
var MicroVmBusyError = class extends ApiError {
|
|
756
|
+
constructor(message, status, code = "microvm_busy", reason) {
|
|
757
|
+
super(message, status, code, reason);
|
|
758
|
+
this.name = "MicroVmBusyError";
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
var MicroVmUnreachableError = class extends ApiError {
|
|
762
|
+
constructor(message, code = "microvm_unreachable", reason) {
|
|
763
|
+
super(message, 502, code, reason);
|
|
764
|
+
this.name = "MicroVmUnreachableError";
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
var MicroVmImageNotReadyError = class extends ApiError {
|
|
768
|
+
constructor(message, code = "image_not_ready", reason) {
|
|
769
|
+
super(message, 409, code, reason);
|
|
770
|
+
this.name = "MicroVmImageNotReadyError";
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
var MicroVmQuotaExceededError = class extends ApiError {
|
|
774
|
+
constructor(message, status, code, reason) {
|
|
775
|
+
super(message, status, code, reason);
|
|
776
|
+
this.name = "MicroVmQuotaExceededError";
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
var CommandFailedError = class _CommandFailedError extends GigadriveError {
|
|
780
|
+
commandId;
|
|
781
|
+
/** Exit code, or `null` when a signal killed the process. */
|
|
782
|
+
exitCode;
|
|
783
|
+
signal;
|
|
784
|
+
stdout;
|
|
785
|
+
stderr;
|
|
786
|
+
constructor(output, label = "Command") {
|
|
787
|
+
const how = output.exitCode !== null ? `exited with code ${output.exitCode}` : `was killed by ${output.signal ?? "a signal"}`;
|
|
788
|
+
const tail = output.stderr.trim().split("\n").slice(-5).join("\n");
|
|
789
|
+
super(tail ? `${label} ${how}:
|
|
790
|
+
${tail}` : `${label} ${how}`);
|
|
791
|
+
this.name = "CommandFailedError";
|
|
792
|
+
this.commandId = output.commandId;
|
|
793
|
+
this.exitCode = output.exitCode;
|
|
794
|
+
this.signal = output.signal;
|
|
795
|
+
this.stdout = output.stdout;
|
|
796
|
+
this.stderr = output.stderr;
|
|
797
|
+
}
|
|
798
|
+
/** Builds the error from a finished command whose output was not collected. */
|
|
799
|
+
static fromInfo(info) {
|
|
800
|
+
return new _CommandFailedError({
|
|
801
|
+
commandId: info.id,
|
|
802
|
+
exitCode: info.exitCode,
|
|
803
|
+
signal: info.signal,
|
|
804
|
+
stdout: "",
|
|
805
|
+
stderr: "",
|
|
806
|
+
durationMs: 0
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
var MicroVmTimeoutError = class extends GigadriveError {
|
|
811
|
+
constructor(message) {
|
|
812
|
+
super(message);
|
|
813
|
+
this.name = "MicroVmTimeoutError";
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
var MicroVmPortNotExposedError = class extends GigadriveError {
|
|
817
|
+
port;
|
|
818
|
+
constructor(port, exposed) {
|
|
819
|
+
super(
|
|
820
|
+
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(", ")}.`
|
|
821
|
+
);
|
|
822
|
+
this.name = "MicroVmPortNotExposedError";
|
|
823
|
+
this.port = port;
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
var MicroVmStartError = class extends GigadriveError {
|
|
827
|
+
microvmId;
|
|
828
|
+
status;
|
|
829
|
+
stopReason;
|
|
830
|
+
constructor(microvmId, status, stopReason) {
|
|
831
|
+
super(`The microVM ${microvmId} is ${status}${stopReason ? ` (${stopReason})` : ""}`);
|
|
832
|
+
this.name = "MicroVmStartError";
|
|
833
|
+
this.microvmId = microvmId;
|
|
834
|
+
this.status = status;
|
|
835
|
+
this.stopReason = stopReason;
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
var MicroVmCleanupError = class extends GigadriveError {
|
|
839
|
+
microvmId;
|
|
840
|
+
/** Why deleting the microVM failed. {@link Error.cause} is why creating it failed. */
|
|
841
|
+
cleanupError;
|
|
842
|
+
constructor(microvmId, cause, cleanupError) {
|
|
843
|
+
super(
|
|
844
|
+
`Creating the microVM ${microvmId} failed and deleting it failed too. Delete it with MicroVM.get('${microvmId}') and delete().`
|
|
845
|
+
);
|
|
846
|
+
this.name = "MicroVmCleanupError";
|
|
847
|
+
this.microvmId = microvmId;
|
|
848
|
+
this.cleanupError = cleanupError;
|
|
849
|
+
this.cause = cause;
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
var MicroVmImageBuildError = class extends GigadriveError {
|
|
853
|
+
imageId;
|
|
854
|
+
constructor(imageId, message) {
|
|
855
|
+
super(`The image build failed${message ? `: ${message}` : ""}`);
|
|
856
|
+
this.name = "MicroVmImageBuildError";
|
|
857
|
+
this.imageId = imageId;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
var NOT_RUNNING = /microvm[_ ]not[_ ]running|not running/i;
|
|
861
|
+
var toMicroVmError = (error, filePath) => {
|
|
862
|
+
if (!(error instanceof ApiError) || Object.getPrototypeOf(error) !== ApiError.prototype) return error;
|
|
863
|
+
const { message, status, code, reason } = error;
|
|
864
|
+
if (code === "microvm_busy") return new MicroVmBusyError(message, status, code, reason);
|
|
865
|
+
if (code === "microvm_unreachable") return new MicroVmUnreachableError(message, code, reason);
|
|
866
|
+
if (code === "image_not_ready") return new MicroVmImageNotReadyError(message, code, reason);
|
|
867
|
+
if (code === "quota_exceeded" || code === "plan_limit_exceeded") {
|
|
868
|
+
return new MicroVmQuotaExceededError(message, status, code, reason);
|
|
869
|
+
}
|
|
870
|
+
if (status === 409 && (code === "microvm_not_running" || code === void 0 && NOT_RUNNING.test(message))) {
|
|
871
|
+
return new MicroVmNotRunningError(message, code ?? "microvm_not_running", reason);
|
|
872
|
+
}
|
|
873
|
+
if (status === 404) {
|
|
874
|
+
const vmMissing = code === "not_found" || code === "microvm_not_found";
|
|
875
|
+
return filePath !== void 0 && !vmMissing ? new MicroVmFileNotFoundError(filePath, message, code) : new MicroVmNotFoundError(message, code, reason);
|
|
876
|
+
}
|
|
877
|
+
return error;
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
// src/queues/encoding.ts
|
|
881
|
+
var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
|
|
882
|
+
var UNIT_SECONDS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
|
|
883
|
+
var durationToSeconds = (value) => {
|
|
884
|
+
let seconds;
|
|
885
|
+
if (typeof value === "number") {
|
|
886
|
+
seconds = value;
|
|
887
|
+
} else {
|
|
888
|
+
const match = DURATION_PATTERN.exec(value.trim());
|
|
889
|
+
if (!match) throw new Error(`Invalid duration "${value}". Use seconds or a string such as 30s, 10m, 12h or 7d.`);
|
|
890
|
+
seconds = Number(match[1]) * UNIT_SECONDS[match[2]];
|
|
891
|
+
}
|
|
892
|
+
if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid duration "${String(value)}"`);
|
|
893
|
+
return Math.ceil(seconds);
|
|
894
|
+
};
|
|
895
|
+
var textEncoder = new TextEncoder();
|
|
896
|
+
var textDecoder = new TextDecoder();
|
|
897
|
+
var utf8Encode = (value) => textEncoder.encode(value);
|
|
898
|
+
var utf8Decode = (bytes) => textDecoder.decode(bytes);
|
|
899
|
+
var bytesToBase64 = (bytes) => {
|
|
900
|
+
let binary = "";
|
|
901
|
+
for (let index = 0; index < bytes.length; index += 32768) {
|
|
902
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
|
|
903
|
+
}
|
|
904
|
+
return btoa(binary);
|
|
905
|
+
};
|
|
906
|
+
var base64ToBytes = (value) => {
|
|
907
|
+
const binary = atob(value);
|
|
908
|
+
const bytes = new Uint8Array(binary.length);
|
|
909
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
910
|
+
return bytes;
|
|
911
|
+
};
|
|
912
|
+
var toHex = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
913
|
+
var toArrayBuffer = (bytes) => {
|
|
914
|
+
const copy = new ArrayBuffer(bytes.byteLength);
|
|
915
|
+
new Uint8Array(copy).set(bytes);
|
|
916
|
+
return copy;
|
|
917
|
+
};
|
|
918
|
+
|
|
643
919
|
// src/streaming.ts
|
|
644
|
-
var
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
920
|
+
var parseBlock = (rawEvent) => {
|
|
921
|
+
let event = "message";
|
|
922
|
+
let id;
|
|
923
|
+
let retry;
|
|
924
|
+
const data = [];
|
|
925
|
+
for (const line of rawEvent.split("\n")) {
|
|
926
|
+
if (line === "" || line.startsWith(":")) continue;
|
|
927
|
+
const colon = line.indexOf(":");
|
|
928
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
929
|
+
const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
930
|
+
if (field === "data") data.push(value);
|
|
931
|
+
else if (field === "event") event = value || "message";
|
|
932
|
+
else if (field === "id" && !value.includes("\0")) id = value;
|
|
933
|
+
else if (field === "retry" && /^\d+$/.test(value)) retry = Number(value);
|
|
934
|
+
}
|
|
935
|
+
if (data.length === 0) return null;
|
|
936
|
+
return { event, data: data.join("\n"), id, retry };
|
|
649
937
|
};
|
|
650
938
|
var parseEvent = (data) => {
|
|
651
939
|
try {
|
|
@@ -655,7 +943,7 @@ var parseEvent = (data) => {
|
|
|
655
943
|
throw new Error(`Failed to parse streamed SSE data as JSON: ${message}`);
|
|
656
944
|
}
|
|
657
945
|
};
|
|
658
|
-
async function*
|
|
946
|
+
async function* readEventBlocks(response) {
|
|
659
947
|
const body = response.body;
|
|
660
948
|
if (!body) {
|
|
661
949
|
throw new Error("The response has no readable body to stream.");
|
|
@@ -663,44 +951,546 @@ async function* parseSSEStream(response) {
|
|
|
663
951
|
const reader = body.getReader();
|
|
664
952
|
const decoder = new TextDecoder();
|
|
665
953
|
let buffer = "";
|
|
666
|
-
const drain = function* (flush) {
|
|
667
|
-
let separator = buffer.indexOf("\n\n");
|
|
668
|
-
while (separator !== -1) {
|
|
669
|
-
const rawEvent = buffer.slice(0, separator);
|
|
670
|
-
buffer = buffer.slice(separator + 2);
|
|
671
|
-
const data = extractData(rawEvent);
|
|
672
|
-
if (data !== null && data !== "[DONE]") {
|
|
673
|
-
yield parseEvent(data);
|
|
674
|
-
}
|
|
675
|
-
separator = buffer.indexOf("\n\n");
|
|
676
|
-
}
|
|
677
|
-
if (flush) {
|
|
678
|
-
const data = extractData(buffer);
|
|
679
|
-
buffer = "";
|
|
680
|
-
if (data !== null && data !== "[DONE]") {
|
|
681
|
-
yield parseEvent(data);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
};
|
|
685
954
|
try {
|
|
686
955
|
for (; ; ) {
|
|
687
956
|
const { done, value } = await reader.read();
|
|
688
957
|
if (done) break;
|
|
689
958
|
buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
|
|
690
|
-
|
|
959
|
+
let separator = buffer.indexOf("\n\n");
|
|
960
|
+
while (separator !== -1) {
|
|
961
|
+
yield buffer.slice(0, separator);
|
|
962
|
+
buffer = buffer.slice(separator + 2);
|
|
963
|
+
separator = buffer.indexOf("\n\n");
|
|
964
|
+
}
|
|
691
965
|
}
|
|
692
|
-
|
|
966
|
+
buffer += decoder.decode();
|
|
967
|
+
if (buffer.length > 0) yield buffer;
|
|
693
968
|
} finally {
|
|
694
969
|
await reader.cancel().catch(() => void 0);
|
|
695
970
|
}
|
|
696
971
|
}
|
|
972
|
+
async function* parseSSEEvents(response) {
|
|
973
|
+
for await (const block of readEventBlocks(response)) {
|
|
974
|
+
const event = parseBlock(block);
|
|
975
|
+
if (event) yield event;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
async function* parseSSEStream(response) {
|
|
979
|
+
for await (const event of parseSSEEvents(response)) {
|
|
980
|
+
if (event.data.trim().length === 0 || event.data === "[DONE]") continue;
|
|
981
|
+
yield parseEvent(event.data);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
697
984
|
|
|
698
|
-
// src/
|
|
699
|
-
var
|
|
700
|
-
|
|
701
|
-
|
|
985
|
+
// src/microvms/logs.ts
|
|
986
|
+
var RECONNECT_DELAYS_MS = [100, 250, 500, 1e3, 2e3];
|
|
987
|
+
var isRetryable = (error) => {
|
|
988
|
+
if (error instanceof ApiError) return error.status === 408 || error.status === 429 || error.status >= 500;
|
|
989
|
+
if (error instanceof Error && error.name === "AbortError") return false;
|
|
990
|
+
return true;
|
|
991
|
+
};
|
|
992
|
+
var parseJson = (data) => {
|
|
993
|
+
try {
|
|
994
|
+
const value = JSON.parse(data);
|
|
995
|
+
return value !== null && typeof value === "object" ? value : null;
|
|
996
|
+
} catch {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
async function* streamCommandLogs(open, options = {}) {
|
|
1001
|
+
const follow = options.follow ?? true;
|
|
1002
|
+
const reconnect = options.reconnect ?? true;
|
|
1003
|
+
const maxAttempts = options.maxReconnectAttempts ?? 5;
|
|
1004
|
+
const next = {
|
|
1005
|
+
stdout: options.stdoutOffset ?? 0,
|
|
1006
|
+
stderr: options.stderrOffset ?? 0
|
|
1007
|
+
};
|
|
1008
|
+
let failures = 0;
|
|
1009
|
+
for (; ; ) {
|
|
1010
|
+
let progressed = false;
|
|
1011
|
+
let serverError;
|
|
1012
|
+
try {
|
|
1013
|
+
const response = await open({
|
|
1014
|
+
follow,
|
|
1015
|
+
stdoutOffset: next.stdout,
|
|
1016
|
+
stderrOffset: next.stderr,
|
|
1017
|
+
signal: options.signal
|
|
1018
|
+
});
|
|
1019
|
+
for await (const event of parseSSEEvents(response)) {
|
|
1020
|
+
if (event.event === "stdout" || event.event === "stderr") {
|
|
1021
|
+
const payload = parseJson(event.data);
|
|
1022
|
+
if (!payload || typeof payload.data !== "string") continue;
|
|
1023
|
+
const stream = event.event;
|
|
1024
|
+
let data = base64ToBytes(payload.data);
|
|
1025
|
+
let offset = typeof payload.offset === "number" ? payload.offset : next[stream];
|
|
1026
|
+
progressed = true;
|
|
1027
|
+
if (offset < next[stream]) {
|
|
1028
|
+
const skip = next[stream] - offset;
|
|
1029
|
+
if (skip >= data.length) continue;
|
|
1030
|
+
data = data.subarray(skip);
|
|
1031
|
+
offset = next[stream];
|
|
1032
|
+
}
|
|
1033
|
+
next[stream] = offset + data.length;
|
|
1034
|
+
if (data.length > 0) yield { type: "output", stream, data, offset };
|
|
1035
|
+
} else if (event.event === "truncated") {
|
|
1036
|
+
progressed = true;
|
|
1037
|
+
yield { type: "truncated" };
|
|
1038
|
+
} else if (event.event === "end") {
|
|
1039
|
+
const payload = parseJson(event.data);
|
|
1040
|
+
yield { type: "end", reason: typeof payload?.reason === "string" ? payload.reason : "snapshot" };
|
|
1041
|
+
return;
|
|
1042
|
+
} else if (event.event === "exit") {
|
|
1043
|
+
const payload = parseJson(event.data) ?? {};
|
|
1044
|
+
yield {
|
|
1045
|
+
type: "exit",
|
|
1046
|
+
exitCode: typeof payload.exitCode === "number" ? payload.exitCode : null,
|
|
1047
|
+
signal: typeof payload.signal === "string" ? payload.signal : null,
|
|
1048
|
+
...typeof payload.timedOut === "boolean" && { timedOut: payload.timedOut },
|
|
1049
|
+
...(typeof payload.finishedAt === "string" || payload.finishedAt === null) && {
|
|
1050
|
+
finishedAt: payload.finishedAt
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
return;
|
|
1054
|
+
} else if (event.event === "error") {
|
|
1055
|
+
const payload = parseJson(event.data);
|
|
1056
|
+
const message = typeof payload?.message === "string" ? payload.message : typeof payload?.error === "string" ? payload.error : event.data;
|
|
1057
|
+
const failure = new ApiError(message, 502, typeof payload?.code === "string" ? payload.code : void 0);
|
|
1058
|
+
if (payload?.retryable !== true) serverError = failure;
|
|
1059
|
+
throw failure;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (!follow || !reconnect) return;
|
|
1063
|
+
const exit = progressed ? null : await options.resolveExit?.();
|
|
1064
|
+
if (exit) {
|
|
1065
|
+
yield { type: "exit", exitCode: exit.exitCode, signal: exit.signal };
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
if (error === serverError || options.signal?.aborted || !reconnect || !isRetryable(error)) throw error;
|
|
1070
|
+
if (!progressed && failures >= maxAttempts) throw error;
|
|
1071
|
+
}
|
|
1072
|
+
failures = progressed ? 0 : failures + 1;
|
|
1073
|
+
if (failures > maxAttempts) {
|
|
1074
|
+
throw new ApiError("The command log stream kept closing without output", 502, "log_stream_closed");
|
|
1075
|
+
}
|
|
1076
|
+
await sleep(RECONNECT_DELAYS_MS[Math.min(failures, RECONNECT_DELAYS_MS.length - 1)], options.signal);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/microvms/resource.ts
|
|
1081
|
+
var vmPath = (microvmId) => `/microvms/${encodeURIComponent(microvmId)}`;
|
|
1082
|
+
var commandPath = (microvmId, commandId) => `${vmPath(microvmId)}/commands/${encodeURIComponent(commandId)}`;
|
|
1083
|
+
var typed = (promise, filePath) => promise.catch((error) => {
|
|
1084
|
+
throw toMicroVmError(error, filePath);
|
|
1085
|
+
});
|
|
1086
|
+
var BUSY_RETRY_DELAYS_MS = [250, 500, 1e3, 2e3, 4e3];
|
|
1087
|
+
var read = async (run, signal, filePath) => {
|
|
1088
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1089
|
+
try {
|
|
1090
|
+
return await typed(run(), filePath);
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
if (!(error instanceof MicroVmBusyError) || attempt >= BUSY_RETRY_DELAYS_MS.length || signal?.aborted) {
|
|
1093
|
+
throw error;
|
|
1094
|
+
}
|
|
1095
|
+
await sleep(BUSY_RETRY_DELAYS_MS[attempt], signal);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
var unwrapList = (body, key = "data") => {
|
|
1100
|
+
if (Array.isArray(body)) return body;
|
|
1101
|
+
if (body && typeof body === "object") {
|
|
1102
|
+
const record = body;
|
|
1103
|
+
const list = record[key] ?? record.data ?? record.items;
|
|
1104
|
+
if (Array.isArray(list)) return list;
|
|
1105
|
+
}
|
|
1106
|
+
return [];
|
|
1107
|
+
};
|
|
1108
|
+
var normalizeCommand = (raw) => {
|
|
1109
|
+
const withId = raw.id === void 0 && raw.commandId !== void 0 ? { ...raw, id: raw.commandId } : raw;
|
|
1110
|
+
return typeof withId.timedOut === "boolean" ? withId : { ...withId, timedOut: false };
|
|
1111
|
+
};
|
|
1112
|
+
var MicroVmCommandsResource = class extends BaseResource {
|
|
1113
|
+
/**
|
|
1114
|
+
* Run a command and wait for it to exit (`POST /microvms/:id/commands`, `wait: true`).
|
|
1115
|
+
*
|
|
1116
|
+
* @example
|
|
1117
|
+
* ```ts
|
|
1118
|
+
* const { exitCode, stdout } = await client.microvms.commands.run(id, { cmd: 'node', args: ['-v'] });
|
|
1119
|
+
* ```
|
|
1120
|
+
*/
|
|
1121
|
+
run(microvmId, input, options = {}) {
|
|
1122
|
+
return typed(
|
|
1123
|
+
this.httpClient.post(`${vmPath(microvmId)}/commands`, { ...input, wait: true }, options)
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Start a detached command and return at once (`POST /microvms/:id/commands`, `wait: false`).
|
|
1128
|
+
* Follow its output with {@link logs}.
|
|
1129
|
+
*/
|
|
1130
|
+
async start(microvmId, input, options = {}) {
|
|
1131
|
+
const raw = await typed(
|
|
1132
|
+
this.httpClient.post(`${vmPath(microvmId)}/commands`, { ...input, wait: false }, options)
|
|
1133
|
+
);
|
|
1134
|
+
return normalizeCommand(raw);
|
|
1135
|
+
}
|
|
1136
|
+
/** List the microVM's commands, newest first (`GET /microvms/:id/commands`). */
|
|
1137
|
+
async list(microvmId, options = {}) {
|
|
1138
|
+
const body = await read(
|
|
1139
|
+
() => this.httpClient.get(`${vmPath(microvmId)}/commands`, options),
|
|
1140
|
+
options.signal
|
|
1141
|
+
);
|
|
1142
|
+
return unwrapList(body).map(normalizeCommand);
|
|
1143
|
+
}
|
|
1144
|
+
/** Get one command (`GET /microvms/:id/commands/:cmdId`). */
|
|
1145
|
+
async get(microvmId, commandId, options = {}) {
|
|
1146
|
+
return normalizeCommand(
|
|
1147
|
+
await read(() => this.httpClient.get(commandPath(microvmId, commandId), options), options.signal)
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Stream a command's output (`GET /microvms/:id/commands/:cmdId/logs`, SSE).
|
|
1152
|
+
*
|
|
1153
|
+
* Yields `output` events with the raw bytes and a final `exit` event. A
|
|
1154
|
+
* dropped connection is reopened from the last received offsets, so no byte
|
|
1155
|
+
* is lost or repeated; pass `reconnect: false` to disable that.
|
|
1156
|
+
*
|
|
1157
|
+
* @example
|
|
1158
|
+
* ```ts
|
|
1159
|
+
* for await (const event of client.microvms.commands.logs(id, commandId)) {
|
|
1160
|
+
* if (event.type === 'output') process.stdout.write(event.data);
|
|
1161
|
+
* else console.log('exit', event.exitCode);
|
|
1162
|
+
* }
|
|
1163
|
+
* ```
|
|
1164
|
+
*/
|
|
1165
|
+
logs(microvmId, commandId, options = {}) {
|
|
1166
|
+
return streamCommandLogs(
|
|
1167
|
+
({ follow, stdoutOffset, stderrOffset, signal }) => typed(
|
|
1168
|
+
this.httpClient.requestStream("GET", `${commandPath(microvmId, commandId)}/logs`, {
|
|
1169
|
+
query: { follow, stdoutOffset, stderrOffset },
|
|
1170
|
+
headers: { Accept: "text/event-stream" },
|
|
1171
|
+
signal
|
|
1172
|
+
})
|
|
1173
|
+
),
|
|
1174
|
+
{
|
|
1175
|
+
...options,
|
|
1176
|
+
resolveExit: options.resolveExit ?? (async () => {
|
|
1177
|
+
const info = await this.get(microvmId, commandId);
|
|
1178
|
+
return info.status === "exited" ? { exitCode: info.exitCode, signal: info.signal } : null;
|
|
1179
|
+
})
|
|
1180
|
+
}
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Wait up to `timeoutMs` (at most 60000) for the command to exit
|
|
1185
|
+
* (`POST /microvms/:id/commands/:cmdId/wait`). The returned command is still
|
|
1186
|
+
* `running` when the wait timed out.
|
|
1187
|
+
*/
|
|
1188
|
+
async wait(microvmId, commandId, options = {}) {
|
|
1189
|
+
const { signal, timeoutMs } = options;
|
|
1190
|
+
const raw = await read(
|
|
1191
|
+
() => this.httpClient.post(
|
|
1192
|
+
`${commandPath(microvmId, commandId)}/wait`,
|
|
1193
|
+
timeoutMs === void 0 ? {} : { timeoutMs },
|
|
1194
|
+
{ signal }
|
|
1195
|
+
),
|
|
1196
|
+
signal
|
|
1197
|
+
);
|
|
1198
|
+
return normalizeCommand(raw);
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Write to the command's stdin (`POST /microvms/:id/commands/:cmdId/stdin`).
|
|
1202
|
+
* The command must have been started with `stdin: true` or a `pty`.
|
|
1203
|
+
*
|
|
1204
|
+
* @param options.eof - Close stdin after writing.
|
|
1205
|
+
*/
|
|
1206
|
+
async stdin(microvmId, commandId, data, options = {}) {
|
|
1207
|
+
await typed(
|
|
1208
|
+
this.httpClient.post(`${commandPath(microvmId, commandId)}/stdin`, data, {
|
|
1209
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
1210
|
+
query: { eof: options.eof ? true : void 0 },
|
|
1211
|
+
signal: options.signal
|
|
1212
|
+
})
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
/** Send a signal to the command's process group (`POST /microvms/:id/commands/:cmdId/signal`). */
|
|
1216
|
+
async signal(microvmId, commandId, signal, options = {}) {
|
|
1217
|
+
await typed(this.httpClient.post(`${commandPath(microvmId, commandId)}/signal`, { signal }, options));
|
|
1218
|
+
}
|
|
1219
|
+
/** Resize a PTY command's terminal (`POST /microvms/:id/commands/:cmdId/resize`). */
|
|
1220
|
+
async resize(microvmId, commandId, size, options = {}) {
|
|
1221
|
+
await typed(
|
|
1222
|
+
this.httpClient.post(
|
|
1223
|
+
`${commandPath(microvmId, commandId)}/resize`,
|
|
1224
|
+
{ cols: size.cols, rows: size.rows },
|
|
1225
|
+
options
|
|
1226
|
+
)
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
var modeString = (mode) => typeof mode === "number" ? mode.toString(8).padStart(4, "0") : mode;
|
|
1231
|
+
var MicroVmFilesResource = class extends BaseResource {
|
|
1232
|
+
/** Read a file as bytes (`GET /microvms/:id/files/content`). */
|
|
1233
|
+
async read(microvmId, path, options = {}) {
|
|
1234
|
+
const response = await this.open(microvmId, path, options);
|
|
1235
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1236
|
+
}
|
|
1237
|
+
/** Read a file as a stream of bytes, without buffering it (`GET /microvms/:id/files/content`). */
|
|
1238
|
+
async readStream(microvmId, path, options = {}) {
|
|
1239
|
+
const response = await this.open(microvmId, path, options);
|
|
1240
|
+
if (!response.body) return new Blob([]).stream();
|
|
1241
|
+
return response.body;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Write a file, replacing it atomically (`PUT /microvms/:id/files/content`).
|
|
1245
|
+
* Missing parent directories are created unless `mkdirs: false`.
|
|
1246
|
+
*/
|
|
1247
|
+
write(microvmId, path, content, options = {}) {
|
|
1248
|
+
return typed(
|
|
1249
|
+
this.httpClient.put(`${vmPath(microvmId)}/files/content`, content, {
|
|
1250
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
1251
|
+
query: { path, mode: modeString(options.mode), mkdirs: options.mkdirs === false ? false : void 0 },
|
|
1252
|
+
signal: options.signal
|
|
1253
|
+
}),
|
|
1254
|
+
path
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* List a directory (`GET /microvms/:id/files`). `truncated` is set when the
|
|
1259
|
+
* listing hit its size limit; list a narrower directory then.
|
|
1260
|
+
*/
|
|
1261
|
+
async list(microvmId, path, options = {}) {
|
|
1262
|
+
const body = await read(
|
|
1263
|
+
() => this.httpClient.get(`${vmPath(microvmId)}/files`, {
|
|
1264
|
+
query: { path, recursive: options.recursive ? true : void 0, depth: options.depth },
|
|
1265
|
+
signal: options.signal
|
|
1266
|
+
}),
|
|
1267
|
+
options.signal,
|
|
1268
|
+
path
|
|
1269
|
+
);
|
|
1270
|
+
const truncated = body?.truncated === true;
|
|
1271
|
+
return { entries: unwrapList(body, "entries"), truncated };
|
|
1272
|
+
}
|
|
1273
|
+
/** Describe a file or directory without following a final symlink (`GET /microvms/:id/files/stat`). */
|
|
1274
|
+
stat(microvmId, path, options = {}) {
|
|
1275
|
+
return read(
|
|
1276
|
+
() => this.httpClient.get(`${vmPath(microvmId)}/files/stat`, { query: { path }, signal: options.signal }),
|
|
1277
|
+
options.signal,
|
|
1278
|
+
path
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
/** Create a directory, with its parents unless `parents: false` (`POST /microvms/:id/files/mkdir`). */
|
|
1282
|
+
mkdir(microvmId, path, options = {}) {
|
|
1283
|
+
const body = options.parents === void 0 ? { path } : { path, parents: options.parents };
|
|
1284
|
+
return typed(this.httpClient.post(`${vmPath(microvmId)}/files/mkdir`, body, options), path);
|
|
1285
|
+
}
|
|
1286
|
+
/** Move or rename a file or directory (`POST /microvms/:id/files/move`). */
|
|
1287
|
+
move(microvmId, from, to, options = {}) {
|
|
1288
|
+
return typed(this.httpClient.post(`${vmPath(microvmId)}/files/move`, { from, to }, options), from);
|
|
1289
|
+
}
|
|
1290
|
+
/** Delete a file, or a directory (non-empty ones need `recursive`) (`DELETE /microvms/:id/files`). */
|
|
1291
|
+
async remove(microvmId, path, options = {}) {
|
|
1292
|
+
await typed(
|
|
1293
|
+
this.httpClient.delete(`${vmPath(microvmId)}/files`, {
|
|
1294
|
+
query: { path, recursive: options.recursive ? true : void 0 },
|
|
1295
|
+
signal: options.signal
|
|
1296
|
+
}),
|
|
1297
|
+
path
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
open(microvmId, path, options) {
|
|
1301
|
+
return read(
|
|
1302
|
+
() => this.httpClient.requestStream("GET", `${vmPath(microvmId)}/files/content`, {
|
|
1303
|
+
query: { path, offset: options.offset, length: options.length },
|
|
1304
|
+
signal: options.signal
|
|
1305
|
+
}),
|
|
1306
|
+
options.signal,
|
|
1307
|
+
path
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
var toImageSource = (input) => {
|
|
1312
|
+
if (input.source) return input.source;
|
|
1313
|
+
const from = input.from;
|
|
1314
|
+
if (typeof from === "string") return { type: "registry", ref: from };
|
|
1315
|
+
if (from && "dockerfile" in from) return { type: "dockerfile", dockerfile: from.dockerfile };
|
|
1316
|
+
if (from && "registry" in from) return { type: "registry", ref: from.registry };
|
|
1317
|
+
throw new TypeError("Pass `from` (a registry reference or { dockerfile }) or `source` to create an image.");
|
|
1318
|
+
};
|
|
1319
|
+
var MicroVmImagesResource = class extends BaseResource {
|
|
1320
|
+
defaultApplicationId;
|
|
1321
|
+
constructor(httpClient, defaultApplicationId) {
|
|
1322
|
+
super(httpClient);
|
|
1323
|
+
this.defaultApplicationId = defaultApplicationId || void 0;
|
|
1324
|
+
}
|
|
1325
|
+
/** List the built-in images and the application's custom images (`GET /microvm-images`). */
|
|
1326
|
+
async list(query = {}, options = {}) {
|
|
1327
|
+
const body = await read(
|
|
1328
|
+
() => this.httpClient.get("/microvm-images", {
|
|
1329
|
+
query: { applicationId: query.applicationId ?? this.defaultApplicationId },
|
|
1330
|
+
signal: options.signal
|
|
1331
|
+
}),
|
|
1332
|
+
options.signal
|
|
1333
|
+
);
|
|
1334
|
+
return unwrapList(body);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Start building a custom image (`POST /microvm-images`). The image is
|
|
1338
|
+
* `building` until {@link waitUntilReady} resolves.
|
|
1339
|
+
*
|
|
1340
|
+
* @example
|
|
1341
|
+
* ```ts
|
|
1342
|
+
* const image = await client.microvms.images.create({ name: 'py-ml', from: 'python:3.13' });
|
|
1343
|
+
* await client.microvms.images.waitUntilReady(image.id);
|
|
1344
|
+
* const vm = await MicroVM.create({ image: 'py-ml' });
|
|
1345
|
+
* ```
|
|
1346
|
+
*/
|
|
1347
|
+
async create(input, options = {}) {
|
|
1348
|
+
const applicationId = input.applicationId ?? this.defaultApplicationId;
|
|
1349
|
+
const source = toImageSource(input);
|
|
1350
|
+
return await typed(
|
|
1351
|
+
this.httpClient.post(
|
|
1352
|
+
"/microvm-images",
|
|
1353
|
+
{ ...applicationId ? { applicationId } : {}, name: input.name, source },
|
|
1354
|
+
options
|
|
1355
|
+
)
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
/** Get one image (`GET /microvm-images/:id`). */
|
|
1359
|
+
get(imageId, options = {}) {
|
|
1360
|
+
return read(
|
|
1361
|
+
() => this.httpClient.get(`/microvm-images/${encodeURIComponent(imageId)}`, options),
|
|
1362
|
+
options.signal
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
/** Delete a custom image (`DELETE /microvm-images/:id`). */
|
|
1366
|
+
async delete(imageId, options = {}) {
|
|
1367
|
+
await typed(this.httpClient.delete(`/microvm-images/${encodeURIComponent(imageId)}`, options));
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Poll an image until its build finished.
|
|
1371
|
+
*
|
|
1372
|
+
* @throws {@link MicroVmImageBuildError} when the build failed.
|
|
1373
|
+
* @throws {@link MicroVmTimeoutError} when it is still building after `timeout`.
|
|
1374
|
+
*/
|
|
1375
|
+
async waitUntilReady(imageId, options = {}) {
|
|
1376
|
+
const deadline = Date.now() + durationToMs(options.timeout ?? "15m");
|
|
1377
|
+
const interval = durationToMs(options.interval ?? "2s");
|
|
1378
|
+
let lastStatus;
|
|
1379
|
+
for (; ; ) {
|
|
1380
|
+
if (options.signal?.aborted) throw abortReason(options.signal);
|
|
1381
|
+
const image = await this.get(imageId, { signal: options.signal });
|
|
1382
|
+
if (image.status !== lastStatus) {
|
|
1383
|
+
lastStatus = image.status;
|
|
1384
|
+
options.onStatus?.(image);
|
|
1385
|
+
}
|
|
1386
|
+
if (image.status === "ready") return image;
|
|
1387
|
+
if (image.status === "failed") throw new MicroVmImageBuildError(image.id, image.error);
|
|
1388
|
+
if (Date.now() + interval > deadline) {
|
|
1389
|
+
throw new MicroVmTimeoutError(`The image ${image.name} is still ${image.status}; gave up waiting.`);
|
|
1390
|
+
}
|
|
1391
|
+
await sleep(interval, options.signal);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
var MicroVmsResource = class extends BaseResource {
|
|
1396
|
+
/** Commands: run, start, logs, wait, stdin, signal, resize. */
|
|
1397
|
+
commands;
|
|
1398
|
+
/** Files: read, write, list, stat, mkdir, move, remove. */
|
|
1399
|
+
files;
|
|
1400
|
+
/** Built-in and custom images. */
|
|
1401
|
+
images;
|
|
1402
|
+
defaultApplicationId;
|
|
1403
|
+
constructor(httpClient, defaultApplicationId) {
|
|
1404
|
+
super(httpClient);
|
|
1405
|
+
this.defaultApplicationId = defaultApplicationId || void 0;
|
|
1406
|
+
this.commands = new MicroVmCommandsResource(httpClient);
|
|
1407
|
+
this.files = new MicroVmFilesResource(httpClient);
|
|
1408
|
+
this.images = new MicroVmImagesResource(httpClient, defaultApplicationId);
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Create a microVM (`POST /microvms`). Resolves once it is `running`; with
|
|
1412
|
+
* `wait: false` it resolves at once with a `starting` microVM.
|
|
1413
|
+
*
|
|
1414
|
+
* `applicationId` defaults to the client's `applicationId` or
|
|
1415
|
+
* `GIGADRIVE_APPLICATION_ID`, and may be omitted entirely with application
|
|
1416
|
+
* or deployment credentials.
|
|
1417
|
+
*/
|
|
1418
|
+
create(input = {}, options = {}) {
|
|
1419
|
+
const applicationId = input.applicationId ?? this.defaultApplicationId;
|
|
1420
|
+
return typed(
|
|
1421
|
+
this.httpClient.post("/microvms", applicationId ? { ...input, applicationId } : input, {
|
|
1422
|
+
query: { wait: options.wait === false ? false : void 0 },
|
|
1423
|
+
signal: options.signal
|
|
1424
|
+
})
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
/** List one page of microVMs, newest first (`GET /microvms`). */
|
|
1428
|
+
async list(query = {}, options = {}) {
|
|
1429
|
+
const body = await read(
|
|
1430
|
+
() => this.httpClient.get("/microvms", {
|
|
1431
|
+
query: {
|
|
1432
|
+
applicationId: query.applicationId ?? this.defaultApplicationId,
|
|
1433
|
+
status: query.status,
|
|
1434
|
+
cursor: query.cursor,
|
|
1435
|
+
limit: query.limit
|
|
1436
|
+
},
|
|
1437
|
+
signal: options.signal
|
|
1438
|
+
}),
|
|
1439
|
+
options.signal
|
|
1440
|
+
);
|
|
1441
|
+
const nextCursor = body.pagination?.nextCursor ?? body.nextCursor ?? void 0;
|
|
1442
|
+
const hasMore = body.pagination?.hasMore ?? nextCursor !== void 0;
|
|
1443
|
+
return {
|
|
1444
|
+
items: body.data ?? body.items ?? [],
|
|
1445
|
+
...nextCursor && hasMore ? { nextCursor } : {},
|
|
1446
|
+
hasMore: hasMore && nextCursor !== void 0
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Iterate every microVM across all pages.
|
|
1451
|
+
*
|
|
1452
|
+
* @example
|
|
1453
|
+
* ```ts
|
|
1454
|
+
* for await (const vm of client.microvms.listAll({ status: 'running' })) console.log(vm.id);
|
|
1455
|
+
* ```
|
|
1456
|
+
*/
|
|
1457
|
+
async *listAll(query = {}, options = {}) {
|
|
1458
|
+
let cursor;
|
|
1459
|
+
do {
|
|
1460
|
+
const page = await this.list({ ...query, cursor }, options);
|
|
1461
|
+
yield* page.items;
|
|
1462
|
+
cursor = page.nextCursor;
|
|
1463
|
+
} while (cursor);
|
|
1464
|
+
}
|
|
1465
|
+
/** Get one microVM (`GET /microvms/:id`). */
|
|
1466
|
+
get(microvmId, options = {}) {
|
|
1467
|
+
return read(() => this.httpClient.get(vmPath(microvmId), options), options.signal);
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Set the remaining lifetime to `timeoutMs` from now (`POST /microvms/:id/timeout`),
|
|
1471
|
+
* up to the plan's maximum lifetime.
|
|
1472
|
+
*/
|
|
1473
|
+
extendTimeout(microvmId, timeoutMs, options = {}) {
|
|
1474
|
+
return typed(this.httpClient.post(`${vmPath(microvmId)}/timeout`, { timeoutMs }, options));
|
|
1475
|
+
}
|
|
1476
|
+
/** Stop a microVM (`POST /microvms/:id/stop`). Its disk and memory are discarded. */
|
|
1477
|
+
stop(microvmId, options = {}) {
|
|
1478
|
+
return typed(this.httpClient.post(`${vmPath(microvmId)}/stop`, void 0, options));
|
|
1479
|
+
}
|
|
1480
|
+
/** Delete a microVM, stopping it first if needed (`DELETE /microvms/:id`). */
|
|
1481
|
+
async delete(microvmId, options = {}) {
|
|
1482
|
+
await typed(this.httpClient.delete(vmPath(microvmId), options));
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Request a public preview URL with the client's `fetch`, without
|
|
1486
|
+
* credentials and without throwing on error statuses. Used by
|
|
1487
|
+
* `MicroVM.waitForPort`.
|
|
1488
|
+
*
|
|
1489
|
+
* @internal
|
|
1490
|
+
*/
|
|
1491
|
+
fetchPreview(url, init) {
|
|
1492
|
+
return this.httpClient.fetchPublic(url, init);
|
|
702
1493
|
}
|
|
703
|
-
httpClient;
|
|
704
1494
|
};
|
|
705
1495
|
|
|
706
1496
|
// src/resources/ai-gateway/index.ts
|
|
@@ -985,7 +1775,7 @@ var ApplicationRequestsResource = class extends BaseResource {
|
|
|
985
1775
|
|
|
986
1776
|
// src/upload/checksum.ts
|
|
987
1777
|
var EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
988
|
-
var
|
|
1778
|
+
var toHex2 = (buffer) => {
|
|
989
1779
|
const bytes = new Uint8Array(buffer);
|
|
990
1780
|
let hex = "";
|
|
991
1781
|
for (let i = 0; i < bytes.length; i++) {
|
|
@@ -993,7 +1783,7 @@ var toHex = (buffer) => {
|
|
|
993
1783
|
}
|
|
994
1784
|
return hex;
|
|
995
1785
|
};
|
|
996
|
-
var subtleDigest = async (algorithm, bytes) =>
|
|
1786
|
+
var subtleDigest = async (algorithm, bytes) => toHex2(await crypto.subtle.digest(algorithm, bytes));
|
|
997
1787
|
var importNodeCrypto = async () => {
|
|
998
1788
|
if (typeof process === "undefined" || !process.versions?.node) return null;
|
|
999
1789
|
try {
|
|
@@ -1802,7 +2592,7 @@ var pollSignal = (ms, signal) => {
|
|
|
1802
2592
|
}
|
|
1803
2593
|
};
|
|
1804
2594
|
};
|
|
1805
|
-
var
|
|
2595
|
+
var sleep2 = (ms, signal) => new Promise((resolve, reject) => {
|
|
1806
2596
|
if (signal?.aborted) {
|
|
1807
2597
|
reject(signal.reason instanceof Error ? signal.reason : new GigadriveError("Aborted"));
|
|
1808
2598
|
return;
|
|
@@ -1926,7 +2716,7 @@ var ApplicationDomainsResource = class extends BaseResource {
|
|
|
1926
2716
|
if (last) throw new DomainNotActiveError(last, "timeout");
|
|
1927
2717
|
throw new GigadriveError("Timed out before the domain could be read");
|
|
1928
2718
|
}
|
|
1929
|
-
await
|
|
2719
|
+
await sleep2(Math.min(intervalMs, untilDeadline), signal);
|
|
1930
2720
|
}
|
|
1931
2721
|
}
|
|
1932
2722
|
};
|
|
@@ -2712,48 +3502,9 @@ var OrganizationsResource = class extends BaseResource {
|
|
|
2712
3502
|
* console.log(org.id, org.slug);
|
|
2713
3503
|
* ```
|
|
2714
3504
|
*/
|
|
2715
|
-
async create(input) {
|
|
2716
|
-
return this.httpClient.post("/organizations", input);
|
|
2717
|
-
}
|
|
2718
|
-
};
|
|
2719
|
-
|
|
2720
|
-
// src/queues/encoding.ts
|
|
2721
|
-
var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
|
|
2722
|
-
var UNIT_SECONDS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
|
|
2723
|
-
var durationToSeconds = (value) => {
|
|
2724
|
-
let seconds;
|
|
2725
|
-
if (typeof value === "number") {
|
|
2726
|
-
seconds = value;
|
|
2727
|
-
} else {
|
|
2728
|
-
const match = DURATION_PATTERN.exec(value.trim());
|
|
2729
|
-
if (!match) throw new Error(`Invalid duration "${value}". Use seconds or a string such as 30s, 10m, 12h or 7d.`);
|
|
2730
|
-
seconds = Number(match[1]) * UNIT_SECONDS[match[2]];
|
|
2731
|
-
}
|
|
2732
|
-
if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid duration "${String(value)}"`);
|
|
2733
|
-
return Math.ceil(seconds);
|
|
2734
|
-
};
|
|
2735
|
-
var textEncoder = new TextEncoder();
|
|
2736
|
-
var textDecoder = new TextDecoder();
|
|
2737
|
-
var utf8Encode = (value) => textEncoder.encode(value);
|
|
2738
|
-
var utf8Decode = (bytes) => textDecoder.decode(bytes);
|
|
2739
|
-
var bytesToBase64 = (bytes) => {
|
|
2740
|
-
let binary = "";
|
|
2741
|
-
for (let index = 0; index < bytes.length; index += 32768) {
|
|
2742
|
-
binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
|
|
3505
|
+
async create(input) {
|
|
3506
|
+
return this.httpClient.post("/organizations", input);
|
|
2743
3507
|
}
|
|
2744
|
-
return btoa(binary);
|
|
2745
|
-
};
|
|
2746
|
-
var base64ToBytes = (value) => {
|
|
2747
|
-
const binary = atob(value);
|
|
2748
|
-
const bytes = new Uint8Array(binary.length);
|
|
2749
|
-
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
2750
|
-
return bytes;
|
|
2751
|
-
};
|
|
2752
|
-
var toHex2 = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2753
|
-
var toArrayBuffer = (bytes) => {
|
|
2754
|
-
const copy = new ArrayBuffer(bytes.byteLength);
|
|
2755
|
-
new Uint8Array(copy).set(bytes);
|
|
2756
|
-
return copy;
|
|
2757
3508
|
};
|
|
2758
3509
|
|
|
2759
3510
|
// src/queues/errors.ts
|
|
@@ -2785,11 +3536,11 @@ var hmacSha256Hex = async (secret, payload) => {
|
|
|
2785
3536
|
false,
|
|
2786
3537
|
["sign"]
|
|
2787
3538
|
);
|
|
2788
|
-
return
|
|
3539
|
+
return toHex(await crypto.subtle.sign("HMAC", key, toArrayBuffer(utf8Encode(payload))));
|
|
2789
3540
|
};
|
|
2790
3541
|
var signedPayload = async (input, timestamp) => {
|
|
2791
3542
|
const body = typeof input.body === "string" ? utf8Encode(input.body) : input.body;
|
|
2792
|
-
const bodyHash =
|
|
3543
|
+
const bodyHash = toHex(await crypto.subtle.digest("SHA-256", toArrayBuffer(body)));
|
|
2793
3544
|
return `${String(timestamp)}.${input.queue}.${input.messageId}.${String(input.attempt)}.${bodyHash}`;
|
|
2794
3545
|
};
|
|
2795
3546
|
var safeEqual = (left, right) => {
|
|
@@ -2872,7 +3623,7 @@ var ACK_FLUSH_MS = 25;
|
|
|
2872
3623
|
var ACK_RETRY_DELAYS_MS = [200, 1e3];
|
|
2873
3624
|
var EMPTY_POLL_MIN_MS = 1e3;
|
|
2874
3625
|
var EMPTY_POLL_MAX_MS = 5e3;
|
|
2875
|
-
var
|
|
3626
|
+
var sleep3 = (ms, signal) => new Promise((resolve) => {
|
|
2876
3627
|
if (signal?.aborted) return resolve();
|
|
2877
3628
|
const timer = setTimeout(done, ms);
|
|
2878
3629
|
function done() {
|
|
@@ -3067,7 +3818,7 @@ var Queue = class {
|
|
|
3067
3818
|
const retryable = !(error instanceof ApiError) || error.status >= 500 || error.status === 429;
|
|
3068
3819
|
const delay2 = ACK_RETRY_DELAYS_MS[attempt];
|
|
3069
3820
|
if (!retryable || delay2 === void 0) throw error;
|
|
3070
|
-
await
|
|
3821
|
+
await sleep3(delay2, void 0);
|
|
3071
3822
|
}
|
|
3072
3823
|
}
|
|
3073
3824
|
};
|
|
@@ -3107,7 +3858,7 @@ var Queue = class {
|
|
|
3107
3858
|
if (messages.length === 0) {
|
|
3108
3859
|
if (options.stopWhenEmpty) return;
|
|
3109
3860
|
const remaining = Math.max(waitMs, EMPTY_POLL_MIN_MS) - (Date.now() - started);
|
|
3110
|
-
if (remaining > 0) await
|
|
3861
|
+
if (remaining > 0) await sleep3(Math.min(remaining, EMPTY_POLL_MAX_MS), options.signal);
|
|
3111
3862
|
continue;
|
|
3112
3863
|
}
|
|
3113
3864
|
await Promise.all(
|
|
@@ -3543,6 +4294,12 @@ var GigadriveClient = class {
|
|
|
3543
4294
|
* handle bound to one queue.
|
|
3544
4295
|
*/
|
|
3545
4296
|
queues;
|
|
4297
|
+
/**
|
|
4298
|
+
* microVMs: isolated Linux machines to run commands in, move files to and
|
|
4299
|
+
* from, and expose ports from. This is the low-level REST surface; the
|
|
4300
|
+
* {@link MicroVM} class (`MicroVM.create({ client })`) is the friendlier API.
|
|
4301
|
+
*/
|
|
4302
|
+
microvms;
|
|
3546
4303
|
constructor(config = {}) {
|
|
3547
4304
|
const baseUrl = config.baseUrl ?? readEnv("GIGADRIVE_API_BASE_URL") ?? DEFAULT_BASE_URL2;
|
|
3548
4305
|
const applicationId = config.applicationId ?? readEnv("GIGADRIVE_APPLICATION_ID");
|
|
@@ -3559,6 +4316,687 @@ var GigadriveClient = class {
|
|
|
3559
4316
|
this.stickySessions = new StickySessionsResource(httpClient);
|
|
3560
4317
|
this.imageOptimization = new ImageOptimizationResource(httpClient);
|
|
3561
4318
|
this.queues = new QueuesResource(httpClient, applicationId);
|
|
4319
|
+
this.microvms = new MicroVmsResource(httpClient, applicationId);
|
|
4320
|
+
}
|
|
4321
|
+
};
|
|
4322
|
+
|
|
4323
|
+
// src/microvms/command.ts
|
|
4324
|
+
var MAX_WAIT_MS = 6e4;
|
|
4325
|
+
var toMs = (value) => {
|
|
4326
|
+
if (value === null || value === void 0) return null;
|
|
4327
|
+
const ms = typeof value === "number" ? value : Date.parse(value);
|
|
4328
|
+
return Number.isFinite(ms) ? ms : null;
|
|
4329
|
+
};
|
|
4330
|
+
var commandDurationMs = (info) => {
|
|
4331
|
+
const started = toMs(info.startedAt);
|
|
4332
|
+
const finished = toMs(info.finishedAt);
|
|
4333
|
+
return started !== null && finished !== null ? Math.max(0, finished - started) : null;
|
|
4334
|
+
};
|
|
4335
|
+
var Command = class {
|
|
4336
|
+
/** @internal Use {@link MicroVM.spawn} or `vm.commands.get(id)`. */
|
|
4337
|
+
constructor(resource, microvmId, info) {
|
|
4338
|
+
this.resource = resource;
|
|
4339
|
+
this.microvmId = microvmId;
|
|
4340
|
+
this.current = info;
|
|
4341
|
+
}
|
|
4342
|
+
resource;
|
|
4343
|
+
microvmId;
|
|
4344
|
+
current;
|
|
4345
|
+
pump;
|
|
4346
|
+
pumpError;
|
|
4347
|
+
skippedOutput = false;
|
|
4348
|
+
/** Command id (`cmd_...`). Pass it to `vm.commands.get(id)` to reattach later. */
|
|
4349
|
+
get id() {
|
|
4350
|
+
return this.current.id;
|
|
4351
|
+
}
|
|
4352
|
+
/** The latest known state. Updated by {@link refresh}, {@link wait} and the end of {@link logs}. */
|
|
4353
|
+
get info() {
|
|
4354
|
+
return this.current;
|
|
4355
|
+
}
|
|
4356
|
+
get status() {
|
|
4357
|
+
return this.current.status;
|
|
4358
|
+
}
|
|
4359
|
+
/** Exit code once exited; `null` while running or when a signal killed it. */
|
|
4360
|
+
get exitCode() {
|
|
4361
|
+
return this.current.exitCode;
|
|
4362
|
+
}
|
|
4363
|
+
/** Signal that killed the process, if any. */
|
|
4364
|
+
get signal() {
|
|
4365
|
+
return this.current.signal;
|
|
4366
|
+
}
|
|
4367
|
+
get pid() {
|
|
4368
|
+
return this.current.pid;
|
|
4369
|
+
}
|
|
4370
|
+
/** Whether the command was killed because it ran past its `timeout`. */
|
|
4371
|
+
get timedOut() {
|
|
4372
|
+
return this.current.timedOut;
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Whether a {@link logs} read found older output already dropped (each
|
|
4376
|
+
* stream keeps its last 8 MiB), so what it delivered starts mid-stream.
|
|
4377
|
+
*/
|
|
4378
|
+
get truncated() {
|
|
4379
|
+
return this.skippedOutput;
|
|
4380
|
+
}
|
|
4381
|
+
/** Fetch the latest state from the API. */
|
|
4382
|
+
async refresh() {
|
|
4383
|
+
this.current = await this.resource.get(this.microvmId, this.id);
|
|
4384
|
+
return this.current;
|
|
4385
|
+
}
|
|
4386
|
+
/**
|
|
4387
|
+
* Stream the command's output, from the beginning of the retained output by
|
|
4388
|
+
* default, until it exits. With `follow: false` it delivers what is
|
|
4389
|
+
* buffered and ends, even while the command keeps running. Reconnects transparently after a dropped
|
|
4390
|
+
* connection. Breaking out of the loop closes the stream; the command keeps
|
|
4391
|
+
* running.
|
|
4392
|
+
*
|
|
4393
|
+
* @example
|
|
4394
|
+
* ```ts
|
|
4395
|
+
* for await (const { stream, text } of command.logs()) {
|
|
4396
|
+
* (stream === 'stdout' ? process.stdout : process.stderr).write(text);
|
|
4397
|
+
* }
|
|
4398
|
+
* ```
|
|
4399
|
+
*/
|
|
4400
|
+
async *logs(options = {}) {
|
|
4401
|
+
const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() };
|
|
4402
|
+
for await (const event of this.resource.logs(this.microvmId, this.id, options)) {
|
|
4403
|
+
if (event.type === "exit") {
|
|
4404
|
+
this.current = {
|
|
4405
|
+
...this.current,
|
|
4406
|
+
status: "exited",
|
|
4407
|
+
exitCode: event.exitCode,
|
|
4408
|
+
signal: event.signal,
|
|
4409
|
+
...event.timedOut !== void 0 && { timedOut: event.timedOut },
|
|
4410
|
+
...event.finishedAt !== void 0 && { finishedAt: event.finishedAt }
|
|
4411
|
+
};
|
|
4412
|
+
continue;
|
|
4413
|
+
}
|
|
4414
|
+
if (event.type === "truncated") {
|
|
4415
|
+
this.skippedOutput = true;
|
|
4416
|
+
continue;
|
|
4417
|
+
}
|
|
4418
|
+
if (event.type === "end") continue;
|
|
4419
|
+
const text = decoders[event.stream].decode(event.data, { stream: true });
|
|
4420
|
+
yield { stream: event.stream, data: event.data, text, offset: event.offset };
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
/**
|
|
4424
|
+
* Collect the output from the start until the command exits.
|
|
4425
|
+
*
|
|
4426
|
+
* @example
|
|
4427
|
+
* ```ts
|
|
4428
|
+
* const { stdout } = await (await vm.spawn('ls', ['-la'])).output();
|
|
4429
|
+
* ```
|
|
4430
|
+
*/
|
|
4431
|
+
async output(options = {}) {
|
|
4432
|
+
let stdout = "";
|
|
4433
|
+
let stderr = "";
|
|
4434
|
+
for await (const chunk of this.logs(options)) {
|
|
4435
|
+
if (chunk.stream === "stdout") stdout += chunk.text;
|
|
4436
|
+
else stderr += chunk.text;
|
|
4437
|
+
}
|
|
4438
|
+
return { stdout, stderr };
|
|
4439
|
+
}
|
|
4440
|
+
/** The complete stdout as text, once the command exited. */
|
|
4441
|
+
async stdout(options = {}) {
|
|
4442
|
+
return (await this.output(options)).stdout;
|
|
4443
|
+
}
|
|
4444
|
+
/** The complete stderr as text, once the command exited. */
|
|
4445
|
+
async stderr(options = {}) {
|
|
4446
|
+
return (await this.output(options)).stderr;
|
|
4447
|
+
}
|
|
4448
|
+
/**
|
|
4449
|
+
* Wait for the command to exit and return its final state. Output callbacks
|
|
4450
|
+
* given to `spawn` have received all output when this resolves.
|
|
4451
|
+
*
|
|
4452
|
+
* @throws {@link MicroVmTimeoutError} when `timeout` elapses first (the command keeps running).
|
|
4453
|
+
* @throws {@link CommandFailedError} with `throwOnError` when it exits non-zero or is killed.
|
|
4454
|
+
*/
|
|
4455
|
+
async wait(options = {}) {
|
|
4456
|
+
const deadline = options.timeout === void 0 ? Infinity : Date.now() + durationToMs(options.timeout);
|
|
4457
|
+
while (this.current.status !== "exited") {
|
|
4458
|
+
const remaining = deadline - Date.now();
|
|
4459
|
+
if (remaining <= 0) {
|
|
4460
|
+
throw new MicroVmTimeoutError(`The command ${this.id} is still running; gave up waiting.`);
|
|
4461
|
+
}
|
|
4462
|
+
this.current = await this.resource.wait(this.microvmId, this.id, {
|
|
4463
|
+
timeoutMs: Math.min(MAX_WAIT_MS, remaining),
|
|
4464
|
+
signal: options.signal
|
|
4465
|
+
});
|
|
4466
|
+
}
|
|
4467
|
+
if (this.pump) {
|
|
4468
|
+
await this.pump;
|
|
4469
|
+
if (this.pumpError) throw this.pumpError;
|
|
4470
|
+
}
|
|
4471
|
+
if (options.throwOnError && this.current.exitCode !== 0) {
|
|
4472
|
+
throw CommandFailedError.fromInfo(this.current);
|
|
4473
|
+
}
|
|
4474
|
+
return this.current;
|
|
4475
|
+
}
|
|
4476
|
+
/**
|
|
4477
|
+
* Send a signal to the command's process group. Default `SIGTERM`; use
|
|
4478
|
+
* `SIGKILL` to stop it unconditionally.
|
|
4479
|
+
*/
|
|
4480
|
+
async kill(signal = "SIGTERM") {
|
|
4481
|
+
await this.resource.signal(this.microvmId, this.id, signal);
|
|
4482
|
+
}
|
|
4483
|
+
/**
|
|
4484
|
+
* Write to stdin. The command must have been spawned with `stdin: true` or a `pty`.
|
|
4485
|
+
*
|
|
4486
|
+
* @example
|
|
4487
|
+
* ```ts
|
|
4488
|
+
* const cat = await vm.spawn('cat', [], { stdin: true });
|
|
4489
|
+
* await cat.write('hello\n');
|
|
4490
|
+
* await cat.end();
|
|
4491
|
+
* console.log(await cat.stdout()); // "hello\n"
|
|
4492
|
+
* ```
|
|
4493
|
+
*/
|
|
4494
|
+
async write(data) {
|
|
4495
|
+
await this.resource.stdin(this.microvmId, this.id, data);
|
|
4496
|
+
}
|
|
4497
|
+
/** Close stdin, optionally writing a last piece of data first. */
|
|
4498
|
+
async end(data = new Uint8Array(0)) {
|
|
4499
|
+
await this.resource.stdin(this.microvmId, this.id, data, { eof: true });
|
|
4500
|
+
}
|
|
4501
|
+
/** Resize the terminal of a PTY command. */
|
|
4502
|
+
async resize(cols, rows) {
|
|
4503
|
+
await this.resource.resize(this.microvmId, this.id, { cols, rows });
|
|
4504
|
+
}
|
|
4505
|
+
/** JSON form: the command's latest state. */
|
|
4506
|
+
toJSON() {
|
|
4507
|
+
return this.current;
|
|
4508
|
+
}
|
|
4509
|
+
/**
|
|
4510
|
+
* Deliver output to callbacks in the background, for `spawn` with
|
|
4511
|
+
* `onStdout`/`onStderr`. {@link wait} waits for it and rethrows its error.
|
|
4512
|
+
*
|
|
4513
|
+
* @internal
|
|
4514
|
+
*/
|
|
4515
|
+
startOutputPump(onStdout, onStderr) {
|
|
4516
|
+
this.pump = (async () => {
|
|
4517
|
+
for await (const chunk of this.logs()) {
|
|
4518
|
+
await (chunk.stream === "stdout" ? onStdout : onStderr)?.(chunk.text, chunk);
|
|
4519
|
+
}
|
|
4520
|
+
})().catch((error) => {
|
|
4521
|
+
this.pumpError = error instanceof Error ? error : new Error(String(error));
|
|
4522
|
+
});
|
|
4523
|
+
}
|
|
4524
|
+
};
|
|
4525
|
+
|
|
4526
|
+
// src/microvms/microvm.ts
|
|
4527
|
+
var defaultClient;
|
|
4528
|
+
var resolveClient = (client) => {
|
|
4529
|
+
if (client instanceof GigadriveClient) return client;
|
|
4530
|
+
if (client) return new GigadriveClient(client);
|
|
4531
|
+
defaultClient ??= new GigadriveClient();
|
|
4532
|
+
return defaultClient;
|
|
4533
|
+
};
|
|
4534
|
+
var CREATE_WAIT_TIMEOUT = "10m";
|
|
4535
|
+
var NOT_LISTENING = /* @__PURE__ */ new Set([502, 503, 504]);
|
|
4536
|
+
var PROBE_TIMEOUT_MS = 5e3;
|
|
4537
|
+
var TERMINAL = ["stopping", "stopped", "failed"];
|
|
4538
|
+
var toStartInput = (cmd, args, options) => {
|
|
4539
|
+
const user = options.sudo ? "root" : options.user;
|
|
4540
|
+
return {
|
|
4541
|
+
cmd,
|
|
4542
|
+
...args ? { args } : {},
|
|
4543
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
4544
|
+
...options.env ? { env: options.env } : {},
|
|
4545
|
+
...user !== void 0 ? { user } : {},
|
|
4546
|
+
...options.timeout !== void 0 ? { timeoutMs: durationToMs(options.timeout) } : {}
|
|
4547
|
+
};
|
|
4548
|
+
};
|
|
4549
|
+
var failIfRequested = (result, throwOnError) => {
|
|
4550
|
+
if (throwOnError && result.exitCode !== 0) throw new CommandFailedError(result);
|
|
4551
|
+
return result;
|
|
4552
|
+
};
|
|
4553
|
+
var MicroVmFiles = class {
|
|
4554
|
+
/** @internal */
|
|
4555
|
+
constructor(resource, microvmId) {
|
|
4556
|
+
this.resource = resource;
|
|
4557
|
+
this.microvmId = microvmId;
|
|
4558
|
+
}
|
|
4559
|
+
resource;
|
|
4560
|
+
microvmId;
|
|
4561
|
+
/** Read a file as bytes. @throws {@link MicroVmFileNotFoundError} */
|
|
4562
|
+
read(path, options) {
|
|
4563
|
+
return this.resource.read(this.microvmId, path, options);
|
|
4564
|
+
}
|
|
4565
|
+
/** Read a file as UTF-8 text. */
|
|
4566
|
+
async readText(path, options) {
|
|
4567
|
+
return new TextDecoder().decode(await this.read(path, options));
|
|
4568
|
+
}
|
|
4569
|
+
/** Read a file as a stream, for files too large to buffer. */
|
|
4570
|
+
readStream(path, options) {
|
|
4571
|
+
return this.resource.readStream(this.microvmId, path, options);
|
|
4572
|
+
}
|
|
4573
|
+
/**
|
|
4574
|
+
* Write a file atomically, creating parent directories.
|
|
4575
|
+
*
|
|
4576
|
+
* @example
|
|
4577
|
+
* ```ts
|
|
4578
|
+
* await vm.files.write('/app/index.js', 'console.log("hi")');
|
|
4579
|
+
* await vm.files.write('/app/logo.png', await (await fetch(url)).blob());
|
|
4580
|
+
* ```
|
|
4581
|
+
*/
|
|
4582
|
+
write(path, content, options) {
|
|
4583
|
+
return this.resource.write(this.microvmId, path, content, options);
|
|
4584
|
+
}
|
|
4585
|
+
/**
|
|
4586
|
+
* Write several files, a few at a time. Resolves in input order once all
|
|
4587
|
+
* succeeded; rejects with the first failure.
|
|
4588
|
+
*
|
|
4589
|
+
* @example
|
|
4590
|
+
* ```ts
|
|
4591
|
+
* await vm.files.writeMany([
|
|
4592
|
+
* { path: 'app/package.json', content: JSON.stringify(pkg) },
|
|
4593
|
+
* { path: 'app/run.sh', content: '#!/bin/sh\nnode .', mode: '0755' },
|
|
4594
|
+
* ]);
|
|
4595
|
+
* ```
|
|
4596
|
+
*/
|
|
4597
|
+
async writeMany(files, options = {}) {
|
|
4598
|
+
const results = new Array(files.length);
|
|
4599
|
+
let next = 0;
|
|
4600
|
+
const worker = async () => {
|
|
4601
|
+
while (next < files.length) {
|
|
4602
|
+
const index = next++;
|
|
4603
|
+
const file = files[index];
|
|
4604
|
+
results[index] = await this.write(file.path, file.content, { mode: file.mode, signal: options.signal });
|
|
4605
|
+
}
|
|
4606
|
+
};
|
|
4607
|
+
const workers = Math.max(1, Math.min(options.concurrency ?? 4, files.length));
|
|
4608
|
+
await Promise.all(Array.from({ length: workers }, worker));
|
|
4609
|
+
return results;
|
|
4610
|
+
}
|
|
4611
|
+
/**
|
|
4612
|
+
* List a directory. `recursive` descends into subdirectories (up to `depth`
|
|
4613
|
+
* levels). Very large listings are cut off; `client.microvms.files.list`
|
|
4614
|
+
* reports that as `truncated`.
|
|
4615
|
+
*/
|
|
4616
|
+
async list(path = ".", options) {
|
|
4617
|
+
return (await this.resource.list(this.microvmId, path, options)).entries;
|
|
4618
|
+
}
|
|
4619
|
+
/** Describe a file or directory. @throws {@link MicroVmFileNotFoundError} */
|
|
4620
|
+
stat(path) {
|
|
4621
|
+
return this.resource.stat(this.microvmId, path);
|
|
4622
|
+
}
|
|
4623
|
+
/** Whether a file or directory exists. */
|
|
4624
|
+
async exists(path) {
|
|
4625
|
+
try {
|
|
4626
|
+
await this.stat(path);
|
|
4627
|
+
return true;
|
|
4628
|
+
} catch (error) {
|
|
4629
|
+
if (error instanceof MicroVmFileNotFoundError) return false;
|
|
4630
|
+
throw error;
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
/** Create a directory and its parents (pass `parents: false` to require the parent). */
|
|
4634
|
+
mkdir(path, options) {
|
|
4635
|
+
return this.resource.mkdir(this.microvmId, path, options);
|
|
4636
|
+
}
|
|
4637
|
+
/** Delete a file, or a directory with `recursive: true`. */
|
|
4638
|
+
remove(path, options) {
|
|
4639
|
+
return this.resource.remove(this.microvmId, path, options);
|
|
4640
|
+
}
|
|
4641
|
+
/** Move or rename a file or directory. */
|
|
4642
|
+
move(from, to) {
|
|
4643
|
+
return this.resource.move(this.microvmId, from, to);
|
|
4644
|
+
}
|
|
4645
|
+
};
|
|
4646
|
+
var MicroVmCommands = class {
|
|
4647
|
+
/** @internal */
|
|
4648
|
+
constructor(resource, microvmId) {
|
|
4649
|
+
this.resource = resource;
|
|
4650
|
+
this.microvmId = microvmId;
|
|
4651
|
+
}
|
|
4652
|
+
resource;
|
|
4653
|
+
microvmId;
|
|
4654
|
+
/**
|
|
4655
|
+
* Reattach to a command, for example one spawned by another process.
|
|
4656
|
+
*
|
|
4657
|
+
* @example
|
|
4658
|
+
* ```ts
|
|
4659
|
+
* const command = await vm.commands.get(savedCommandId);
|
|
4660
|
+
* for await (const chunk of command.logs()) process.stdout.write(chunk.data);
|
|
4661
|
+
* ```
|
|
4662
|
+
*/
|
|
4663
|
+
async get(commandId) {
|
|
4664
|
+
return new Command(this.resource, this.microvmId, await this.resource.get(this.microvmId, commandId));
|
|
4665
|
+
}
|
|
4666
|
+
/** All commands the microVM still remembers, newest first. */
|
|
4667
|
+
async list() {
|
|
4668
|
+
const infos = await this.resource.list(this.microvmId);
|
|
4669
|
+
return infos.map((info) => new Command(this.resource, this.microvmId, info));
|
|
4670
|
+
}
|
|
4671
|
+
};
|
|
4672
|
+
var MicroVM = class _MicroVM {
|
|
4673
|
+
/** @internal Use {@link MicroVM.create} or {@link MicroVM.get}. */
|
|
4674
|
+
constructor(resource, info) {
|
|
4675
|
+
this.resource = resource;
|
|
4676
|
+
this.current = info;
|
|
4677
|
+
this.files = new MicroVmFiles(resource.files, info.id);
|
|
4678
|
+
this.commands = new MicroVmCommands(resource.commands, info.id);
|
|
4679
|
+
}
|
|
4680
|
+
resource;
|
|
4681
|
+
/** Files inside the microVM. */
|
|
4682
|
+
files;
|
|
4683
|
+
/** Reattach to commands by id. */
|
|
4684
|
+
commands;
|
|
4685
|
+
current;
|
|
4686
|
+
/**
|
|
4687
|
+
* Create a microVM and wait until it runs.
|
|
4688
|
+
*
|
|
4689
|
+
* @throws {RangeError} When `timeout` is longer than 24 hours.
|
|
4690
|
+
*
|
|
4691
|
+
* The application is taken from `applicationId`, the client's
|
|
4692
|
+
* `applicationId` or `GIGADRIVE_APPLICATION_ID`; inside a Gigadrive
|
|
4693
|
+
* deployment or Function nothing needs configuring.
|
|
4694
|
+
*
|
|
4695
|
+
* @example
|
|
4696
|
+
* ```ts
|
|
4697
|
+
* const vm = await MicroVM.create({
|
|
4698
|
+
* image: 'python3.13',
|
|
4699
|
+
* vcpus: 2,
|
|
4700
|
+
* memoryMb: 4096,
|
|
4701
|
+
* timeout: '15m',
|
|
4702
|
+
* ports: [8000],
|
|
4703
|
+
* env: { PYTHONUNBUFFERED: '1' },
|
|
4704
|
+
* metadata: { user: 'u_123' },
|
|
4705
|
+
* network: 'deny',
|
|
4706
|
+
* });
|
|
4707
|
+
* ```
|
|
4708
|
+
*/
|
|
4709
|
+
static async create(options = {}) {
|
|
4710
|
+
const { client, timeout, timeoutMs, network, wait, signal, ...input } = options;
|
|
4711
|
+
const resource = resolveClient(client).microvms;
|
|
4712
|
+
const info = await resource.create(
|
|
4713
|
+
{
|
|
4714
|
+
...input,
|
|
4715
|
+
...timeout !== void 0 ? { timeoutMs: microvmTimeoutMs(timeout) } : timeoutMs !== void 0 ? { timeoutMs } : {},
|
|
4716
|
+
...network !== void 0 ? { network: typeof network === "string" ? { egress: network } : network } : {}
|
|
4717
|
+
},
|
|
4718
|
+
// Never hold one request open for the whole boot: a create can wait
|
|
4719
|
+
// minutes for capacity, longer than proxies in front of the API keep a
|
|
4720
|
+
// response open. Start it, then poll.
|
|
4721
|
+
{ wait: false, signal }
|
|
4722
|
+
);
|
|
4723
|
+
const vm = new _MicroVM(resource, info);
|
|
4724
|
+
if (wait === false) return vm;
|
|
4725
|
+
try {
|
|
4726
|
+
return await vm.waitUntilRunning({ timeout: CREATE_WAIT_TIMEOUT, signal });
|
|
4727
|
+
} catch (error) {
|
|
4728
|
+
if (error instanceof MicroVmStartError) throw error;
|
|
4729
|
+
try {
|
|
4730
|
+
await vm.delete();
|
|
4731
|
+
} catch (cleanupError) {
|
|
4732
|
+
throw new MicroVmCleanupError(vm.id, error, cleanupError);
|
|
4733
|
+
}
|
|
4734
|
+
throw error;
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
/**
|
|
4738
|
+
* Connect to an existing microVM by id.
|
|
4739
|
+
*
|
|
4740
|
+
* @throws {@link MicroVmNotFoundError} when it does not exist.
|
|
4741
|
+
*/
|
|
4742
|
+
static async get(microvmId, options = {}) {
|
|
4743
|
+
const resource = resolveClient(options.client).microvms;
|
|
4744
|
+
return new _MicroVM(resource, await resource.get(microvmId));
|
|
4745
|
+
}
|
|
4746
|
+
/** Alias of {@link MicroVM.get}. */
|
|
4747
|
+
static connect(microvmId, options = {}) {
|
|
4748
|
+
return _MicroVM.get(microvmId, options);
|
|
4749
|
+
}
|
|
4750
|
+
/**
|
|
4751
|
+
* Iterate microVMs, newest first, across all pages.
|
|
4752
|
+
*
|
|
4753
|
+
* @example
|
|
4754
|
+
* ```ts
|
|
4755
|
+
* for await (const vm of MicroVM.list({ status: 'running' })) {
|
|
4756
|
+
* console.log(vm.id, vm.metadata);
|
|
4757
|
+
* }
|
|
4758
|
+
* ```
|
|
4759
|
+
*/
|
|
4760
|
+
static async *list(options = {}) {
|
|
4761
|
+
const { client, ...query } = options;
|
|
4762
|
+
const resource = resolveClient(client).microvms;
|
|
4763
|
+
for await (const info of resource.listAll(query)) yield new _MicroVM(resource, info);
|
|
4764
|
+
}
|
|
4765
|
+
/** Stable UUID. */
|
|
4766
|
+
get id() {
|
|
4767
|
+
return this.current.id;
|
|
4768
|
+
}
|
|
4769
|
+
/** Label used in preview URLs. */
|
|
4770
|
+
get slug() {
|
|
4771
|
+
return this.current.slug;
|
|
4772
|
+
}
|
|
4773
|
+
get applicationId() {
|
|
4774
|
+
return this.current.applicationId;
|
|
4775
|
+
}
|
|
4776
|
+
/** Status as of the last API response; call {@link refresh} for the current one. */
|
|
4777
|
+
get status() {
|
|
4778
|
+
return this.current.status;
|
|
4779
|
+
}
|
|
4780
|
+
/** Exposed ports. */
|
|
4781
|
+
get ports() {
|
|
4782
|
+
return this.current.ports.map((entry) => entry.port);
|
|
4783
|
+
}
|
|
4784
|
+
get metadata() {
|
|
4785
|
+
return this.current.metadata;
|
|
4786
|
+
}
|
|
4787
|
+
/** When the microVM stops automatically. */
|
|
4788
|
+
get expiresAt() {
|
|
4789
|
+
return this.current.expiresAt ? new Date(this.current.expiresAt) : null;
|
|
4790
|
+
}
|
|
4791
|
+
/** The full API object as of the last response. */
|
|
4792
|
+
get info() {
|
|
4793
|
+
return this.current;
|
|
4794
|
+
}
|
|
4795
|
+
/**
|
|
4796
|
+
* The public preview URL of an exposed port.
|
|
4797
|
+
*
|
|
4798
|
+
* @throws {@link MicroVmPortNotExposedError} when the port was not in `ports` at create.
|
|
4799
|
+
*
|
|
4800
|
+
* @example
|
|
4801
|
+
* ```ts
|
|
4802
|
+
* vm.url(3000); // "https://3000-k3j9x2m1q8zt.vm.gigadrive.app"
|
|
4803
|
+
* ```
|
|
4804
|
+
*/
|
|
4805
|
+
url(port) {
|
|
4806
|
+
const entry = this.current.ports.find((candidate) => candidate.port === port);
|
|
4807
|
+
if (!entry) throw new MicroVmPortNotExposedError(port, this.ports);
|
|
4808
|
+
return entry.url;
|
|
4809
|
+
}
|
|
4810
|
+
/** Fetch the latest state. */
|
|
4811
|
+
async refresh(options = {}) {
|
|
4812
|
+
this.current = await this.resource.get(this.id, { signal: options.signal });
|
|
4813
|
+
return this.current;
|
|
4814
|
+
}
|
|
4815
|
+
async run(cmd, argsOrOptions, maybeOptions) {
|
|
4816
|
+
const args = Array.isArray(argsOrOptions) ? argsOrOptions : void 0;
|
|
4817
|
+
const options = (Array.isArray(argsOrOptions) ? maybeOptions : argsOrOptions) ?? {};
|
|
4818
|
+
const input = toStartInput(cmd, args, options);
|
|
4819
|
+
const streaming = options.onStdout !== void 0 || options.onStderr !== void 0 || options.stdin !== void 0 || options.signal !== void 0;
|
|
4820
|
+
if (!streaming) {
|
|
4821
|
+
const raw = await this.resource.commands.run(this.id, input);
|
|
4822
|
+
return failIfRequested(
|
|
4823
|
+
{
|
|
4824
|
+
commandId: raw.id,
|
|
4825
|
+
exitCode: raw.exitCode,
|
|
4826
|
+
signal: raw.signal ?? null,
|
|
4827
|
+
timedOut: raw.timedOut === true,
|
|
4828
|
+
stdout: raw.stdout,
|
|
4829
|
+
stderr: raw.stderr,
|
|
4830
|
+
truncated: raw.truncated,
|
|
4831
|
+
durationMs: raw.durationMs
|
|
4832
|
+
},
|
|
4833
|
+
options.throwOnError
|
|
4834
|
+
);
|
|
4835
|
+
}
|
|
4836
|
+
const { signal } = options;
|
|
4837
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
4838
|
+
const startedAt = Date.now();
|
|
4839
|
+
const info = await this.resource.commands.start(
|
|
4840
|
+
this.id,
|
|
4841
|
+
{ ...input, ...options.stdin !== void 0 ? { stdin: true } : {} },
|
|
4842
|
+
{ signal }
|
|
4843
|
+
);
|
|
4844
|
+
const command = new Command(this.resource.commands, this.id, info);
|
|
4845
|
+
const kill = () => void command.kill("SIGKILL").catch(() => void 0);
|
|
4846
|
+
signal?.addEventListener("abort", kill, { once: true });
|
|
4847
|
+
try {
|
|
4848
|
+
if (options.stdin !== void 0) await command.end(options.stdin);
|
|
4849
|
+
const output = { stdout: "", stderr: "" };
|
|
4850
|
+
const expected = { stdout: 0, stderr: 0 };
|
|
4851
|
+
let truncated = false;
|
|
4852
|
+
for await (const chunk of command.logs({ signal })) {
|
|
4853
|
+
if (chunk.offset > expected[chunk.stream]) truncated = true;
|
|
4854
|
+
expected[chunk.stream] = chunk.offset + chunk.data.length;
|
|
4855
|
+
output[chunk.stream] += chunk.text;
|
|
4856
|
+
await (chunk.stream === "stdout" ? options.onStdout : options.onStderr)?.(chunk.text, chunk);
|
|
4857
|
+
}
|
|
4858
|
+
const final = command.status === "exited" ? command.info : await command.wait({ signal });
|
|
4859
|
+
return failIfRequested(
|
|
4860
|
+
{
|
|
4861
|
+
commandId: command.id,
|
|
4862
|
+
exitCode: final.exitCode,
|
|
4863
|
+
signal: final.signal,
|
|
4864
|
+
timedOut: final.timedOut === true,
|
|
4865
|
+
...output,
|
|
4866
|
+
truncated: truncated || command.truncated,
|
|
4867
|
+
durationMs: commandDurationMs(final) ?? Date.now() - startedAt
|
|
4868
|
+
},
|
|
4869
|
+
options.throwOnError
|
|
4870
|
+
);
|
|
4871
|
+
} finally {
|
|
4872
|
+
signal?.removeEventListener("abort", kill);
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
async spawn(cmd, argsOrOptions, maybeOptions) {
|
|
4876
|
+
const args = Array.isArray(argsOrOptions) ? argsOrOptions : void 0;
|
|
4877
|
+
const options = (Array.isArray(argsOrOptions) ? maybeOptions : argsOrOptions) ?? {};
|
|
4878
|
+
const pty = options.pty === true ? { cols: 80, rows: 24 } : options.pty || void 0;
|
|
4879
|
+
const info = await this.resource.commands.start(this.id, {
|
|
4880
|
+
...toStartInput(cmd, args, options),
|
|
4881
|
+
...options.stdin ? { stdin: true } : {},
|
|
4882
|
+
...pty ? { pty } : {}
|
|
4883
|
+
});
|
|
4884
|
+
const command = new Command(this.resource.commands, this.id, info);
|
|
4885
|
+
if (options.onStdout || options.onStderr) command.startOutputPump(options.onStdout, options.onStderr);
|
|
4886
|
+
return command;
|
|
4887
|
+
}
|
|
4888
|
+
/**
|
|
4889
|
+
* Set the remaining lifetime to `duration` from now (up to the plan's
|
|
4890
|
+
* maximum lifetime).
|
|
4891
|
+
*
|
|
4892
|
+
* @example
|
|
4893
|
+
* ```ts
|
|
4894
|
+
* await vm.extendTimeout('30m');
|
|
4895
|
+
* ```
|
|
4896
|
+
*/
|
|
4897
|
+
async extendTimeout(duration) {
|
|
4898
|
+
this.current = await this.resource.extendTimeout(this.id, microvmTimeoutMs(duration));
|
|
4899
|
+
return this.current;
|
|
4900
|
+
}
|
|
4901
|
+
/** Stop the microVM. Its disk, memory and running commands are discarded. */
|
|
4902
|
+
async stop() {
|
|
4903
|
+
this.current = await this.resource.stop(this.id);
|
|
4904
|
+
return this.current;
|
|
4905
|
+
}
|
|
4906
|
+
/** Delete the microVM, stopping it first if needed. */
|
|
4907
|
+
async delete() {
|
|
4908
|
+
await this.resource.delete(this.id);
|
|
4909
|
+
}
|
|
4910
|
+
/**
|
|
4911
|
+
* Poll until the microVM is running, for microVMs created with `wait: false`.
|
|
4912
|
+
*
|
|
4913
|
+
* @throws {@link MicroVmStartError} when it failed or stopped instead.
|
|
4914
|
+
* @throws {@link MicroVmTimeoutError} when it is still starting after `timeout`.
|
|
4915
|
+
*/
|
|
4916
|
+
async waitUntilRunning(options = {}) {
|
|
4917
|
+
const deadline = Date.now() + durationToMs(options.timeout ?? "2m");
|
|
4918
|
+
const interval = durationToMs(options.interval ?? "500ms");
|
|
4919
|
+
while (this.current.status !== "running") {
|
|
4920
|
+
if (TERMINAL.includes(this.current.status)) {
|
|
4921
|
+
throw new MicroVmStartError(this.id, this.current.status, this.current.stopReason);
|
|
4922
|
+
}
|
|
4923
|
+
if (Date.now() + interval > deadline) {
|
|
4924
|
+
throw new MicroVmTimeoutError(`The microVM ${this.id} is still ${this.current.status}; gave up waiting.`);
|
|
4925
|
+
}
|
|
4926
|
+
await sleep(interval, options.signal);
|
|
4927
|
+
await this.refresh({ signal: options.signal });
|
|
4928
|
+
if (options.signal?.aborted) throw abortReason(options.signal);
|
|
4929
|
+
}
|
|
4930
|
+
return this;
|
|
4931
|
+
}
|
|
4932
|
+
/**
|
|
4933
|
+
* Poll an exposed port's preview URL until a server answers (any status
|
|
4934
|
+
* other than 502, 503 or 504, which the gateway returns while nothing
|
|
4935
|
+
* listens).
|
|
4936
|
+
*
|
|
4937
|
+
* @returns The preview URL.
|
|
4938
|
+
* @throws {@link MicroVmPortNotExposedError} when the port is not exposed.
|
|
4939
|
+
* @throws {@link MicroVmNotRunningError} when the microVM has stopped.
|
|
4940
|
+
* @throws {@link MicroVmTimeoutError} when nothing answers within `timeout`.
|
|
4941
|
+
*
|
|
4942
|
+
* @example
|
|
4943
|
+
* ```ts
|
|
4944
|
+
* await vm.spawn('python3', ['-m', 'http.server', '8000']);
|
|
4945
|
+
* const url = await vm.waitForPort(8000);
|
|
4946
|
+
* ```
|
|
4947
|
+
*/
|
|
4948
|
+
async waitForPort(port, options = {}) {
|
|
4949
|
+
const base = this.url(port);
|
|
4950
|
+
const path = options.path ?? "/";
|
|
4951
|
+
const target = `${base.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
4952
|
+
const deadline = Date.now() + durationToMs(options.timeout ?? "60s");
|
|
4953
|
+
const interval = durationToMs(options.interval ?? "500ms");
|
|
4954
|
+
for (; ; ) {
|
|
4955
|
+
if (options.signal?.aborted) throw abortReason(options.signal);
|
|
4956
|
+
const controller = new AbortController();
|
|
4957
|
+
const timer = setTimeout(
|
|
4958
|
+
() => controller.abort(),
|
|
4959
|
+
Math.min(PROBE_TIMEOUT_MS, Math.max(1, deadline - Date.now()))
|
|
4960
|
+
);
|
|
4961
|
+
const forward = () => controller.abort();
|
|
4962
|
+
options.signal?.addEventListener("abort", forward, { once: true });
|
|
4963
|
+
let stopped = false;
|
|
4964
|
+
try {
|
|
4965
|
+
const response = await this.resource.fetchPreview(target, {
|
|
4966
|
+
method: "GET",
|
|
4967
|
+
redirect: "manual",
|
|
4968
|
+
signal: controller.signal
|
|
4969
|
+
});
|
|
4970
|
+
await response.body?.cancel().catch(() => void 0);
|
|
4971
|
+
stopped = response.status === 410 && response.headers.get("x-gigadrive-error") === "EDGE_MICROVM_STOPPED";
|
|
4972
|
+
if (!stopped && !NOT_LISTENING.has(response.status)) return base;
|
|
4973
|
+
} catch {
|
|
4974
|
+
if (options.signal?.aborted) throw abortReason(options.signal);
|
|
4975
|
+
} finally {
|
|
4976
|
+
clearTimeout(timer);
|
|
4977
|
+
options.signal?.removeEventListener("abort", forward);
|
|
4978
|
+
}
|
|
4979
|
+
if (stopped) {
|
|
4980
|
+
throw new MicroVmNotRunningError(`The microVM ${this.id} has stopped; nothing will answer on port ${port}.`);
|
|
4981
|
+
}
|
|
4982
|
+
if (Date.now() + interval > deadline) {
|
|
4983
|
+
throw new MicroVmTimeoutError(`Nothing answered on port ${port} of the microVM ${this.id}.`);
|
|
4984
|
+
}
|
|
4985
|
+
await sleep(interval, options.signal);
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
/** Stops the microVM at the end of an `await using` block. Already stopped microVMs are fine. */
|
|
4989
|
+
async [Symbol.asyncDispose]() {
|
|
4990
|
+
try {
|
|
4991
|
+
await this.stop();
|
|
4992
|
+
} catch (error) {
|
|
4993
|
+
if (error instanceof MicroVmNotRunningError || error instanceof MicroVmNotFoundError) return;
|
|
4994
|
+
throw error;
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
/** JSON form: the API object. */
|
|
4998
|
+
toJSON() {
|
|
4999
|
+
return this.current;
|
|
3562
5000
|
}
|
|
3563
5001
|
};
|
|
3564
5002
|
|
|
@@ -3575,10 +5013,10 @@ async function* paginate(fetchPage) {
|
|
|
3575
5013
|
}
|
|
3576
5014
|
|
|
3577
5015
|
// src/queues/default-client.ts
|
|
3578
|
-
var
|
|
5016
|
+
var defaultClient2;
|
|
3579
5017
|
var defaultQueuesResource = () => {
|
|
3580
|
-
|
|
3581
|
-
return
|
|
5018
|
+
defaultClient2 ??= new GigadriveClient();
|
|
5019
|
+
return defaultClient2.queues;
|
|
3582
5020
|
};
|
|
3583
5021
|
function queue(name, options = {}) {
|
|
3584
5022
|
const { client, ...queueOptions } = options;
|
|
@@ -3602,7 +5040,7 @@ var decodeWorkflowMessage = (text) => JSON.parse(text, (_key, value) => {
|
|
|
3602
5040
|
return value;
|
|
3603
5041
|
});
|
|
3604
5042
|
var readEnv2 = (name) => globalThis.process?.env?.[name] || void 0;
|
|
3605
|
-
var deduplicationKeyFor = async (idempotencyKey) => idempotencyKey.length <= 256 ? idempotencyKey : `wkf:sha256:${
|
|
5043
|
+
var deduplicationKeyFor = async (idempotencyKey) => idempotencyKey.length <= 256 ? idempotencyKey : `wkf:sha256:${toHex(await crypto.subtle.digest("SHA-256", toArrayBuffer(utf8Encode(idempotencyKey))))}`;
|
|
3606
5044
|
function createWorkflowQueue(options = {}) {
|
|
3607
5045
|
const resource = () => options.client?.queues ?? defaultQueuesResource();
|
|
3608
5046
|
const scope = { applicationId: options.applicationId, environment: options.environment };
|
|
@@ -3744,12 +5182,34 @@ function createWorkflowQueue(options = {}) {
|
|
|
3744
5182
|
ApplicationStorageResource,
|
|
3745
5183
|
ApplicationsResource,
|
|
3746
5184
|
AuthenticationError,
|
|
5185
|
+
Command,
|
|
5186
|
+
CommandFailedError,
|
|
3747
5187
|
ConfigurationError,
|
|
3748
5188
|
DeploymentsResource,
|
|
3749
5189
|
DomainNotActiveError,
|
|
3750
5190
|
GigadriveClient,
|
|
3751
5191
|
GigadriveError,
|
|
3752
5192
|
ImageOptimizationResource,
|
|
5193
|
+
MICROVM_MAX_TIMEOUT_MS,
|
|
5194
|
+
MicroVM,
|
|
5195
|
+
MicroVmBusyError,
|
|
5196
|
+
MicroVmCleanupError,
|
|
5197
|
+
MicroVmCommands,
|
|
5198
|
+
MicroVmCommandsResource,
|
|
5199
|
+
MicroVmFileNotFoundError,
|
|
5200
|
+
MicroVmFiles,
|
|
5201
|
+
MicroVmFilesResource,
|
|
5202
|
+
MicroVmImageBuildError,
|
|
5203
|
+
MicroVmImageNotReadyError,
|
|
5204
|
+
MicroVmImagesResource,
|
|
5205
|
+
MicroVmNotFoundError,
|
|
5206
|
+
MicroVmNotRunningError,
|
|
5207
|
+
MicroVmPortNotExposedError,
|
|
5208
|
+
MicroVmQuotaExceededError,
|
|
5209
|
+
MicroVmStartError,
|
|
5210
|
+
MicroVmTimeoutError,
|
|
5211
|
+
MicroVmUnreachableError,
|
|
5212
|
+
MicroVmsResource,
|
|
3753
5213
|
NonRetryableError,
|
|
3754
5214
|
OrganizationAiGatewayResource,
|
|
3755
5215
|
OrganizationDomainsResource,
|
|
@@ -3777,8 +5237,10 @@ function createWorkflowQueue(options = {}) {
|
|
|
3777
5237
|
createManagedImageUrl,
|
|
3778
5238
|
createWorkflowQueue,
|
|
3779
5239
|
defaultQueueCodec,
|
|
5240
|
+
durationToMs,
|
|
3780
5241
|
durationToSeconds,
|
|
3781
5242
|
paginate,
|
|
5243
|
+
parseSSEEvents,
|
|
3782
5244
|
parseSSEStream,
|
|
3783
5245
|
queue,
|
|
3784
5246
|
signQueueDelivery,
|