withruntime 0.23.0 → 0.33.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.
Files changed (76) hide show
  1. package/COMPATIBILITY.md +64 -3
  2. package/E2B.md +1 -1
  3. package/README.md +36 -2
  4. package/README.src.md +35 -1
  5. package/compatibility-lock.json +3 -3
  6. package/dist/agents/browser.d.ts +12 -0
  7. package/dist/agents/browser.js +12 -0
  8. package/dist/agents/index.d.ts +7 -7
  9. package/dist/agents/index.js +7 -8
  10. package/dist/agents-cli.d.ts +1 -1
  11. package/dist/agents-cli.js +250 -3
  12. package/dist/agents-deploy.d.ts +186 -0
  13. package/dist/agents-deploy.js +752 -0
  14. package/dist/agents-file.d.ts +19 -0
  15. package/dist/agents-file.js +135 -0
  16. package/dist/apps/index.d.ts +10 -0
  17. package/dist/apps/index.js +11 -0
  18. package/dist/apps-cli.d.ts +3 -1
  19. package/dist/apps-cli.js +285 -38
  20. package/dist/blaxel/errors.js +2 -1
  21. package/dist/ci/index.d.ts +10 -0
  22. package/dist/ci/index.js +11 -0
  23. package/dist/ci-cli.d.ts +10 -0
  24. package/dist/ci-cli.js +343 -0
  25. package/dist/cli.d.ts +18 -7
  26. package/dist/cli.js +775 -85
  27. package/dist/daytona/errors.js +2 -1
  28. package/dist/e2b/errors.js +2 -1
  29. package/dist/e2b/filesystem.d.ts +1 -0
  30. package/dist/e2b/filesystem.js +1 -1
  31. package/dist/e2b/sandbox.d.ts +11 -2
  32. package/dist/e2b/sandbox.js +22 -6
  33. package/dist/errors.d.ts +11 -4
  34. package/dist/errors.js +27 -4
  35. package/dist/fly/index.d.ts +102 -0
  36. package/dist/fly/index.js +930 -0
  37. package/dist/index.d.ts +7 -3
  38. package/dist/machine.d.ts +10 -6
  39. package/dist/machine.js +14 -6
  40. package/dist/modal/index.d.ts +34 -0
  41. package/dist/modal/index.js +128 -22
  42. package/dist/products/agents.d.ts +101 -3
  43. package/dist/products/agents.js +49 -0
  44. package/dist/products/apps.d.ts +206 -10
  45. package/dist/products/apps.js +172 -1
  46. package/dist/products/ci.d.ts +332 -0
  47. package/dist/products/ci.js +101 -0
  48. package/dist/products/index.d.ts +8 -0
  49. package/dist/products/index.js +15 -0
  50. package/dist/products/jobs.d.ts +105 -5
  51. package/dist/products/jobs.js +47 -3
  52. package/dist/products/network.d.ts +13 -0
  53. package/dist/products/observability.d.ts +1 -1
  54. package/dist/products/plans.d.ts +131 -0
  55. package/dist/products/plans.js +24 -0
  56. package/dist/products/reservations.d.ts +108 -0
  57. package/dist/products/reservations.js +41 -0
  58. package/dist/products/resources.d.ts +2 -1
  59. package/dist/products/secrets.d.ts +90 -6
  60. package/dist/products/secrets.js +8 -2
  61. package/dist/proxy.d.ts +3 -3
  62. package/dist/proxy.js +20 -0
  63. package/dist/sandbox.d.ts +29 -1
  64. package/dist/sandbox.js +56 -3
  65. package/dist/snapshots.d.ts +1 -1
  66. package/dist/switched-off.d.ts +5 -0
  67. package/dist/switched-off.js +8 -0
  68. package/dist/tar.d.ts +1 -1
  69. package/dist/tar.js +19 -1
  70. package/dist/transport.d.ts +1 -1
  71. package/dist/transport.js +48 -9
  72. package/dist/types.d.ts +35 -4
  73. package/dist/vercel/errors.d.ts +1 -1
  74. package/dist/vercel/errors.js +2 -1
  75. package/dist/vercel/snapshot.js +4 -2
  76. package/package.json +23 -6
@@ -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,
@@ -1,8 +1,15 @@
1
- import type { RequestOptions, Transport } from "../transport.js";
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
- /** Private until launch. A deployment's completion follows its own record,
4
- * not the app's state. */
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
- export type App = AppSettings & {
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 RuntimeFunction = Omit<App, "minCopies"> & {
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. */
@@ -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({ method: "POST", path: `${base}/${enc(id)}/${verb}`, body, ...rest });
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
  }