withruntime 0.23.0 → 0.33.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.
- package/COMPATIBILITY.md +66 -5
- package/E2B.md +1 -1
- package/README.md +36 -2
- package/README.src.md +35 -1
- package/compatibility-lock.json +3 -3
- package/dist/agents/browser.d.ts +12 -0
- package/dist/agents/browser.js +12 -0
- package/dist/agents/index.d.ts +7 -7
- package/dist/agents/index.js +7 -8
- package/dist/agents-cli.d.ts +1 -1
- package/dist/agents-cli.js +250 -3
- package/dist/agents-deploy.d.ts +186 -0
- package/dist/agents-deploy.js +752 -0
- package/dist/agents-file.d.ts +19 -0
- package/dist/agents-file.js +135 -0
- package/dist/apps/index.d.ts +10 -0
- package/dist/apps/index.js +11 -0
- package/dist/apps-cli.d.ts +3 -1
- package/dist/apps-cli.js +285 -38
- package/dist/blaxel/errors.js +2 -1
- package/dist/ci/index.d.ts +13 -0
- package/dist/ci/index.js +14 -0
- package/dist/ci-cli.d.ts +10 -0
- package/dist/ci-cli.js +402 -0
- package/dist/ci-import.d.ts +23 -0
- package/dist/ci-import.js +565 -0
- package/dist/cli.d.ts +18 -7
- package/dist/cli.js +775 -85
- package/dist/daytona/errors.js +2 -1
- package/dist/e2b/errors.js +2 -1
- package/dist/e2b/filesystem.d.ts +1 -0
- package/dist/e2b/filesystem.js +1 -1
- package/dist/e2b/sandbox.d.ts +11 -2
- package/dist/e2b/sandbox.js +22 -6
- package/dist/errors.d.ts +11 -4
- package/dist/errors.js +27 -4
- package/dist/fly/index.d.ts +102 -0
- package/dist/fly/index.js +930 -0
- package/dist/index.d.ts +7 -3
- package/dist/machine.d.ts +10 -6
- package/dist/machine.js +14 -6
- package/dist/modal/index.d.ts +34 -0
- package/dist/modal/index.js +128 -22
- package/dist/products/agents.d.ts +101 -3
- package/dist/products/agents.js +49 -0
- package/dist/products/apps.d.ts +206 -10
- package/dist/products/apps.js +172 -1
- package/dist/products/ci.d.ts +335 -0
- package/dist/products/ci.js +108 -0
- package/dist/products/index.d.ts +8 -0
- package/dist/products/index.js +15 -0
- package/dist/products/jobs.d.ts +105 -5
- package/dist/products/jobs.js +47 -3
- package/dist/products/network.d.ts +13 -0
- package/dist/products/observability.d.ts +1 -1
- package/dist/products/plans.d.ts +131 -0
- package/dist/products/plans.js +24 -0
- package/dist/products/reservations.d.ts +108 -0
- package/dist/products/reservations.js +41 -0
- package/dist/products/resources.d.ts +2 -1
- package/dist/products/secrets.d.ts +90 -6
- package/dist/products/secrets.js +8 -2
- package/dist/proxy.d.ts +3 -3
- package/dist/proxy.js +20 -0
- package/dist/sandbox.d.ts +29 -1
- package/dist/sandbox.js +56 -3
- package/dist/snapshots.d.ts +1 -1
- package/dist/switched-off.d.ts +5 -0
- package/dist/switched-off.js +8 -0
- package/dist/tar.d.ts +1 -1
- package/dist/tar.js +19 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +48 -9
- package/dist/types.d.ts +35 -4
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +2 -1
- package/dist/vercel/snapshot.js +4 -2
- package/package.json +23 -6
package/dist/products/agents.js
CHANGED
|
@@ -71,6 +71,10 @@ export const AGENT_TEMPLATES = {
|
|
|
71
71
|
definition: { command: "python3 ~/agent.py", secrets: ["OPENAI_API_KEY"] },
|
|
72
72
|
},
|
|
73
73
|
};
|
|
74
|
+
/** A harness by a key read from a file or a flag, or undefined. */
|
|
75
|
+
export function agentTemplate(key) {
|
|
76
|
+
return Object.hasOwn(AGENT_TEMPLATES, key) ? AGENT_TEMPLATES[key] : undefined;
|
|
77
|
+
}
|
|
74
78
|
export const AGENT_USES = {
|
|
75
79
|
support: {
|
|
76
80
|
key: "support",
|
|
@@ -162,6 +166,20 @@ export function agents(t) {
|
|
|
162
166
|
});
|
|
163
167
|
return new Page(page.data, page.nextCursor, (cursor) => listSessions({ ...query, cursor }, options));
|
|
164
168
|
};
|
|
169
|
+
const listMemory = async (agent, query = {}, options) => {
|
|
170
|
+
const page = await t.json({
|
|
171
|
+
method: "GET",
|
|
172
|
+
path: `/v1/agents/${enc(agent)}/memory`,
|
|
173
|
+
query,
|
|
174
|
+
...options,
|
|
175
|
+
});
|
|
176
|
+
return Object.assign(new Page(page.data, page.nextCursor, (cursor) => listMemory(agent, { ...query, cursor }, options)), { usage: page.usage });
|
|
177
|
+
};
|
|
178
|
+
const memoryFile = (method, agent, path, scope = "") => ({
|
|
179
|
+
method,
|
|
180
|
+
path: `/v1/agents/${enc(agent)}/memory/content`,
|
|
181
|
+
query: { scope, path },
|
|
182
|
+
});
|
|
165
183
|
return {
|
|
166
184
|
/** An agent: a name and a definition its sessions start from. */
|
|
167
185
|
create: (input, options) => t.json({ method: "POST", path: "/v1/agents", body: input, ...options }),
|
|
@@ -242,6 +260,37 @@ export function agents(t) {
|
|
|
242
260
|
});
|
|
243
261
|
},
|
|
244
262
|
},
|
|
263
|
+
/** Its built-in memory: text files kept across sessions, which the
|
|
264
|
+
* agent reads, writes and searches from inside its sessions. A scope is
|
|
265
|
+
* whose: "" (the default) for what every session reads, or a session key
|
|
266
|
+
* ("customer:42") for that key's alone. Up to 1 MiB a file, 1 GiB and
|
|
267
|
+
* 100,000 files an agent, billed as storage. */
|
|
268
|
+
memory: {
|
|
269
|
+
/** Its files by scope and path, a page at a time, with its usage.
|
|
270
|
+
* Without scope, every scope. */
|
|
271
|
+
list: listMemory,
|
|
272
|
+
/** The files whose words or paths match, best first, each with a
|
|
273
|
+
* snippet: words, "a phrase", or, -not. Without scope, every scope. */
|
|
274
|
+
search: async (agent, q, query = {}, options) => (await t.json({
|
|
275
|
+
method: "GET",
|
|
276
|
+
path: `/v1/agents/${enc(agent)}/memory`,
|
|
277
|
+
query: { ...query, q },
|
|
278
|
+
...options,
|
|
279
|
+
})).data,
|
|
280
|
+
/** One file, with its text; not_found when there is none. */
|
|
281
|
+
read: (agent, path, query = {}, options) => t.json({ ...memoryFile("GET", agent, path, query.scope), ...options }),
|
|
282
|
+
/** Seeds or replaces a file. */
|
|
283
|
+
write: (agent, path, content, query = {}, options) => t.json({
|
|
284
|
+
...memoryFile("PUT", agent, path, query.scope),
|
|
285
|
+
body: { content },
|
|
286
|
+
...options,
|
|
287
|
+
}),
|
|
288
|
+
/** Answers whether there was a file. */
|
|
289
|
+
delete: async (agent, path, query = {}, options) => (await t.json({
|
|
290
|
+
...memoryFile("DELETE", agent, path, query.scope),
|
|
291
|
+
...options,
|
|
292
|
+
})).deleted,
|
|
293
|
+
},
|
|
245
294
|
/** The harnesses, as data: claude-code, codex, gemini-cli,
|
|
246
295
|
* claude-agent-sdk, openai-agents-sdk. */
|
|
247
296
|
templates: AGENT_TEMPLATES,
|
package/dist/products/apps.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Sandbox } from "../sandbox.js";
|
|
2
|
+
import { type RequestOptions, type Transport } from "../transport.js";
|
|
2
3
|
import { type ResourceDeleteOptions, type ResourceGetOptions, type ResourceListQuery, type ResourceWaitOptions } from "./resources.js";
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
4
|
+
/** On the client as runtime.apps for Apps' private beta: an account outside
|
|
5
|
+
* it is refused with product_in_beta. Plain request builders over Transport:
|
|
6
|
+
* nothing runs at import, no dependency beyond this package. A deployment's
|
|
7
|
+
* completion follows its own record, not the app's state. */
|
|
5
8
|
export type AppState = "waiting" | "live" | "failed" | "deleting" | "deleted";
|
|
9
|
+
/** What Runtime does when a new version's errors or speed get clearly worse
|
|
10
|
+
* in its first 15 minutes: tell the owners (alert, the default), go back to
|
|
11
|
+
* the previous version and tell them (rollback), or neither (off). */
|
|
12
|
+
export type ReleaseWatch = "rollback" | "alert" | "off";
|
|
6
13
|
export type AppSettings = {
|
|
7
14
|
minCopies: number;
|
|
8
15
|
maxCopies: number;
|
|
@@ -12,10 +19,34 @@ export type AppSettings = {
|
|
|
12
19
|
memoryMiB: number;
|
|
13
20
|
port: number;
|
|
14
21
|
healthPath?: string;
|
|
22
|
+
releaseWatch: ReleaseWatch;
|
|
23
|
+
/** Cloudflare's CDN in front of the App's addresses (its runtimehost.com
|
|
24
|
+
* address and its custom domains). On by default; false sends visitors
|
|
25
|
+
* straight to Runtime's routers, with nothing cached. */
|
|
26
|
+
cdn: boolean;
|
|
27
|
+
/** A live copy out of every router's rotation this long is replaced. */
|
|
28
|
+
healthFailSeconds: number;
|
|
15
29
|
};
|
|
16
|
-
|
|
30
|
+
/** One read-write volume of this account (POST /v1/volumes), mounted at path
|
|
31
|
+
* in the App's one copy. */
|
|
32
|
+
export type AppVolume = {
|
|
33
|
+
volumeId: string;
|
|
34
|
+
/** Under /data, /mnt, /state, /storage or /volumes, with no . or .. segments. */
|
|
35
|
+
path: string;
|
|
36
|
+
};
|
|
37
|
+
export type StatefulSettings = {
|
|
38
|
+
volume: AppVolume | null;
|
|
39
|
+
/** Seconds a stopped copy has after SIGTERM to exit. */
|
|
40
|
+
stopGraceSeconds: number;
|
|
41
|
+
};
|
|
42
|
+
export type App = AppSettings & StatefulSettings & {
|
|
17
43
|
id: string;
|
|
18
44
|
name: string | null;
|
|
45
|
+
/** Its address for life, its name then its account's:
|
|
46
|
+
* `https://shop-3f9a1c2e7b04d915-acme1.runtimehost.com/`. A rename leaves it as it is; its
|
|
47
|
+
* id's address (`https://<id without dashes>.runtimehost.com/`) answers
|
|
48
|
+
* too. */
|
|
49
|
+
url: string;
|
|
19
50
|
labels: Record<string, string>;
|
|
20
51
|
state: AppState;
|
|
21
52
|
createdAt: number;
|
|
@@ -27,9 +58,21 @@ export type App = AppSettings & {
|
|
|
27
58
|
} | null;
|
|
28
59
|
liveVersion: number | null;
|
|
29
60
|
previousVersion: number | null;
|
|
61
|
+
/** While a copy waits for room in its region: its account's lane, its
|
|
62
|
+
* place in line (1 is next), how many wait ahead by lane, and why.
|
|
63
|
+
* Higher lanes get room first. Absent while nothing of it waits. */
|
|
64
|
+
line?: AppLine;
|
|
30
65
|
};
|
|
31
|
-
export type
|
|
66
|
+
export type AppLine = {
|
|
67
|
+
lane: string;
|
|
68
|
+
place: number;
|
|
69
|
+
ahead: Record<string, number>;
|
|
70
|
+
words: string;
|
|
71
|
+
};
|
|
72
|
+
export type RuntimeFunction = Omit<App, "minCopies" | keyof StatefulSettings> & {
|
|
32
73
|
minCopies: 0;
|
|
74
|
+
/** 250 to 5000 ms: at most how long a copy stays awake after its last request ends. */
|
|
75
|
+
sleepAfterMs: number;
|
|
33
76
|
};
|
|
34
77
|
export type CreateFunction = {
|
|
35
78
|
/** Up to 128 letters, digits, dots, dashes or underscores; alphanumeric first. */
|
|
@@ -51,32 +94,72 @@ export type CreateFunction = {
|
|
|
51
94
|
/** Starts with /, no whitespace, at most 256 characters.
|
|
52
95
|
* Omit to check whether the port accepts connections. */
|
|
53
96
|
healthPath?: string;
|
|
97
|
+
/** Defaults to "alert"; "rollback" is opted into. */
|
|
98
|
+
releaseWatch?: ReleaseWatch;
|
|
99
|
+
/** Cloudflare's CDN in front of the address and custom domains; defaults to
|
|
100
|
+
* true. false sends visitors straight to Runtime's routers, nothing cached. */
|
|
101
|
+
cdn?: boolean;
|
|
102
|
+
/** 10 to 3600; defaults to 60. A live copy failing its health check this
|
|
103
|
+
* long is replaced, and the version's history says so. */
|
|
104
|
+
healthFailSeconds?: number;
|
|
54
105
|
/** Only Apps can keep copies awake. Omit this field, including zero. */
|
|
55
106
|
minCopies?: never;
|
|
107
|
+
/** 250 to 5000 ms; defaults to 1000. How long a copy stays awake after its
|
|
108
|
+
* last request ends before it goes back to sleep, at most (copies whose
|
|
109
|
+
* requests come closer together than this stay awake between them; a lone
|
|
110
|
+
* first request may sleep sooner, from 250 ms). Awake time with no
|
|
111
|
+
* request in flight is not billed. */
|
|
112
|
+
sleepAfterMs?: number;
|
|
56
113
|
};
|
|
57
|
-
export type CreateApp = Omit<CreateFunction, "minCopies"> & {
|
|
58
|
-
/** Defaults to 0. No greater than maxCopies. */
|
|
114
|
+
export type CreateApp = Omit<CreateFunction, "minCopies" | "sleepAfterMs"> & {
|
|
115
|
+
/** Defaults to 0, or 1 with a volume. No greater than maxCopies. */
|
|
59
116
|
minCopies?: number;
|
|
117
|
+
/** Only Functions sleep on this rule; an App is refused it. */
|
|
118
|
+
sleepAfterMs?: never;
|
|
119
|
+
/** Needs maxCopies 1, minCopies 1 and spares 0 (spares defaults to 0 with a
|
|
120
|
+
* volume), placed on the volume's server. Deploys, rollbacks and restarts
|
|
121
|
+
* then stop the old copy before the new one boots fresh with the volume:
|
|
122
|
+
* an image's start, or a build's files with its start command (a build
|
|
123
|
+
* deployed here saves its files rather than its memory). The deploy record
|
|
124
|
+
* says the downtime. */
|
|
125
|
+
volume?: AppVolume;
|
|
126
|
+
/** 0 to 900; defaults to 30. */
|
|
127
|
+
stopGraceSeconds?: number;
|
|
60
128
|
};
|
|
61
|
-
export type UpdateApp = Partial<AppSettings> & {
|
|
129
|
+
export type UpdateApp = Partial<AppSettings & StatefulSettings> & {
|
|
62
130
|
name?: string | null;
|
|
63
131
|
labels?: Record<string, string>;
|
|
132
|
+
/** Only Functions sleep on this rule; an App is refused it. */
|
|
133
|
+
sleepAfterMs?: never;
|
|
64
134
|
};
|
|
65
|
-
export type UpdateFunction = Omit<UpdateApp, "minCopies"> & {
|
|
135
|
+
export type UpdateFunction = Omit<UpdateApp, "minCopies" | "sleepAfterMs" | keyof StatefulSettings> & {
|
|
66
136
|
minCopies?: never;
|
|
137
|
+
/** 250 to 5000 ms; see CreateFunction.sleepAfterMs. */
|
|
138
|
+
sleepAfterMs?: number;
|
|
67
139
|
};
|
|
68
140
|
export type DeployState = "building" | "warming" | "switching" | "live" | "failed" | "superseded";
|
|
141
|
+
export type AppDowntime = {
|
|
142
|
+
stopMs: number;
|
|
143
|
+
bootMs: number;
|
|
144
|
+
totalMs: number;
|
|
145
|
+
};
|
|
69
146
|
export type AppDeploy = {
|
|
70
147
|
id: string;
|
|
71
148
|
version: number;
|
|
72
149
|
url: string;
|
|
150
|
+
/** The account secrets whose placeholders its copies see; absent when none. */
|
|
151
|
+
secrets?: string[];
|
|
152
|
+
/** A restart's: the version it started afresh. */
|
|
153
|
+
restartOf?: number;
|
|
154
|
+
/** A stop-then-start's (an App with a volume), once live. */
|
|
155
|
+
downtime?: AppDowntime;
|
|
73
156
|
} & ({
|
|
74
157
|
state: "building" | "warming" | "switching" | "live";
|
|
75
158
|
error?: never;
|
|
76
159
|
} | {
|
|
77
160
|
state: "failed" | "superseded";
|
|
78
161
|
error: {
|
|
79
|
-
code: "deploy_build_failed" | "deploy_health_failed" | "deploy_superseded";
|
|
162
|
+
code: "deploy_build_failed" | "deploy_health_failed" | "deploy_stop_failed" | "deploy_switch_failed" | "deploy_superseded";
|
|
80
163
|
message: string;
|
|
81
164
|
hint: string;
|
|
82
165
|
lastOutput: string;
|
|
@@ -100,6 +183,14 @@ export type DeployApp = ({
|
|
|
100
183
|
* may reveal the App's secrets, and only an environment named with an image
|
|
101
184
|
* deploy. Otherwise refused with keep_env_refused (409). */
|
|
102
185
|
keepEnv?: boolean;
|
|
186
|
+
/** Account secrets whose placeholders every copy of this version sees, each
|
|
187
|
+
* in the environment variable of its name; the egress proxy puts the value
|
|
188
|
+
* into the copy's requests to the secret's hosts, so the value never enters
|
|
189
|
+
* a copy. Each a secret of the whole account or one scoped to this App
|
|
190
|
+
* (`secrets.scope(name, { apps: [id] })`), and not also in `env`. Naming
|
|
191
|
+
* any needs a key that may reveal secrets. Absent: the last deploy's stay
|
|
192
|
+
* (refused for a key that may only deploy an image); `[]` names none. */
|
|
193
|
+
secrets?: string[];
|
|
103
194
|
port?: number;
|
|
104
195
|
healthPath?: string;
|
|
105
196
|
/** Return live, failed or superseded, including its URL/version or failure details. */
|
|
@@ -114,6 +205,17 @@ export type AppVersion = {
|
|
|
114
205
|
deploy: string;
|
|
115
206
|
/** The build it was deployed from, or null for an image. */
|
|
116
207
|
build: string | null;
|
|
208
|
+
/** saved: copies start from a saved server; cold: they boot fresh (an
|
|
209
|
+
* image, or a build's files) and start its process. */
|
|
210
|
+
boot: "saved" | "cold";
|
|
211
|
+
restartOf: number | null;
|
|
212
|
+
/** Live copies replaced for failing their health check or exiting. */
|
|
213
|
+
replacements: {
|
|
214
|
+
at: number;
|
|
215
|
+
reason: "health" | "exited";
|
|
216
|
+
downtimeMs?: number;
|
|
217
|
+
}[];
|
|
218
|
+
downtime: AppDowntime | null;
|
|
117
219
|
};
|
|
118
220
|
export type AppCopy = {
|
|
119
221
|
id: string;
|
|
@@ -122,6 +224,60 @@ export type AppCopy = {
|
|
|
122
224
|
state: "starting" | "serving" | "draining" | "frozen" | "parked";
|
|
123
225
|
since: number;
|
|
124
226
|
};
|
|
227
|
+
/** One copy's commands, processes and files, as a sandbox's: `exec`,
|
|
228
|
+
* `execStream`, `processes`, `files` (write, read, upload, download, ...).
|
|
229
|
+
* Only an owner's or admin's key reaches into a copy (403 otherwise): it holds
|
|
230
|
+
* the App's environment, its secrets included. */
|
|
231
|
+
export type AppCopyMachine = Pick<Sandbox, "id" | "exec" | "execStream" | "processes" | "files">;
|
|
232
|
+
export type AppCopyOptions = RequestOptions & {
|
|
233
|
+
/** A copy id from `copies()`. Omitted: a serving copy of the live version,
|
|
234
|
+
* or any serving copy while a deploy switches. */
|
|
235
|
+
copy?: string;
|
|
236
|
+
};
|
|
237
|
+
/** One line a copy printed. */
|
|
238
|
+
export type AppLogLine = {
|
|
239
|
+
/** Unix milliseconds, within a second of when the copy printed it. */
|
|
240
|
+
at: number;
|
|
241
|
+
stream: "stdout" | "stderr";
|
|
242
|
+
/** The copy that printed it. */
|
|
243
|
+
copy: string | null;
|
|
244
|
+
/** Its version, as versions() lists them. */
|
|
245
|
+
version: number | null;
|
|
246
|
+
text: string;
|
|
247
|
+
};
|
|
248
|
+
/** The newest lines of a window, oldest first. */
|
|
249
|
+
export type AppLogPage = {
|
|
250
|
+
lines: AppLogLine[];
|
|
251
|
+
nextCursor: string | null;
|
|
252
|
+
hasMore: boolean;
|
|
253
|
+
};
|
|
254
|
+
/** A follow's events: each line once, and a gap where the log store dropped
|
|
255
|
+
* lines before they could be read. */
|
|
256
|
+
export type AppLogEvent = ({
|
|
257
|
+
type: "line";
|
|
258
|
+
cursor: string;
|
|
259
|
+
} & AppLogLine) | {
|
|
260
|
+
type: "gap";
|
|
261
|
+
cursor: string;
|
|
262
|
+
};
|
|
263
|
+
export type AppLogOptions = {
|
|
264
|
+
/** A time (2026-10-18T20:00:00Z) or a duration back from now (30s, 15m,
|
|
265
|
+
* 2h, 7d). Default 1h; lines are kept 14 days. */
|
|
266
|
+
since?: string;
|
|
267
|
+
/** Only this version's copies. */
|
|
268
|
+
version?: number;
|
|
269
|
+
/** Only this copy. */
|
|
270
|
+
copy?: string;
|
|
271
|
+
stream?: "stdout" | "stderr";
|
|
272
|
+
/** 1 to 500, default 100: a page, or the lines a follow starts with. */
|
|
273
|
+
limit?: number;
|
|
274
|
+
/** A page's nextCursor (the page before it), or with follow an event's
|
|
275
|
+
* cursor to resume after it. */
|
|
276
|
+
cursor?: string;
|
|
277
|
+
/** Stream new lines as copies print them, until the signal aborts or
|
|
278
|
+
* timeoutMs passes (none by default); the SDK reconnects by itself. */
|
|
279
|
+
follow?: boolean;
|
|
280
|
+
};
|
|
125
281
|
export type DeployGetOptions = RequestOptions & {
|
|
126
282
|
waitFor?: DeployState;
|
|
127
283
|
timeoutSeconds?: number;
|
|
@@ -131,6 +287,10 @@ export type AppListQuery = ResourceListQuery<AppState>;
|
|
|
131
287
|
export type AppGetOptions = ResourceGetOptions<AppState>;
|
|
132
288
|
export type AppWaitOptions = ResourceWaitOptions;
|
|
133
289
|
export type AppDeleteOptions = ResourceDeleteOptions;
|
|
290
|
+
/** The copy `wanted` names, else a serving copy of the live version, else any
|
|
291
|
+
* serving copy (a deploy may be switching). Refuses with what to do rather
|
|
292
|
+
* than wake a frozen copy, which the App's own traffic does. */
|
|
293
|
+
export declare function pickCopy(app: Pick<App, "id" | "liveVersion">, list: readonly AppCopy[], wanted?: string): AppCopy;
|
|
134
294
|
export declare function apps(t: Transport): {
|
|
135
295
|
/** Create a resource waiting for its first version. Defaults belong to
|
|
136
296
|
* the API, so the SDK does not override them. */
|
|
@@ -142,8 +302,26 @@ export declare function apps(t: Transport): {
|
|
|
142
302
|
/** Resume by deploy id after a process restart, without creating another version. */
|
|
143
303
|
waitDeploy: (id: string, deployId: string, options?: AppWaitOptions) => Promise<AppDeploy>;
|
|
144
304
|
rollback: (id: string, options?: AppDeleteOptions) => Promise<AppDeploy>;
|
|
305
|
+
/** Start a fresh copy of the live version as a deploy of its own; with a
|
|
306
|
+
* volume, the old copy stops first. restart_unavailable: nothing is live. */
|
|
307
|
+
restart: (id: string, options?: AppDeleteOptions) => Promise<AppDeploy>;
|
|
145
308
|
versions: (id: string, options?: RequestOptions) => Promise<AppVersion[]>;
|
|
146
309
|
copies: (id: string, options?: RequestOptions) => Promise<AppCopy[]>;
|
|
310
|
+
/** Reach into one copy: `(await runtime.apps.copy(id)).exec("ls /srv")`.
|
|
311
|
+
* Picks the copy once, by GET .../copies; a copy the App replaces later
|
|
312
|
+
* answers not found, so pick again then. */
|
|
313
|
+
copy: (id: string, options?: AppCopyOptions) => Promise<AppCopyMachine>;
|
|
314
|
+
/** What its copies printed: a page of the newest lines, oldest first, or
|
|
315
|
+
* with follow: true each new line as it is printed. */
|
|
316
|
+
logs: {
|
|
317
|
+
(id: string, query: AppLogOptions & {
|
|
318
|
+
follow: true;
|
|
319
|
+
}, options?: RequestOptions): AsyncGenerator<AppLogEvent>;
|
|
320
|
+
(id: string, query?: (AppLogOptions & {
|
|
321
|
+
follow?: false;
|
|
322
|
+
}) | undefined, options?: RequestOptions): Promise<AppLogPage>;
|
|
323
|
+
(id: string, query: AppLogOptions, options?: RequestOptions): Promise<AppLogPage> | AsyncGenerator<AppLogEvent>;
|
|
324
|
+
};
|
|
147
325
|
update: (id: string, input: UpdateApp, options?: RequestOptions) => Promise<App>;
|
|
148
326
|
/** Follow resource state, not a deployment: an
|
|
149
327
|
* older live version can survive a failed rollout of a newer version. */
|
|
@@ -161,8 +339,26 @@ export declare function functions(t: Transport): {
|
|
|
161
339
|
/** Resume by deploy id after a process restart, without creating another version. */
|
|
162
340
|
waitDeploy: (id: string, deployId: string, options?: AppWaitOptions) => Promise<AppDeploy>;
|
|
163
341
|
rollback: (id: string, options?: AppDeleteOptions) => Promise<AppDeploy>;
|
|
342
|
+
/** Start a fresh copy of the live version as a deploy of its own; with a
|
|
343
|
+
* volume, the old copy stops first. restart_unavailable: nothing is live. */
|
|
344
|
+
restart: (id: string, options?: AppDeleteOptions) => Promise<AppDeploy>;
|
|
164
345
|
versions: (id: string, options?: RequestOptions) => Promise<AppVersion[]>;
|
|
165
346
|
copies: (id: string, options?: RequestOptions) => Promise<AppCopy[]>;
|
|
347
|
+
/** Reach into one copy: `(await runtime.apps.copy(id)).exec("ls /srv")`.
|
|
348
|
+
* Picks the copy once, by GET .../copies; a copy the App replaces later
|
|
349
|
+
* answers not found, so pick again then. */
|
|
350
|
+
copy: (id: string, options?: AppCopyOptions) => Promise<AppCopyMachine>;
|
|
351
|
+
/** What its copies printed: a page of the newest lines, oldest first, or
|
|
352
|
+
* with follow: true each new line as it is printed. */
|
|
353
|
+
logs: {
|
|
354
|
+
(id: string, query: AppLogOptions & {
|
|
355
|
+
follow: true;
|
|
356
|
+
}, options?: RequestOptions): AsyncGenerator<AppLogEvent>;
|
|
357
|
+
(id: string, query?: (AppLogOptions & {
|
|
358
|
+
follow?: false;
|
|
359
|
+
}) | undefined, options?: RequestOptions): Promise<AppLogPage>;
|
|
360
|
+
(id: string, query: AppLogOptions, options?: RequestOptions): Promise<AppLogPage> | AsyncGenerator<AppLogEvent>;
|
|
361
|
+
};
|
|
166
362
|
update: (id: string, input: UpdateFunction, options?: RequestOptions) => Promise<RuntimeFunction>;
|
|
167
363
|
/** Follow resource state, not a deployment: an
|
|
168
364
|
* older live version can survive a failed rollout of a newer version. */
|
package/dist/products/apps.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
|
+
import { ConnectionError, RuntimeError } from "../errors.js";
|
|
2
|
+
import { Sandbox } from "../sandbox.js";
|
|
3
|
+
import { timeoutSignal } from "../transport.js";
|
|
1
4
|
import { enc, following, resource, } from "./resources.js";
|
|
5
|
+
function logFailure(code, hint, cursor) {
|
|
6
|
+
return new RuntimeError({ code, status: 0, message: hint, hint, details: { cursor } });
|
|
7
|
+
}
|
|
8
|
+
const validLogCursor = (value) => typeof value === "string" &&
|
|
9
|
+
value.length > 0 &&
|
|
10
|
+
value.length <= 4096 &&
|
|
11
|
+
![...value].some((c) => c.charCodeAt(0) < 0x20 || c.charCodeAt(0) === 0x7f);
|
|
12
|
+
const validLine = (event) => typeof event.text === "string" &&
|
|
13
|
+
(event.stream === "stdout" || event.stream === "stderr") &&
|
|
14
|
+
Number.isSafeInteger(event.at) &&
|
|
15
|
+
event.at >= 0 &&
|
|
16
|
+
(event.copy === null || typeof event.copy === "string") &&
|
|
17
|
+
(event.version === null || Number.isSafeInteger(event.version));
|
|
2
18
|
function copies(t, plural) {
|
|
3
19
|
const shape = resource(t, plural, ["failed", "deleted", "live"]);
|
|
4
20
|
const { base, get } = shape;
|
|
@@ -21,11 +37,116 @@ function copies(t, plural) {
|
|
|
21
37
|
};
|
|
22
38
|
const writeDeploy = (id, verb, body, wait, options) => {
|
|
23
39
|
const { pollMs, ...request } = options;
|
|
24
|
-
const write = (rest) => t.json({
|
|
40
|
+
const write = (rest) => t.json({
|
|
41
|
+
method: "POST",
|
|
42
|
+
path: verb === "restart" ? `${base}/${enc(id)}:restart` : `${base}/${enc(id)}/${verb}`,
|
|
43
|
+
body,
|
|
44
|
+
...rest,
|
|
45
|
+
});
|
|
25
46
|
if (!wait)
|
|
26
47
|
return write(request);
|
|
27
48
|
return following({ ...request, pollMs, idempotencyKey: request.idempotencyKey ?? crypto.randomUUID() }, async (rest, pause) => followDeploy(id, await write(rest), rest, pause));
|
|
28
49
|
};
|
|
50
|
+
/** A follow: each line once, across the server's two-minute calls and
|
|
51
|
+
* dropped connections, resumed from the last cursor every time. */
|
|
52
|
+
async function* followLogs(id, query, options = {}) {
|
|
53
|
+
const { follow: _, cursor: from, ...filters } = query;
|
|
54
|
+
let cursor = from ?? null;
|
|
55
|
+
if (cursor !== null && !validLogCursor(cursor))
|
|
56
|
+
throw logFailure("invalid_log_options", "Pass an event's cursor unchanged.", cursor);
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
let failures = 0;
|
|
59
|
+
const deadline = options.timeoutMs ?? 0;
|
|
60
|
+
const signal = deadline > 0
|
|
61
|
+
? AbortSignal.any([...(options.signal ? [options.signal] : []), timeoutSignal(deadline)])
|
|
62
|
+
: options.signal;
|
|
63
|
+
for (;;) {
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
|
+
let continued = false;
|
|
66
|
+
try {
|
|
67
|
+
for await (const event of t.events({
|
|
68
|
+
method: "GET",
|
|
69
|
+
path: `${base}/${enc(id)}/logs`,
|
|
70
|
+
query: { ...filters, follow: true, ...(cursor === null ? {} : { cursor }) },
|
|
71
|
+
// One call lasts about two minutes; the follow, until it is stopped.
|
|
72
|
+
timeoutMs: 0,
|
|
73
|
+
...(signal ? { signal } : {}),
|
|
74
|
+
})) {
|
|
75
|
+
signal?.throwIfAborted();
|
|
76
|
+
if (!event ||
|
|
77
|
+
typeof event !== "object" ||
|
|
78
|
+
(event.cursor !== null && !validLogCursor(event.cursor)))
|
|
79
|
+
throw logFailure("app_log_invalid", "Resume from details.cursor.", cursor);
|
|
80
|
+
if (event.type === "line") {
|
|
81
|
+
if (!validLine(event) || !validLogCursor(event.cursor))
|
|
82
|
+
throw logFailure("app_log_invalid", "Resume from details.cursor.", cursor);
|
|
83
|
+
failures = 0;
|
|
84
|
+
cursor = event.cursor;
|
|
85
|
+
if (seen.has(event.cursor))
|
|
86
|
+
continue;
|
|
87
|
+
seen.add(event.cursor);
|
|
88
|
+
if (seen.size > 10_000)
|
|
89
|
+
seen.delete(seen.values().next().value);
|
|
90
|
+
yield event;
|
|
91
|
+
}
|
|
92
|
+
else if (event.type === "gap") {
|
|
93
|
+
if (!validLogCursor(event.cursor))
|
|
94
|
+
throw logFailure("app_log_invalid", "Resume from details.cursor.", cursor);
|
|
95
|
+
cursor = event.cursor;
|
|
96
|
+
yield { type: "gap", cursor: event.cursor };
|
|
97
|
+
}
|
|
98
|
+
else if (event.type === "continue") {
|
|
99
|
+
if (event.cursor !== null)
|
|
100
|
+
cursor = event.cursor;
|
|
101
|
+
continued = true;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
else if (event.type === "error") {
|
|
105
|
+
throw logFailure(typeof event.code === "string" && event.code ? event.code : "app_log_failed", typeof event.hint === "string" && event.hint
|
|
106
|
+
? event.hint
|
|
107
|
+
: "Resume from details.cursor in a few seconds.", cursor);
|
|
108
|
+
}
|
|
109
|
+
// An event this SDK does not know yet is passed over.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
signal?.throwIfAborted();
|
|
114
|
+
if (error instanceof SyntaxError)
|
|
115
|
+
throw logFailure("app_log_invalid", "Resume from details.cursor.", cursor);
|
|
116
|
+
// Refusals are the caller's to read; a lost connection is retried.
|
|
117
|
+
if (!(error instanceof TypeError) && !(error instanceof ConnectionError))
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
if (!continued && ++failures > 3)
|
|
121
|
+
throw logFailure("app_log_interrupted", "Follow again with cursor: details.cursor.", cursor);
|
|
122
|
+
await new Promise((resolve, reject) => {
|
|
123
|
+
const done = () => {
|
|
124
|
+
signal?.removeEventListener("abort", abort);
|
|
125
|
+
resolve();
|
|
126
|
+
};
|
|
127
|
+
const timer = setTimeout(done, continued ? 0 : 250 * failures);
|
|
128
|
+
const abort = () => {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
131
|
+
};
|
|
132
|
+
if (signal?.aborted)
|
|
133
|
+
abort();
|
|
134
|
+
else
|
|
135
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function logs(id, query = {}, options) {
|
|
140
|
+
if (query.follow)
|
|
141
|
+
return followLogs(id, query, options);
|
|
142
|
+
const { follow: _, ...page } = query;
|
|
143
|
+
return t.json({
|
|
144
|
+
method: "GET",
|
|
145
|
+
path: `${base}/${enc(id)}/logs`,
|
|
146
|
+
query: page,
|
|
147
|
+
...options,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
29
150
|
return {
|
|
30
151
|
/** Create a resource waiting for its first version. Defaults belong to
|
|
31
152
|
* the API, so the SDK does not override them. */
|
|
@@ -43,8 +164,28 @@ function copies(t, plural) {
|
|
|
43
164
|
const { wait = false, ...request } = options;
|
|
44
165
|
return writeDeploy(id, "rollback", {}, wait, request);
|
|
45
166
|
},
|
|
167
|
+
/** Start a fresh copy of the live version as a deploy of its own; with a
|
|
168
|
+
* volume, the old copy stops first. restart_unavailable: nothing is live. */
|
|
169
|
+
restart: (id, options = {}) => {
|
|
170
|
+
const { wait = false, ...request } = options;
|
|
171
|
+
return writeDeploy(id, "restart", {}, wait, request);
|
|
172
|
+
},
|
|
46
173
|
versions: (id, options) => t.json({ method: "GET", path: `${base}/${enc(id)}/versions`, ...options }),
|
|
47
174
|
copies: (id, options) => t.json({ method: "GET", path: `${base}/${enc(id)}/copies`, ...options }),
|
|
175
|
+
/** Reach into one copy: `(await runtime.apps.copy(id)).exec("ls /srv")`.
|
|
176
|
+
* Picks the copy once, by GET .../copies; a copy the App replaces later
|
|
177
|
+
* answers not found, so pick again then. */
|
|
178
|
+
copy: async (id, options = {}) => {
|
|
179
|
+
const { copy: wanted, ...request } = options;
|
|
180
|
+
const [app, list] = await Promise.all([
|
|
181
|
+
get(id, request),
|
|
182
|
+
t.json({ method: "GET", path: `${base}/${enc(id)}/copies`, ...request }),
|
|
183
|
+
]);
|
|
184
|
+
return new Sandbox(t, { id: pickCopy(app, list, wanted).id });
|
|
185
|
+
},
|
|
186
|
+
/** What its copies printed: a page of the newest lines, oldest first, or
|
|
187
|
+
* with follow: true each new line as it is printed. */
|
|
188
|
+
logs,
|
|
48
189
|
update: shape.update,
|
|
49
190
|
/** Follow resource state, not a deployment: an
|
|
50
191
|
* older live version can survive a failed rollout of a newer version. */
|
|
@@ -52,6 +193,36 @@ function copies(t, plural) {
|
|
|
52
193
|
delete: shape.delete,
|
|
53
194
|
};
|
|
54
195
|
}
|
|
196
|
+
/** The copy `wanted` names, else a serving copy of the live version, else any
|
|
197
|
+
* serving copy (a deploy may be switching). Refuses with what to do rather
|
|
198
|
+
* than wake a frozen copy, which the App's own traffic does. */
|
|
199
|
+
export function pickCopy(app, list, wanted) {
|
|
200
|
+
if (wanted) {
|
|
201
|
+
const found = list.find((c) => c.id === wanted);
|
|
202
|
+
if (found)
|
|
203
|
+
return found;
|
|
204
|
+
throw new RuntimeError({
|
|
205
|
+
message: `App ${app.id} has no copy ${wanted}.`,
|
|
206
|
+
code: "not_found",
|
|
207
|
+
status: 404,
|
|
208
|
+
hint: `List its copies with GET /v1/apps/${app.id}/copies (runtime apps copies ${app.id}).`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const serving = list.filter((c) => c.state === "serving");
|
|
212
|
+
const found = serving.find((c) => c.version === app.liveVersion) ?? serving[0];
|
|
213
|
+
if (found)
|
|
214
|
+
return found;
|
|
215
|
+
throw new RuntimeError({
|
|
216
|
+
message: list.length
|
|
217
|
+
? `No copy of App ${app.id} is serving: ${list.map((c) => `${c.id} ${c.state}`).join(", ")}.`
|
|
218
|
+
: `App ${app.id} has no copies.`,
|
|
219
|
+
code: "invalid_state",
|
|
220
|
+
status: 409,
|
|
221
|
+
hint: app.liveVersion === null
|
|
222
|
+
? "Deploy a version first."
|
|
223
|
+
: "Send a request to the App's URL to wake one, or keep one awake with minCopies: 1.",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
55
226
|
export function apps(t) {
|
|
56
227
|
return copies(t, "apps");
|
|
57
228
|
}
|