withruntime 0.17.2 → 0.21.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/COMPATIBILITY.md +32 -1
- package/README.md +19 -0
- package/README.src.md +19 -0
- package/dist/agents/index.d.ts +11 -0
- package/dist/agents/index.js +12 -0
- package/dist/agents-cli.d.ts +10 -0
- package/dist/agents-cli.js +362 -0
- package/dist/apps-cli.d.ts +14 -0
- package/dist/apps-cli.js +737 -0
- package/dist/builds/index.d.ts +10 -0
- package/dist/builds/index.js +11 -0
- package/dist/cli-words.js +11 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +104 -6
- package/dist/client.d.ts +19 -0
- package/dist/client.js +20 -0
- package/dist/cloudflare/errors.d.ts +23 -0
- package/dist/cloudflare/errors.js +16 -0
- package/dist/cloudflare/index.d.ts +82 -9
- package/dist/cloudflare/index.js +162 -38
- package/dist/cloudflare/interpreter.js +14 -5
- package/dist/cloudflare/mime.d.ts +1 -0
- package/dist/cloudflare/mime.js +66 -0
- package/dist/cloudflare/streams.d.ts +8 -0
- package/dist/cloudflare/streams.js +30 -0
- package/dist/cloudflare/types.d.ts +338 -0
- package/dist/cloudflare/types.js +80 -0
- package/dist/cloudflare/worker.d.ts +297 -10
- package/dist/cloudflare/worker.js +203 -17
- package/dist/containers/index.d.ts +10 -0
- package/dist/containers/index.js +11 -0
- package/dist/daytona/sandbox.d.ts +2 -1
- package/dist/daytona/sandbox.js +2 -1
- package/dist/identity.js +18 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/product-names.js +3 -0
- package/dist/products/agents.d.ts +272 -0
- package/dist/products/agents.js +251 -0
- package/dist/products/builds.d.ts +11 -1
- package/dist/products/builds.js +7 -0
- package/dist/products/containers.d.ts +71 -0
- package/dist/products/containers.js +6 -0
- package/dist/products/index.d.ts +6 -0
- package/dist/products/index.js +7 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/transport.d.ts +2 -2
- package/dist/transport.js +1 -1
- package/dist/tunnel.js +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/vercel/errors.d.ts +1 -1
- package/package.json +16 -1
package/dist/cloudflare/index.js
CHANGED
|
@@ -3,14 +3,16 @@ import { Runtime } from "../client.js";
|
|
|
3
3
|
import { NotFoundError } from "../errors.js";
|
|
4
4
|
import { timeoutSignal } from "../transport.js";
|
|
5
5
|
import { quote, client, closeShell, CompatibilityError, refuse, destroy, environment, execOptions, only, pathAt, saveEnvironment, shellCommand, shellRoot, } from "../compat/core.js";
|
|
6
|
-
import { BucketUnmountError, InvalidMountConfigError, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, response, } from "./errors.js";
|
|
6
|
+
import { BucketUnmountError, InvalidMountConfigError, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, ValidationFailedError, response, } from "./errors.js";
|
|
7
7
|
import { CodeInterpreter, hasEnv, RUN_ENV, } from "./interpreter.js";
|
|
8
8
|
import { sseStream } from "./streams.js";
|
|
9
|
+
import { mimeType } from "./mime.js";
|
|
9
10
|
import { CHANGES, watchFilters } from "./watch.js";
|
|
10
11
|
export { CompatibilityError };
|
|
11
12
|
export * from "./errors.js";
|
|
12
13
|
export { CodeInterpreter } from "./interpreter.js";
|
|
13
|
-
export { collectFile, isExecResult, isProcess, isProcessStatus, parseSSEStream, streamFile, } from "./streams.js";
|
|
14
|
+
export { collectFile, isExecResult, isProcess, isProcessStatus, parseSSEStream, streamFile, asyncIterableToSSEStream, responseToAsyncIterable, } from "./streams.js";
|
|
15
|
+
export * from "./types.js";
|
|
14
16
|
export class SandboxSecurityError extends Error {
|
|
15
17
|
code;
|
|
16
18
|
constructor(message, code) {
|
|
@@ -262,7 +264,7 @@ export class Sandbox {
|
|
|
262
264
|
return sseStream(async (send) => {
|
|
263
265
|
send({
|
|
264
266
|
type: "metadata",
|
|
265
|
-
mimeType:
|
|
267
|
+
mimeType: read.mimeType,
|
|
266
268
|
size: bytes.length,
|
|
267
269
|
isBinary,
|
|
268
270
|
encoding: isBinary ? "base64" : "utf-8",
|
|
@@ -800,11 +802,18 @@ export class ExecutionSession {
|
|
|
800
802
|
if (options.encoding && !["utf8", "utf-8", "base64"].includes(options.encoding))
|
|
801
803
|
throw new CompatibilityError("Cloudflare", `file encoding ${options.encoding}`);
|
|
802
804
|
let bytes;
|
|
803
|
-
if (typeof content === "string")
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
:
|
|
805
|
+
if (typeof content === "string" && options.encoding === "base64") {
|
|
806
|
+
// Cloudflare's container refuses anything but base64's alphabet.
|
|
807
|
+
if (!/^[A-Za-z0-9+/=]*$/.test(content))
|
|
808
|
+
throw new ValidationFailedError(response("VALIDATION_FAILED", `Invalid base64 content for '${path}': must contain only A-Z, a-z, 0-9, +, /, =`, {
|
|
809
|
+
validationErrors: [
|
|
810
|
+
{ field: "content", message: "Invalid base64 characters", code: "INVALID_BASE64" },
|
|
811
|
+
],
|
|
812
|
+
}, { httpStatus: 400 }));
|
|
813
|
+
bytes = Uint8Array.from(atob(content), (c) => c.charCodeAt(0));
|
|
814
|
+
}
|
|
815
|
+
else if (typeof content === "string")
|
|
816
|
+
bytes = new TextEncoder().encode(content);
|
|
808
817
|
else {
|
|
809
818
|
const buffer = await new Response(content).arrayBuffer();
|
|
810
819
|
bytes = new Uint8Array(buffer);
|
|
@@ -813,7 +822,7 @@ export class ExecutionSession {
|
|
|
813
822
|
return { success: true, path, bytesWritten: bytes.length, timestamp: new Date().toISOString() };
|
|
814
823
|
}
|
|
815
824
|
async readFile(path, options = {}) {
|
|
816
|
-
const s = await this.sandbox.native(), bytes = await s.files.read(
|
|
825
|
+
const s = await this.sandbox.native(), resolved = await this.path(path), bytes = await s.files.read(resolved), type = mimeType(resolved, bytes);
|
|
817
826
|
if (options.encoding === "none")
|
|
818
827
|
return {
|
|
819
828
|
success: true,
|
|
@@ -825,7 +834,7 @@ export class ExecutionSession {
|
|
|
825
834
|
},
|
|
826
835
|
}),
|
|
827
836
|
size: bytes.length,
|
|
828
|
-
mimeType:
|
|
837
|
+
mimeType: type,
|
|
829
838
|
timestamp: new Date().toISOString(),
|
|
830
839
|
};
|
|
831
840
|
const text = new TextDecoder().decode(bytes), binary = text.includes("\0") || text.includes("\ufffd"), encoded = options.encoding === "base64" || (options.encoding === undefined && binary);
|
|
@@ -839,6 +848,7 @@ export class ExecutionSession {
|
|
|
839
848
|
content: encoded ? btoa(raw) : text,
|
|
840
849
|
encoding: encoded ? "base64" : "utf-8",
|
|
841
850
|
isBinary: binary,
|
|
851
|
+
mimeType: type,
|
|
842
852
|
size: bytes.length,
|
|
843
853
|
timestamp: new Date().toISOString(),
|
|
844
854
|
};
|
|
@@ -888,7 +898,68 @@ export class ExecutionSession {
|
|
|
888
898
|
this.options = { ...this.options, env: { ...this.options.env, ...cleanEnv(env) } };
|
|
889
899
|
await (await this.sandbox.native()).files.write(`/workspace/.runtime-compat/cloudflare/${encodeURIComponent(this.id)}.json`, JSON.stringify(this.options), { mode: 0o600 });
|
|
890
900
|
}
|
|
901
|
+
// The rest of Cloudflare's ExecutionSession: the sandbox's calls, in this session.
|
|
902
|
+
moveFile(path, newPath) {
|
|
903
|
+
return this.renameFile(path, newPath);
|
|
904
|
+
}
|
|
905
|
+
readFileStream(path) {
|
|
906
|
+
return this.sandbox.readFileStream(path, { sessionId: this.id });
|
|
907
|
+
}
|
|
908
|
+
gitCheckout(repoUrl, options = {}) {
|
|
909
|
+
return this.sandbox.gitCheckout(repoUrl, { ...options, sessionId: this.id });
|
|
910
|
+
}
|
|
911
|
+
watch(path, options = {}) {
|
|
912
|
+
return this.sandbox.watch(path, { ...options, sessionId: this.id });
|
|
913
|
+
}
|
|
914
|
+
checkChanges(path, options = {}) {
|
|
915
|
+
return this.sandbox.checkChanges(path, { ...options, sessionId: this.id });
|
|
916
|
+
}
|
|
917
|
+
listProcesses() {
|
|
918
|
+
return this.sandbox.listProcesses();
|
|
919
|
+
}
|
|
920
|
+
getProcess(id) {
|
|
921
|
+
return this.sandbox.getProcess(id);
|
|
922
|
+
}
|
|
923
|
+
killProcess(id, signal) {
|
|
924
|
+
return this.sandbox.killProcess(id, signal);
|
|
925
|
+
}
|
|
926
|
+
killAllProcesses() {
|
|
927
|
+
return this.sandbox.killAllProcesses();
|
|
928
|
+
}
|
|
929
|
+
cleanupCompletedProcesses() {
|
|
930
|
+
return this.sandbox.cleanupCompletedProcesses();
|
|
931
|
+
}
|
|
932
|
+
getProcessLogs(id) {
|
|
933
|
+
return this.sandbox.getProcessLogs(id);
|
|
934
|
+
}
|
|
935
|
+
streamProcessLogs(id, options) {
|
|
936
|
+
return this.sandbox.streamProcessLogs(id, options);
|
|
937
|
+
}
|
|
938
|
+
createCodeContext(options) {
|
|
939
|
+
return this.sandbox.createCodeContext(options);
|
|
940
|
+
}
|
|
941
|
+
runCode(code, options) {
|
|
942
|
+
return this.sandbox.runCode(code, options);
|
|
943
|
+
}
|
|
944
|
+
runCodeStream(code, options) {
|
|
945
|
+
return this.sandbox.runCodeStream(code, options);
|
|
946
|
+
}
|
|
947
|
+
listCodeContexts() {
|
|
948
|
+
return this.sandbox.listCodeContexts();
|
|
949
|
+
}
|
|
950
|
+
deleteCodeContext(contextId) {
|
|
951
|
+
return this.sandbox.deleteCodeContext(contextId);
|
|
952
|
+
}
|
|
953
|
+
mountBucket(bucket, mountPath, options) {
|
|
954
|
+
return this.sandbox.mountBucket(bucket, mountPath, options);
|
|
955
|
+
}
|
|
956
|
+
unmountBucket(mountPath) {
|
|
957
|
+
return this.sandbox.unmountBucket(mountPath);
|
|
958
|
+
}
|
|
891
959
|
}
|
|
960
|
+
refuse("Cloudflare", "ExecutionSession", ExecutionSession, {
|
|
961
|
+
terminal: "open a terminal with the native Runtime sandbox's terminal()",
|
|
962
|
+
});
|
|
892
963
|
export class Process {
|
|
893
964
|
sandbox;
|
|
894
965
|
native;
|
|
@@ -940,43 +1011,96 @@ export class Process {
|
|
|
940
1011
|
});
|
|
941
1012
|
return { exitCode: r.exitCode ?? -1 };
|
|
942
1013
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1014
|
+
/** Cloudflare's errors for a wait, with its messages: a timeout, or the
|
|
1015
|
+
* process ending first. */
|
|
1016
|
+
#timedOut(condition, timeout) {
|
|
1017
|
+
return new ProcessReadyTimeoutError(response("PROCESS_READY_TIMEOUT", `Process did not become ready within ${timeout}ms. Waiting for: ${condition}`, { processId: this.id, command: this.command, condition, timeout }, {
|
|
1018
|
+
httpStatus: 408,
|
|
1019
|
+
suggestion: `Check if your process outputs ${condition}. You can increase the timeout parameter.`,
|
|
1020
|
+
}));
|
|
1021
|
+
}
|
|
1022
|
+
#exited(condition, exitCode) {
|
|
1023
|
+
return new ProcessExitedBeforeReadyError(response("PROCESS_EXITED_BEFORE_READY", `Process exited with code ${exitCode} before becoming ready. Waiting for: ${condition}`, { processId: this.id, command: this.command, condition, exitCode }, { httpStatus: 500, suggestion: "Check process logs with getLogs() for error messages" }));
|
|
1024
|
+
}
|
|
1025
|
+
/** Waits as Cloudflare does: with no limit unless `timeout` is given. */
|
|
1026
|
+
async #within(condition, timeout, wait) {
|
|
1027
|
+
if (timeout === undefined)
|
|
1028
|
+
return wait();
|
|
946
1029
|
const signal = timeoutSignal(timeout);
|
|
947
1030
|
try {
|
|
948
|
-
return await
|
|
1031
|
+
return await wait(signal);
|
|
949
1032
|
}
|
|
950
1033
|
catch (error) {
|
|
951
1034
|
if (!signal.aborted)
|
|
952
1035
|
throw error;
|
|
953
|
-
throw
|
|
1036
|
+
throw this.#timedOut(condition, timeout);
|
|
954
1037
|
}
|
|
955
1038
|
}
|
|
956
|
-
async
|
|
957
|
-
|
|
1039
|
+
async waitForLog(pattern, timeout) {
|
|
1040
|
+
const condition = typeof pattern === "string" ? `"${pattern}"` : pattern.toString();
|
|
1041
|
+
return this.#within(condition, timeout, (signal) => this.#waitForLog(pattern, condition, signal));
|
|
1042
|
+
}
|
|
1043
|
+
async #waitForLog(pattern, condition, signal) {
|
|
1044
|
+
let stdout = "", stderr = "";
|
|
958
1045
|
for await (const e of this.native.output({ signal })) {
|
|
959
|
-
if (e.type === "stdout"
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
: new RegExp(pattern.source, pattern.flags.replace("g", "")).test(line)) ?? output,
|
|
969
|
-
...(typeof pattern === "string" ? {} : { match }),
|
|
970
|
-
};
|
|
971
|
-
}
|
|
972
|
-
else if (e.type === "exit")
|
|
973
|
-
throw new ProcessExitedBeforeReadyError(response("PROCESS_EXITED_BEFORE_READY", "Process exited before expected output", {
|
|
974
|
-
processId: this.id,
|
|
975
|
-
command: this.command,
|
|
976
|
-
condition: `log pattern ${String(pattern)}`,
|
|
977
|
-
exitCode: e.exitCode ?? -1,
|
|
978
|
-
}, { httpStatus: 409, operation: "process.start" }));
|
|
1046
|
+
if (e.type === "stdout")
|
|
1047
|
+
stdout += e.data;
|
|
1048
|
+
else if (e.type === "stderr")
|
|
1049
|
+
stderr += e.data;
|
|
1050
|
+
const found = matchLine(stdout, pattern) ?? matchLine(stderr, pattern);
|
|
1051
|
+
if (found)
|
|
1052
|
+
return found;
|
|
1053
|
+
if (e.type === "exit")
|
|
1054
|
+
throw this.#exited(condition, e.exitCode ?? 1);
|
|
979
1055
|
}
|
|
980
|
-
throw
|
|
1056
|
+
throw this.#exited(condition, 0);
|
|
1057
|
+
}
|
|
1058
|
+
/** Resolves once the port answers: an HTTP status in range (200-399 by
|
|
1059
|
+
* default) at `path`, or a TCP connection in `tcp` mode. Throws if the
|
|
1060
|
+
* process ends first, or after `timeout` milliseconds when one is given. */
|
|
1061
|
+
async waitForPort(port, options = {}) {
|
|
1062
|
+
only("Cloudflare waitForPort", options, ["mode", "path", "status", "timeout", "interval"]);
|
|
1063
|
+
const { mode = "http", path = "/", status = { min: 200, max: 399 }, interval = 500 } = options;
|
|
1064
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
1065
|
+
throw new TypeError(`Invalid port number: ${port}`);
|
|
1066
|
+
if (mode !== "http" && mode !== "tcp")
|
|
1067
|
+
throw new TypeError("mode is http or tcp");
|
|
1068
|
+
const condition = mode === "http" ? `port ${port} (HTTP ${path})` : `port ${port} (TCP)`;
|
|
1069
|
+
const min = typeof status === "number" ? status : status.min;
|
|
1070
|
+
const max = typeof status === "number" ? status : status.max;
|
|
1071
|
+
const url = `http://127.0.0.1:${port}${path.startsWith("/") ? path : `/${path}`}`;
|
|
1072
|
+
const probe = mode === "http"
|
|
1073
|
+
? `code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 ${quote(url)}) && [ "$code" -ge ${min} ] && [ "$code" -le ${max} ]`
|
|
1074
|
+
: `exec 3<>/dev/tcp/127.0.0.1/${port}`;
|
|
1075
|
+
const s = await this.sandbox.native();
|
|
1076
|
+
await this.#within(condition, options.timeout, async (signal) => {
|
|
1077
|
+
for (;;) {
|
|
1078
|
+
await this.native.refresh();
|
|
1079
|
+
if (this.native.info.state !== "running")
|
|
1080
|
+
throw this.#exited(condition, this.native.info.exitCode ?? 1);
|
|
1081
|
+
if ((await s.exec(["bash", "-c", probe], { signal })).exitCode === 0)
|
|
1082
|
+
return;
|
|
1083
|
+
await new Promise((done) => setTimeout(done, interval));
|
|
1084
|
+
signal?.throwIfAborted();
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
981
1087
|
}
|
|
982
1088
|
}
|
|
1089
|
+
/** The first line holding a match, as Cloudflare's waitForLog answers. */
|
|
1090
|
+
function matchLine(text, pattern) {
|
|
1091
|
+
if (typeof pattern === "string") {
|
|
1092
|
+
if (!text.includes(pattern))
|
|
1093
|
+
return null;
|
|
1094
|
+
return { line: text.split("\n").find((line) => line.includes(pattern)) ?? pattern };
|
|
1095
|
+
}
|
|
1096
|
+
const safe = new RegExp(pattern.source, pattern.flags.replace("g", ""));
|
|
1097
|
+
const match = text.match(safe);
|
|
1098
|
+
if (!match)
|
|
1099
|
+
return null;
|
|
1100
|
+
for (const line of text.split("\n")) {
|
|
1101
|
+
const lineMatch = line.match(safe);
|
|
1102
|
+
if (lineMatch)
|
|
1103
|
+
return { line, match: lineMatch };
|
|
1104
|
+
}
|
|
1105
|
+
return { line: match[0], match };
|
|
1106
|
+
}
|
|
@@ -106,11 +106,20 @@ export class CodeInterpreter {
|
|
|
106
106
|
only("Cloudflare createCodeContext", options, ["language", "cwd", "envVars", "timeout"]);
|
|
107
107
|
const language = this.#language(options.language);
|
|
108
108
|
const s = await this.#native();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
try {
|
|
110
|
+
return context(await s.interpreter.contexts.create({
|
|
111
|
+
language,
|
|
112
|
+
cwd: options.cwd ?? "/workspace",
|
|
113
|
+
env: Object.fromEntries(Object.entries(options.envVars ?? {}).filter((e) => e[1] !== undefined)),
|
|
114
|
+
}, options.timeout !== undefined ? { timeoutMs: options.timeout } : {}));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// Cloudflare's container has no fixed count; Runtime's does. Say so,
|
|
118
|
+
// and what frees one, in the message: only it survives Worker RPC.
|
|
119
|
+
if (error.code !== "context_limit")
|
|
120
|
+
throw error;
|
|
121
|
+
throw new Error(`${error.message} Cloudflare's container has no fixed limit; delete a context you no longer need with deleteCodeContext(), and list them with listCodeContexts(). runCode without a context uses one per language.`, { cause: error });
|
|
122
|
+
}
|
|
114
123
|
}
|
|
115
124
|
async runCode(code, options = {}) {
|
|
116
125
|
if (hasEnv(options.envVars))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function mimeType(path: string, bytes: Uint8Array): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** A file's MIME type as Cloudflare's container reports it: from the
|
|
2
|
+
* extension first, then from the content (`file --mime-type` there; the
|
|
3
|
+
* common signatures and text here), else application/octet-stream. */
|
|
4
|
+
const BY_EXTENSION = {
|
|
5
|
+
png: "image/png",
|
|
6
|
+
jpg: "image/jpeg",
|
|
7
|
+
jpeg: "image/jpeg",
|
|
8
|
+
gif: "image/gif",
|
|
9
|
+
webp: "image/webp",
|
|
10
|
+
svg: "image/svg+xml",
|
|
11
|
+
ico: "image/x-icon",
|
|
12
|
+
bmp: "image/bmp",
|
|
13
|
+
avif: "image/avif",
|
|
14
|
+
pdf: "application/pdf",
|
|
15
|
+
zip: "application/zip",
|
|
16
|
+
gz: "application/gzip",
|
|
17
|
+
tgz: "application/gzip",
|
|
18
|
+
tar: "application/x-tar",
|
|
19
|
+
wasm: "application/wasm",
|
|
20
|
+
json: "application/json",
|
|
21
|
+
xml: "application/xml",
|
|
22
|
+
js: "text/javascript",
|
|
23
|
+
mjs: "text/javascript",
|
|
24
|
+
cjs: "text/javascript",
|
|
25
|
+
css: "text/css",
|
|
26
|
+
html: "text/html",
|
|
27
|
+
htm: "text/html",
|
|
28
|
+
md: "text/markdown",
|
|
29
|
+
txt: "text/plain",
|
|
30
|
+
csv: "text/csv",
|
|
31
|
+
yaml: "text/yaml",
|
|
32
|
+
yml: "text/yaml",
|
|
33
|
+
py: "text/x-python",
|
|
34
|
+
sh: "application/x-sh",
|
|
35
|
+
mp3: "audio/mpeg",
|
|
36
|
+
wav: "audio/wav",
|
|
37
|
+
mp4: "video/mp4",
|
|
38
|
+
webm: "video/webm",
|
|
39
|
+
woff: "font/woff",
|
|
40
|
+
woff2: "font/woff2",
|
|
41
|
+
ttf: "font/ttf",
|
|
42
|
+
};
|
|
43
|
+
const SIGNATURES = [
|
|
44
|
+
[[0x89, 0x50, 0x4e, 0x47], "image/png"],
|
|
45
|
+
[[0xff, 0xd8, 0xff], "image/jpeg"],
|
|
46
|
+
[[0x47, 0x49, 0x46, 0x38], "image/gif"],
|
|
47
|
+
[[0x25, 0x50, 0x44, 0x46], "application/pdf"],
|
|
48
|
+
[[0x50, 0x4b, 0x03, 0x04], "application/zip"],
|
|
49
|
+
[[0x1f, 0x8b], "application/gzip"],
|
|
50
|
+
[[0x00, 0x61, 0x73, 0x6d], "application/wasm"],
|
|
51
|
+
[[0x7f, 0x45, 0x4c, 0x46], "application/x-executable"],
|
|
52
|
+
];
|
|
53
|
+
export function mimeType(path, bytes) {
|
|
54
|
+
const name = path.split("/").pop() ?? "";
|
|
55
|
+
const dot = name.lastIndexOf(".");
|
|
56
|
+
const known = dot > 0 ? BY_EXTENSION[name.slice(dot + 1).toLowerCase()] : undefined;
|
|
57
|
+
if (known)
|
|
58
|
+
return known;
|
|
59
|
+
for (const [signature, type] of SIGNATURES)
|
|
60
|
+
if (signature.every((byte, i) => bytes[i] === byte))
|
|
61
|
+
return type;
|
|
62
|
+
if (bytes.length === 0)
|
|
63
|
+
return "inode/x-empty";
|
|
64
|
+
const text = new TextDecoder().decode(bytes.subarray(0, 8192));
|
|
65
|
+
return text.includes("\0") || text.includes("�") ? "application/octet-stream" : "text/plain";
|
|
66
|
+
}
|
|
@@ -25,6 +25,14 @@ export declare function sseStream<T>(events: (send: (event: T) => void, signal:
|
|
|
25
25
|
/** The events of a server-sent-events stream, each `data:` parsed as JSON;
|
|
26
26
|
* `[DONE]`, blank and unparsable data are skipped, as Cloudflare's do. */
|
|
27
27
|
export declare function parseSSEStream<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncIterable<T>;
|
|
28
|
+
/** The events of a server-sent-events Response, as parseSSEStream reads them. */
|
|
29
|
+
export declare function responseToAsyncIterable<T>(response: Response, signal?: AbortSignal): AsyncIterable<T>;
|
|
30
|
+
/** Events as a server-sent-events stream, one `data:` line each, then
|
|
31
|
+
* `data: [DONE]`, as Cloudflare's helper writes them. */
|
|
32
|
+
export declare function asyncIterableToSSEStream<T>(events: AsyncIterable<T>, options?: {
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
serialize?: (event: T) => string;
|
|
35
|
+
}): ReadableStream<Uint8Array>;
|
|
28
36
|
/** A `readFileStream()` stream's chunks: strings for text, bytes for binary.
|
|
29
37
|
* Returns the file's metadata. */
|
|
30
38
|
export declare function streamFile(stream: ReadableStream<Uint8Array>): AsyncGenerator<FileChunk, FileMetadata>;
|
|
@@ -77,6 +77,36 @@ export async function* parseSSEStream(stream, signal) {
|
|
|
77
77
|
reader.releaseLock();
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
/** The events of a server-sent-events Response, as parseSSEStream reads them. */
|
|
81
|
+
export async function* responseToAsyncIterable(response, signal) {
|
|
82
|
+
if (!response.ok)
|
|
83
|
+
throw new Error(`Response not ok: ${response.status} ${response.statusText}`);
|
|
84
|
+
if (!response.body)
|
|
85
|
+
throw new Error("No response body");
|
|
86
|
+
yield* parseSSEStream(response.body, signal);
|
|
87
|
+
}
|
|
88
|
+
/** Events as a server-sent-events stream, one `data:` line each, then
|
|
89
|
+
* `data: [DONE]`, as Cloudflare's helper writes them. */
|
|
90
|
+
export function asyncIterableToSSEStream(events, options = {}) {
|
|
91
|
+
const encoder = new TextEncoder();
|
|
92
|
+
const serialize = options.serialize ?? ((event) => JSON.stringify(event));
|
|
93
|
+
return new ReadableStream({
|
|
94
|
+
async start(controller) {
|
|
95
|
+
try {
|
|
96
|
+
for await (const event of events) {
|
|
97
|
+
if (options.signal?.aborted)
|
|
98
|
+
throw new Error("Operation was aborted");
|
|
99
|
+
controller.enqueue(encoder.encode(`data: ${serialize(event)}\n\n`));
|
|
100
|
+
}
|
|
101
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
102
|
+
controller.close();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
controller.error(error);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
80
110
|
/** A `readFileStream()` stream's chunks: strings for text, bytes for binary.
|
|
81
111
|
* Returns the file's metadata. */
|
|
82
112
|
export async function* streamFile(stream) {
|