withruntime 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ import { SandboxInstance } from "./sandbox.js";
2
+ import type { Port, Sandbox as SandboxModel, SandboxCreateConfiguration, SandboxLifecycle } from "./types.js";
3
+ declare class OutputMessage {
4
+ text: string;
5
+ timestamp: number | null;
6
+ isStderr: boolean;
7
+ constructor(text: string, timestamp: number | null, isStderr: boolean);
8
+ }
9
+ declare class Result {
10
+ [key: string]: unknown;
11
+ constructor(kwargs?: Record<string, unknown>);
12
+ }
13
+ declare class ExecutionError {
14
+ name: string;
15
+ value: unknown;
16
+ traceback: unknown;
17
+ constructor(name: string, value: unknown, traceback: unknown);
18
+ }
19
+ declare class Logs {
20
+ stdout: string[];
21
+ stderr: string[];
22
+ }
23
+ declare class Execution {
24
+ results: Result[];
25
+ logs: Logs;
26
+ error: ExecutionError | null;
27
+ executionCount: number | null;
28
+ }
29
+ declare class Context {
30
+ id: string;
31
+ constructor(id: string);
32
+ static fromJson(data: Record<string, unknown>): Context;
33
+ }
34
+ /** A sandbox that runs code, as Blaxel's CodeInterpreter. */
35
+ export declare class CodeInterpreter extends SandboxInstance {
36
+ #private;
37
+ static readonly DEFAULT_IMAGE = "blaxel/jupyter-server";
38
+ static readonly DEFAULT_PORTS: Port[];
39
+ static readonly DEFAULT_LIFECYCLE: SandboxLifecycle;
40
+ static readonly OutputMessage: typeof OutputMessage;
41
+ static readonly Result: typeof Result;
42
+ static readonly ExecutionError: typeof ExecutionError;
43
+ static readonly Logs: typeof Logs;
44
+ static readonly Execution: typeof Execution;
45
+ static readonly Context: typeof Context;
46
+ /** Blaxel's defaults for a code interpreter: its image, port 8888, and
47
+ * deletion after 30 idle minutes (a paused sandbox kept one day). */
48
+ static create<T extends typeof SandboxInstance>(this: T, sandbox?: SandboxCreateConfiguration | SandboxModel | Record<string, unknown> | null, options?: {
49
+ safe?: boolean;
50
+ createIfNotExist?: boolean;
51
+ }): Promise<InstanceType<T>>;
52
+ get _jupyterUrl(): string;
53
+ /** Runs code in a stateful context. Errors in the code come back in
54
+ * `execution.error`; a run past `timeout` seconds (default 60, 0 for none)
55
+ * throws "Request timeout", as in Blaxel. */
56
+ runCode(code: string, options?: {
57
+ language?: string | null;
58
+ context?: Context | null;
59
+ onStdout?: (msg: OutputMessage) => void;
60
+ onStderr?: (msg: OutputMessage) => void;
61
+ onResult?: (result: Result) => void;
62
+ onError?: (error: ExecutionError) => void;
63
+ envs?: Record<string, string> | null;
64
+ timeout?: number | null;
65
+ requestTimeout?: number | null;
66
+ }): Promise<Execution>;
67
+ /** A new context: its own variables, directory and the sandbox's envs. */
68
+ createCodeContext(options?: {
69
+ cwd?: string | null;
70
+ language?: string | null;
71
+ requestTimeout?: number | null;
72
+ }): Promise<Context>;
73
+ }
74
+ export {};
@@ -0,0 +1,256 @@
1
+ import { ENV_FILE, parseEnvFile } from "./context.js";
2
+ import { guard, isNotFound, NotSupportedError, ResponseError } from "./errors.js";
3
+ import { SandboxInstance } from "./sandbox.js";
4
+ const LANGUAGES = new Set([
5
+ "python",
6
+ "javascript",
7
+ "typescript",
8
+ "r",
9
+ "java",
10
+ "bash",
11
+ "go",
12
+ ]);
13
+ const TEXT_MIME = /^text\/|json|javascript|svg|latex/;
14
+ /** A MIME bundle's keys, as Blaxel's Jupyter server names them. */
15
+ const FIELDS = {
16
+ "text/plain": "text",
17
+ "text/html": "html",
18
+ "text/markdown": "markdown",
19
+ "image/svg+xml": "svg",
20
+ "image/png": "png",
21
+ "image/jpeg": "jpeg",
22
+ "application/pdf": "pdf",
23
+ "text/latex": "latex",
24
+ "application/json": "json",
25
+ "application/javascript": "javascript",
26
+ };
27
+ function languageOf(requested) {
28
+ const language = requested ?? "python";
29
+ if (language === "js")
30
+ return "javascript";
31
+ if (language === "ts")
32
+ return "typescript";
33
+ if (!LANGUAGES.has(language))
34
+ throw new NotSupportedError(`Running ${language} code in the interpreter`, "Runtime's interpreter runs Python, JavaScript, TypeScript, R, Java, Bash and Go; run anything else with sandbox.process.exec.");
35
+ return language;
36
+ }
37
+ class OutputMessage {
38
+ text;
39
+ timestamp;
40
+ isStderr;
41
+ constructor(text, timestamp, isStderr) {
42
+ this.text = text;
43
+ this.timestamp = timestamp;
44
+ this.isStderr = isStderr;
45
+ }
46
+ }
47
+ class Result {
48
+ constructor(kwargs = {}) {
49
+ for (const [key, value] of Object.entries(kwargs))
50
+ this[key] = value;
51
+ }
52
+ }
53
+ class ExecutionError {
54
+ name;
55
+ value;
56
+ traceback;
57
+ constructor(name, value, traceback) {
58
+ this.name = name;
59
+ this.value = value;
60
+ this.traceback = traceback;
61
+ }
62
+ }
63
+ class Logs {
64
+ stdout = [];
65
+ stderr = [];
66
+ }
67
+ class Execution {
68
+ results = [];
69
+ logs = new Logs();
70
+ error = null;
71
+ executionCount = null;
72
+ }
73
+ class Context {
74
+ id;
75
+ constructor(id) {
76
+ this.id = id;
77
+ }
78
+ static fromJson(data) {
79
+ const id = data.id ?? data.context_id;
80
+ return new Context(typeof id === "string" || typeof id === "number" ? String(id) : "");
81
+ }
82
+ }
83
+ /** Output as the Jupyter server reports it: one entry per line. */
84
+ function lines(text) {
85
+ return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
86
+ }
87
+ /** A sandbox that runs code, as Blaxel's CodeInterpreter. */
88
+ export class CodeInterpreter extends SandboxInstance {
89
+ static DEFAULT_IMAGE = "blaxel/jupyter-server";
90
+ static DEFAULT_PORTS = [{ name: "jupyter", target: 8888, protocol: "HTTP" }];
91
+ static DEFAULT_LIFECYCLE = {
92
+ expirationPolicies: [{ type: "ttl-idle", value: "30m", action: "delete" }],
93
+ };
94
+ static OutputMessage = OutputMessage;
95
+ static Result = Result;
96
+ static ExecutionError = ExecutionError;
97
+ static Logs = Logs;
98
+ static Execution = Execution;
99
+ static Context = Context;
100
+ /** Contexts made to carry the sandbox's envs, one per language. */
101
+ #envContexts = new Map();
102
+ #envs;
103
+ /** Blaxel's defaults for a code interpreter: its image, port 8888, and
104
+ * deletion after 30 idle minutes (a paused sandbox kept one day). */
105
+ static async create(sandbox, options = {}) {
106
+ const defaults = {
107
+ image: CodeInterpreter.DEFAULT_IMAGE,
108
+ ports: CodeInterpreter.DEFAULT_PORTS,
109
+ lifecycle: CodeInterpreter.DEFAULT_LIFECYCLE,
110
+ };
111
+ let merged = defaults;
112
+ if (sandbox && typeof sandbox === "object") {
113
+ if ("metadata" in sandbox || "spec" in sandbox) {
114
+ const model = sandbox;
115
+ merged = {
116
+ ...model,
117
+ spec: {
118
+ ...model.spec,
119
+ runtime: { image: defaults.image, ports: defaults.ports, ...model.spec?.runtime },
120
+ lifecycle: model.spec?.lifecycle ?? defaults.lifecycle,
121
+ },
122
+ };
123
+ }
124
+ else
125
+ merged = { ...defaults, ...sandbox };
126
+ }
127
+ return super.create.call(this, merged, options);
128
+ }
129
+ get _jupyterUrl() {
130
+ throw new NotSupportedError("The Jupyter server's address (_jupyterUrl)", "No Jupyter server runs: use runCode, or sandbox.withruntime.interpreter.");
131
+ }
132
+ get #interpreter() {
133
+ return this.withruntime.interpreter;
134
+ }
135
+ /** The sandbox's envs: those this object set, else the env file's. */
136
+ #sandboxEnvs() {
137
+ const known = this.spec.runtime?.envs;
138
+ if (known)
139
+ return Promise.resolve(Object.fromEntries(known.map((e) => [e.name, e.value ?? ""])));
140
+ this.#envs ??= this.fs.read(ENV_FILE).then(parseEnvFile, (error) => {
141
+ if (isNotFound(error))
142
+ return {};
143
+ throw error;
144
+ });
145
+ return this.#envs;
146
+ }
147
+ /** The context a run goes to: the one asked for; else, when the sandbox has
148
+ * envs, a context made once with them; else Runtime's default one. */
149
+ async #contextFor(language) {
150
+ let id = this.#envContexts.get(language);
151
+ if (!id) {
152
+ id = (async () => {
153
+ const envs = await this.#sandboxEnvs();
154
+ if (!Object.keys(envs).length)
155
+ return undefined;
156
+ const wanted = `blaxel-${language}`;
157
+ try {
158
+ return (await guard(() => this.#interpreter.contexts.create({ id: wanted, language, env: envs }))).id;
159
+ }
160
+ catch (error) {
161
+ // Made already, by another object for this sandbox.
162
+ if (!(error instanceof ResponseError && error.status === 409))
163
+ throw error;
164
+ return wanted;
165
+ }
166
+ })();
167
+ this.#envContexts.set(language, id);
168
+ id.catch(() => this.#envContexts.delete(language));
169
+ }
170
+ return id;
171
+ }
172
+ async #inline(result) {
173
+ const data = { ...result.data };
174
+ for (const [mime, ref] of Object.entries(result.refs ?? {})) {
175
+ if (data[mime] !== undefined)
176
+ continue;
177
+ const bytes = await guard(() => this.#interpreter.result(ref));
178
+ data[mime] = TEXT_MIME.test(mime)
179
+ ? new TextDecoder().decode(bytes)
180
+ : Buffer.from(bytes).toString("base64");
181
+ }
182
+ const fields = {};
183
+ const extra = {};
184
+ for (const [mime, value] of Object.entries(data)) {
185
+ const field = FIELDS[mime];
186
+ if (field)
187
+ fields[field] = value;
188
+ else if (mime === "application/vnd.runtime.table+json")
189
+ fields.data = value;
190
+ else
191
+ extra[mime] = value;
192
+ }
193
+ return new Result({
194
+ ...fields,
195
+ ...(Object.keys(extra).length ? { extra } : {}),
196
+ is_main_result: result.main,
197
+ });
198
+ }
199
+ /** Runs code in a stateful context. Errors in the code come back in
200
+ * `execution.error`; a run past `timeout` seconds (default 60, 0 for none)
201
+ * throws "Request timeout", as in Blaxel. */
202
+ async runCode(code, options = {}) {
203
+ if (options.language && options.context)
204
+ throw new Error("You can provide context or language, but not both at the same time.");
205
+ if (options.envs && Object.keys(options.envs).length)
206
+ throw new NotSupportedError("Environment variables for one run (runCode envs)", "Set them in the sandbox's envs at create (every context gets them), or in the code (os.environ).");
207
+ const language = languageOf(options.language);
208
+ const context = options.context?.id ?? (await this.#contextFor(language));
209
+ const timeout = options.timeout === 0 ? null : (options.timeout ?? 60);
210
+ const now = () => Date.now() / 1000;
211
+ const streaming = options.onStdout || options.onStderr || options.onError;
212
+ const execution = await guard(() => this.#interpreter.run(code, {
213
+ ...(context ? { context } : { language }),
214
+ ...(timeout === null
215
+ ? { timeoutMs: 86_400_000 }
216
+ : { timeoutMs: Math.ceil(timeout * 1000) }),
217
+ ...(streaming
218
+ ? {
219
+ onStdout: (text) => options.onStdout?.(new OutputMessage(text, now(), false)),
220
+ onStderr: (text) => options.onStderr?.(new OutputMessage(text, now(), true)),
221
+ onError: (error) => options.onError?.(new ExecutionError(error.name, error.value, error.traceback)),
222
+ }
223
+ : {}),
224
+ }));
225
+ if (execution.status === "timeout")
226
+ throw new Error("Request timeout");
227
+ if (execution.status === "lost")
228
+ throw new Error("The interpreter lost this run (its context stopped); run it again.");
229
+ const out = new Execution();
230
+ out.results = await Promise.all(execution.results.map((result) => this.#inline(result)));
231
+ if (options.onResult)
232
+ for (const result of out.results)
233
+ options.onResult(result);
234
+ out.logs.stdout = lines(execution.stdout);
235
+ out.logs.stderr = lines(execution.stderr);
236
+ if (execution.error) {
237
+ out.error = new ExecutionError(execution.error.name, execution.error.value, execution.error.traceback);
238
+ if (!streaming)
239
+ options.onError?.(out.error);
240
+ }
241
+ out.executionCount = execution.executionCount;
242
+ return out;
243
+ }
244
+ /** A new context: its own variables, directory and the sandbox's envs. */
245
+ async createCodeContext(options = {}) {
246
+ const language = languageOf(options.language);
247
+ const envs = await this.#sandboxEnvs();
248
+ const cwd = options.cwd ? options.cwd.replace(/^\/blaxel(?=\/|$)/, "/workspace") : undefined;
249
+ const made = await guard(() => this.#interpreter.contexts.create({
250
+ language,
251
+ ...(cwd ? { cwd } : {}),
252
+ ...(Object.keys(envs).length ? { env: envs } : {}),
253
+ }));
254
+ return new Context(made.id);
255
+ }
256
+ }
@@ -0,0 +1,37 @@
1
+ import type { Runtime } from "../client.js";
2
+ import type { RuntimeSandbox } from "./client.js";
3
+ /** Labels the adapter keeps on a sandbox, under this prefix; the Python
4
+ * adapter uses the same keys, so either reads what the other wrote: image,
5
+ * memory, ports ("3000,8080"), region, ttl, expires (ISO), lifecycle (JSON),
6
+ * externalId, displayName, archived ("1"), preview.<name> (its port) and
7
+ * idlePauseSeconds (the idle pause to give back after keepAlive processes). */
8
+ export declare const LABEL = "blaxel/";
9
+ export declare const EXTERNAL_ID = "blaxel/externalId";
10
+ export declare const DISPLAY_NAME = "blaxel/displayName";
11
+ export declare const ARCHIVED = "blaxel/archived";
12
+ export declare const IDLE_PAUSE = "blaxel/idlePauseSeconds";
13
+ /** A named preview: `blaxel/preview.<name>` holds its port. */
14
+ export declare const PREVIEW = "blaxel/preview.";
15
+ /** The caller's labels, without the adapter's. */
16
+ export declare function userLabels(labels?: Record<string, string>): Record<string, string>;
17
+ /** The adapter's labels, without the caller's. */
18
+ export declare function ownLabels(labels?: Record<string, string>): Record<string, string>;
19
+ /** How a Blaxel sandbox behaves on Runtime: it pauses (Blaxel's standby)
20
+ * after the shortest idle time Runtime allows, wakes by itself on the next
21
+ * call, and pauses rather than ends when its lease runs out. */
22
+ export declare const STANDBY: {
23
+ idlePauseSeconds: number;
24
+ autoWake: true;
25
+ };
26
+ /** Runtime's longest lease. The adapter renews it while the sandbox is used. */
27
+ export declare const LEASE_SECONDS = 3600;
28
+ /** The live sandbox with this name (or Runtime id): one call. A 404 when
29
+ * there is none, as Blaxel answers. */
30
+ export declare function findSandbox(client: Runtime, name: string): Promise<RuntimeSandbox>;
31
+ /** The newest live sandbox carrying `externalId`. */
32
+ export declare function findByExternalId(client: Runtime, externalId: string): Promise<RuntimeSandbox>;
33
+ /** Runs a create or fork that names a sandbox. When the name is still held
34
+ * by a sandbox that is stopping (deleted a moment ago, by this client or
35
+ * another), waits for it to stop, up to 30 seconds, and runs it once more, as
36
+ * Blaxel's createIfNotExists waits out a sandbox being deleted. */
37
+ export declare function whenNameFree<T>(client: Runtime, work: () => Promise<T>): Promise<T>;
@@ -0,0 +1,82 @@
1
+ import { RuntimeError } from "../errors.js";
2
+ /* Finding a Blaxel sandbox on Runtime, and the labels that carry what Runtime
3
+ has no field for. The labels are the adapter's own: `metadata.labels`
4
+ shows only the caller's. */
5
+ /** Labels the adapter keeps on a sandbox, under this prefix; the Python
6
+ * adapter uses the same keys, so either reads what the other wrote: image,
7
+ * memory, ports ("3000,8080"), region, ttl, expires (ISO), lifecycle (JSON),
8
+ * externalId, displayName, archived ("1"), preview.<name> (its port) and
9
+ * idlePauseSeconds (the idle pause to give back after keepAlive processes). */
10
+ export const LABEL = "blaxel/";
11
+ export const EXTERNAL_ID = `${LABEL}externalId`;
12
+ export const DISPLAY_NAME = `${LABEL}displayName`;
13
+ export const ARCHIVED = `${LABEL}archived`;
14
+ export const IDLE_PAUSE = `${LABEL}idlePauseSeconds`;
15
+ /** A named preview: `blaxel/preview.<name>` holds its port. */
16
+ export const PREVIEW = `${LABEL}preview.`;
17
+ const OWN = /^blaxel\//;
18
+ /** The caller's labels, without the adapter's. */
19
+ export function userLabels(labels = {}) {
20
+ return Object.fromEntries(Object.entries(labels).filter(([key]) => !OWN.test(key)));
21
+ }
22
+ /** The adapter's labels, without the caller's. */
23
+ export function ownLabels(labels = {}) {
24
+ return Object.fromEntries(Object.entries(labels).filter(([key]) => OWN.test(key)));
25
+ }
26
+ /** How a Blaxel sandbox behaves on Runtime: it pauses (Blaxel's standby)
27
+ * after the shortest idle time Runtime allows, wakes by itself on the next
28
+ * call, and pauses rather than ends when its lease runs out. */
29
+ export const STANDBY = {
30
+ idlePauseSeconds: 60,
31
+ autoWake: true,
32
+ };
33
+ /** Runtime's longest lease. The adapter renews it while the sandbox is used. */
34
+ export const LEASE_SECONDS = 3600;
35
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
36
+ /** The live sandbox with this name (or Runtime id): one call. A 404 when
37
+ * there is none, as Blaxel answers. */
38
+ export async function findSandbox(client, name) {
39
+ if (UUID.test(name))
40
+ return client.sandboxes.get(name);
41
+ const page = await client.sandboxes.list({ name });
42
+ const found = page.data.filter((one) => one.state !== "stopped").at(-1);
43
+ if (!found)
44
+ throw notFound(`Sandbox ${name} was not found.`);
45
+ return found;
46
+ }
47
+ /** The newest live sandbox carrying `externalId`. */
48
+ export async function findByExternalId(client, externalId) {
49
+ const page = await client.sandboxes.list({ labels: { [EXTERNAL_ID]: externalId } });
50
+ const found = page.data.filter((one) => one.state !== "stopped").at(-1);
51
+ if (!found)
52
+ throw notFound(`No sandbox has the external id ${externalId}.`);
53
+ return found;
54
+ }
55
+ /** Runs a create or fork that names a sandbox. When the name is still held
56
+ * by a sandbox that is stopping (deleted a moment ago, by this client or
57
+ * another), waits for it to stop, up to 30 seconds, and runs it once more, as
58
+ * Blaxel's createIfNotExists waits out a sandbox being deleted. */
59
+ export async function whenNameFree(client, work) {
60
+ try {
61
+ return await work();
62
+ }
63
+ catch (error) {
64
+ const holder = error instanceof RuntimeError ? error.details?.sandboxId : undefined;
65
+ if (!(error instanceof RuntimeError) ||
66
+ error.code !== "name_taken" ||
67
+ error.details?.state !== "stopping" ||
68
+ typeof holder !== "string")
69
+ throw error;
70
+ const stopping = await client.sandboxes.get(holder);
71
+ await stopping.waitFor("stopped", { timeoutSeconds: 30 });
72
+ return work();
73
+ }
74
+ }
75
+ function notFound(message) {
76
+ return new RuntimeError({
77
+ message,
78
+ code: "not_found",
79
+ status: 404,
80
+ hint: "SandboxInstance.createIfNotExists({ name }) makes it when it is missing.",
81
+ });
82
+ }
@@ -0,0 +1,39 @@
1
+ import type { Page } from "../page.js";
2
+ export type PaginatedListMeta = {
3
+ hasMore?: boolean;
4
+ nextCursor?: string;
5
+ total?: number;
6
+ totalIsPartial?: boolean;
7
+ };
8
+ export type CursorPaginationQuery = {
9
+ cursor?: string;
10
+ };
11
+ export type AutoPagingEachCallback<T> = (item: T) => boolean | void | Promise<boolean | void>;
12
+ export type AutoPagingToArrayOptions = {
13
+ limit: number;
14
+ };
15
+ export type ListResponse<T> = T[] | {
16
+ data?: T[] | null;
17
+ meta?: PaginatedListMeta | null;
18
+ } | null | undefined;
19
+ export type PaginatedList<T, TQuery extends CursorPaginationQuery = CursorPaginationQuery> = AsyncIterable<T> & {
20
+ data: T[];
21
+ meta: PaginatedListMeta;
22
+ hasMore: boolean;
23
+ nextCursor?: string;
24
+ nextPage(): Promise<PaginatedList<T, TQuery> | null>;
25
+ autoPagingEach(onItem: AutoPagingEachCallback<T>): Promise<void>;
26
+ autoPagingToArray(options: AutoPagingToArrayOptions): Promise<T[]>;
27
+ };
28
+ /** Blaxel's own helper: the items of a list answer in any of its shapes. */
29
+ export declare function unwrapListData<T>(response: ListResponse<T>): T[];
30
+ /** Blaxel's own helper, for a list fetched by `fetchPage`. */
31
+ export declare function createPaginatedList<TRaw, TItem, TQuery extends CursorPaginationQuery>({ response, fetchPage, mapItem, query, }: {
32
+ response: ListResponse<TRaw>;
33
+ fetchPage: (query?: TQuery) => Promise<ListResponse<TRaw>>;
34
+ mapItem: (item: TRaw) => TItem | Promise<TItem>;
35
+ query?: TQuery;
36
+ seenCursors?: Set<string>;
37
+ }): Promise<PaginatedList<TItem, TQuery>>;
38
+ /** A Runtime page, each item mapped, as Blaxel's paginated list. */
39
+ export declare function paginate<TRaw, TItem>(page: Page<TRaw>, map: (item: TRaw) => TItem): PaginatedList<TItem>;
@@ -0,0 +1,60 @@
1
+ import { guard } from "./errors.js";
2
+ /** Blaxel's own helper: the items of a list answer in any of its shapes. */
3
+ export function unwrapListData(response) {
4
+ if (!response)
5
+ return [];
6
+ if (Array.isArray(response))
7
+ return response;
8
+ return response.data ?? [];
9
+ }
10
+ /** Blaxel's own helper, for a list fetched by `fetchPage`. */
11
+ export async function createPaginatedList({ response, fetchPage, mapItem, query, }) {
12
+ const meta = !response || Array.isArray(response) ? {} : (response.meta ?? {});
13
+ const data = await Promise.all(unwrapListData(response).map(mapItem));
14
+ return build(data, meta.nextCursor || undefined, async (cursor) => createPaginatedList({
15
+ response: await fetchPage({ ...(query ?? {}), cursor }),
16
+ fetchPage,
17
+ mapItem,
18
+ ...(query ? { query: { ...query, cursor } } : {}),
19
+ }));
20
+ }
21
+ /** A Runtime page, each item mapped, as Blaxel's paginated list. */
22
+ export function paginate(page, map) {
23
+ return build(page.data.map(map), page.nextCursor ?? undefined, async () => {
24
+ const next = await guard(() => page.next());
25
+ return next ? paginate(next, map) : null;
26
+ });
27
+ }
28
+ function build(data, nextCursor, more) {
29
+ const list = {
30
+ data,
31
+ meta: { hasMore: nextCursor !== undefined, ...(nextCursor ? { nextCursor } : {}) },
32
+ hasMore: nextCursor !== undefined,
33
+ ...(nextCursor ? { nextCursor } : {}),
34
+ nextPage: async () => (nextCursor ? more(nextCursor) : null),
35
+ async autoPagingEach(onItem) {
36
+ for await (const item of list)
37
+ if ((await onItem(item)) === false)
38
+ return;
39
+ },
40
+ async autoPagingToArray(options) {
41
+ if (!options || !Number.isFinite(options.limit) || options.limit <= 0)
42
+ throw new Error("autoPagingToArray requires a positive limit");
43
+ const items = [];
44
+ for await (const item of list) {
45
+ items.push(item);
46
+ if (items.length >= options.limit)
47
+ break;
48
+ }
49
+ return items;
50
+ },
51
+ async *[Symbol.asyncIterator]() {
52
+ let current = list;
53
+ while (current) {
54
+ yield* current.data;
55
+ current = await current.nextPage();
56
+ }
57
+ },
58
+ };
59
+ return list;
60
+ }
@@ -0,0 +1,110 @@
1
+ import type { RuntimeSandbox } from "./client.js";
2
+ export type PreviewMetadata = {
3
+ name: string;
4
+ displayName?: string;
5
+ resourceName?: string;
6
+ resourceType?: string;
7
+ workspace?: string;
8
+ createdAt?: string;
9
+ updatedAt?: string;
10
+ };
11
+ export type PreviewSpec = {
12
+ customDomain?: string;
13
+ expires?: string;
14
+ port?: number;
15
+ prefixUrl?: string;
16
+ public?: boolean;
17
+ readonly region?: string;
18
+ requestHeaders?: {
19
+ [key: string]: string;
20
+ };
21
+ responseHeaders?: {
22
+ [key: string]: string;
23
+ };
24
+ ttl?: string;
25
+ readonly url?: string;
26
+ };
27
+ export type Preview = {
28
+ events?: Array<Record<string, unknown>>;
29
+ metadata: PreviewMetadata;
30
+ spec: PreviewSpec;
31
+ status?: string;
32
+ };
33
+ export type PreviewToken = {
34
+ metadata: {
35
+ name: string;
36
+ resourceName?: string;
37
+ previewName?: string;
38
+ };
39
+ spec: {
40
+ readonly expired?: boolean;
41
+ expiresAt?: string;
42
+ readonly token?: string;
43
+ };
44
+ };
45
+ /** What the previews need from their sandbox. */
46
+ export interface PreviewContext {
47
+ readonly sandboxName: string;
48
+ run<T>(work: (runtime: RuntimeSandbox) => Promise<T>): Promise<T>;
49
+ /** The sandbox's labels as last read. */
50
+ labels(): Record<string, string>;
51
+ /** Sets (or with undefined removes) some of the adapter's labels. */
52
+ setLabels(changes: Record<string, string | undefined>): Promise<void>;
53
+ }
54
+ /** A preview token, as Blaxel's. Runtime reads it from the
55
+ * `x-runtime-preview-token` header or `?runtime_preview_token=`. */
56
+ export declare class SandboxPreviewToken {
57
+ #private;
58
+ constructor(previewToken: PreviewToken);
59
+ get value(): string;
60
+ get expiresAt(): string | Date;
61
+ get expired(): boolean;
62
+ }
63
+ /** A preview's tokens. Runtime signs a token for a duration; it keeps no
64
+ * list of the tokens issued. */
65
+ export declare class SandboxPreviewTokens {
66
+ #private;
67
+ constructor(preview: SandboxPreview, ctx: PreviewContext);
68
+ get previewName(): string;
69
+ get resourceName(): string;
70
+ /** A token that lasts until `expiresAt` (at least a minute, at most a week). */
71
+ create(expiresAt: Date): Promise<SandboxPreviewToken>;
72
+ list(): Promise<never>;
73
+ /** Revokes the preview's tokens. Runtime cannot revoke one token alone, so
74
+ * every token issued for this preview stops working. */
75
+ delete(_tokenName: string): Promise<{
76
+ message?: string;
77
+ }>;
78
+ }
79
+ /** A shared port, as Blaxel's SandboxPreview. */
80
+ export declare class SandboxPreview {
81
+ #private;
82
+ readonly tokens: SandboxPreviewTokens;
83
+ constructor(preview: Preview, ctx: PreviewContext);
84
+ get name(): string;
85
+ get metadata(): PreviewMetadata;
86
+ get spec(): PreviewSpec;
87
+ }
88
+ /** `sandbox.previews`. */
89
+ export declare class SandboxPreviews {
90
+ #private;
91
+ constructor(ctx: PreviewContext);
92
+ get sandboxName(): string;
93
+ /** Every shared port. One Runtime shared without a Blaxel name is named
94
+ * `preview-<port>`. */
95
+ list(): Promise<SandboxPreview[]>;
96
+ /** Shares `spec.port`: public with `spec.public`, else behind a token. */
97
+ create(preview: Preview, _force?: boolean): Promise<SandboxPreview>;
98
+ /** The named preview, or a new one when there is none. */
99
+ createIfNotExists(preview: Preview, force?: boolean): Promise<SandboxPreview>;
100
+ get(previewName: string): Promise<SandboxPreview>;
101
+ /** Stops sharing the port; open connections close within seconds. */
102
+ delete(previewName: string): Promise<Preview>;
103
+ }
104
+ /** A private preview of `port` with a token that lasts a while, for
105
+ * `sandbox.fetch`: made once, then reused until its token nears its end. */
106
+ export declare function portAccess(runtime: RuntimeSandbox, port: number): Promise<{
107
+ url: string;
108
+ token: string | null;
109
+ until: number;
110
+ }>;