withruntime 0.11.1 → 0.12.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 (176) hide show
  1. package/BLAXEL.md +3 -3
  2. package/COMPATIBILITY.md +74 -9
  3. package/DAYTONA.md +33 -5
  4. package/E2B.md +137 -48
  5. package/README.md +11 -9
  6. package/README.src.md +11 -9
  7. package/VERCEL.md +43 -5
  8. package/compatibility-lock.json +58 -49
  9. package/dist/billing-cli.d.ts +1 -1
  10. package/dist/billing-cli.js +77 -1
  11. package/dist/blaxel/errors.d.ts +14 -0
  12. package/dist/blaxel/errors.js +33 -4
  13. package/dist/blaxel/index.d.ts +2 -2
  14. package/dist/blaxel/index.js +4 -4
  15. package/dist/blaxel/interpreter.d.ts +2 -5
  16. package/dist/blaxel/sandbox.d.ts +15 -7
  17. package/dist/blaxel/sandbox.js +64 -9
  18. package/dist/blaxel/snapshot.d.ts +4 -0
  19. package/dist/blaxel/snapshot.js +4 -0
  20. package/dist/cli/logs.d.ts +21 -0
  21. package/dist/cli/logs.js +188 -0
  22. package/dist/cli/logs.test.d.ts +1 -0
  23. package/dist/cli/logs.test.js +272 -0
  24. package/dist/cli-extras.d.ts +3 -3
  25. package/dist/cli-extras.js +17 -12
  26. package/dist/cli-words.d.ts +10 -1
  27. package/dist/cli-words.js +18 -0
  28. package/dist/cli.d.ts +6 -2
  29. package/dist/cli.js +468 -219
  30. package/dist/client.d.ts +13 -63
  31. package/dist/client.js +35 -124
  32. package/dist/cloudflare/errors.d.ts +143 -0
  33. package/dist/cloudflare/errors.js +167 -0
  34. package/dist/cloudflare/index.d.ts +175 -6
  35. package/dist/cloudflare/index.js +416 -26
  36. package/dist/cloudflare/interpreter.d.ts +119 -0
  37. package/dist/cloudflare/interpreter.js +223 -0
  38. package/dist/cloudflare/streams.d.ts +38 -0
  39. package/dist/cloudflare/streams.js +145 -0
  40. package/dist/cloudflare/watch.d.ts +9 -0
  41. package/dist/cloudflare/watch.js +24 -0
  42. package/dist/codesandbox/index.d.ts +216 -28
  43. package/dist/codesandbox/index.js +478 -78
  44. package/dist/compat/core.d.ts +18 -0
  45. package/dist/compat/core.js +29 -0
  46. package/dist/compat/files.d.ts +1 -0
  47. package/dist/compat/files.js +25 -5
  48. package/dist/daytona/computer-use.d.ts +13 -4
  49. package/dist/daytona/computer-use.js +66 -8
  50. package/dist/daytona/daytona.d.ts +7 -2
  51. package/dist/daytona/daytona.js +31 -5
  52. package/dist/daytona/errors.d.ts +11 -0
  53. package/dist/daytona/errors.js +18 -0
  54. package/dist/daytona/filesystem.js +2 -1
  55. package/dist/daytona/image.d.ts +13 -9
  56. package/dist/daytona/image.js +18 -12
  57. package/dist/daytona/index.d.ts +1 -1
  58. package/dist/daytona/sandbox.d.ts +9 -3
  59. package/dist/daytona/sandbox.js +17 -3
  60. package/dist/domains/index.d.ts +9 -0
  61. package/dist/domains/index.js +10 -0
  62. package/dist/e2b/client.d.ts +15 -2
  63. package/dist/e2b/client.js +61 -7
  64. package/dist/e2b/commands.d.ts +4 -1
  65. package/dist/e2b/commands.js +17 -7
  66. package/dist/e2b/errors.d.ts +43 -2
  67. package/dist/e2b/errors.js +78 -6
  68. package/dist/e2b/filesystem.d.ts +1 -1
  69. package/dist/e2b/filesystem.js +82 -16
  70. package/dist/e2b/index.d.ts +3 -3
  71. package/dist/e2b/index.js +2 -2
  72. package/dist/e2b/sandbox.d.ts +50 -35
  73. package/dist/e2b/sandbox.js +228 -185
  74. package/dist/e2b/unsupported.d.ts +19 -0
  75. package/dist/e2b/unsupported.js +22 -1
  76. package/dist/e2b/users.d.ts +9 -0
  77. package/dist/e2b/users.js +22 -0
  78. package/dist/errors.d.ts +14 -5
  79. package/dist/errors.js +28 -14
  80. package/dist/freestyle/index.d.ts +285 -37
  81. package/dist/freestyle/index.js +656 -144
  82. package/dist/identity.d.ts +3 -0
  83. package/dist/identity.js +27 -0
  84. package/dist/images/index.d.ts +9 -0
  85. package/dist/images/index.js +10 -0
  86. package/dist/index.d.ts +1 -0
  87. package/dist/ips/index.d.ts +9 -0
  88. package/dist/ips/index.js +10 -0
  89. package/dist/jobs/index.d.ts +9 -0
  90. package/dist/jobs/index.js +10 -0
  91. package/dist/keys.js +2 -0
  92. package/dist/login.js +2 -0
  93. package/dist/machine.d.ts +189 -0
  94. package/dist/machine.js +521 -0
  95. package/dist/mcp.js +1 -1
  96. package/dist/modal/index.d.ts +339 -61
  97. package/dist/modal/index.js +781 -121
  98. package/dist/network-cli.d.ts +3 -1
  99. package/dist/network-cli.js +78 -36
  100. package/dist/networks/index.d.ts +11 -0
  101. package/dist/networks/index.js +10 -0
  102. package/dist/openai-agents/index.d.ts +1 -1
  103. package/dist/openai-agents/index.js +1 -1
  104. package/dist/ports/index.d.ts +9 -0
  105. package/dist/ports/index.js +10 -0
  106. package/dist/product-names.d.ts +52 -0
  107. package/dist/product-names.js +164 -0
  108. package/dist/product-renames.d.ts +103 -0
  109. package/dist/product-renames.js +189 -0
  110. package/dist/products/apps.d.ts +176 -0
  111. package/dist/products/apps.js +165 -0
  112. package/dist/products/billing.d.ts +41 -0
  113. package/dist/products/billing.js +13 -0
  114. package/dist/products/browser.d.ts +10 -0
  115. package/dist/products/browsers.d.ts +71 -0
  116. package/dist/products/browsers.js +102 -0
  117. package/dist/products/builds.d.ts +126 -0
  118. package/dist/products/builds.js +221 -0
  119. package/dist/products/index.d.ts +3 -16
  120. package/dist/products/index.js +3 -14
  121. package/dist/products/jobs.d.ts +19 -3
  122. package/dist/products/jobs.js +28 -12
  123. package/dist/products/limits.d.ts +4 -10
  124. package/dist/products/logs.d.ts +245 -0
  125. package/dist/products/logs.js +274 -0
  126. package/dist/products/logs.test.d.ts +1 -0
  127. package/dist/products/logs.test.js +343 -0
  128. package/dist/products/network-products.d.ts +38 -7
  129. package/dist/products/network-products.js +31 -18
  130. package/dist/products/network.d.ts +1 -1
  131. package/dist/products/observability.d.ts +3 -2
  132. package/dist/products/private-network.js +1 -1
  133. package/dist/products/queues.d.ts +149 -0
  134. package/dist/products/queues.js +213 -0
  135. package/dist/products/referrals.d.ts +21 -7
  136. package/dist/products/referrals.js +3 -3
  137. package/dist/products/secrets.d.ts +8 -4
  138. package/dist/products/secrets.js +39 -18
  139. package/dist/products/signed-requests.d.ts +21 -0
  140. package/dist/products/signed-requests.js +11 -0
  141. package/dist/products/streams.d.ts +73 -0
  142. package/dist/products/streams.js +17 -0
  143. package/dist/products/switching.d.ts +37 -10
  144. package/dist/products/switching.js +4 -3
  145. package/dist/products/volumes.d.ts +3 -0
  146. package/dist/record-cli.d.ts +23 -0
  147. package/dist/record-cli.js +308 -0
  148. package/dist/renamed-products.d.ts +38 -0
  149. package/dist/renamed-products.js +34 -0
  150. package/dist/runloop/index.d.ts +9 -2
  151. package/dist/runloop/index.js +29 -8
  152. package/dist/runloop/sdk.d.ts +131 -0
  153. package/dist/runloop/sdk.js +167 -0
  154. package/dist/sandbox.d.ts +10 -94
  155. package/dist/sandbox.js +22 -371
  156. package/dist/sandboxes/index.d.ts +11 -0
  157. package/dist/sandboxes/index.js +12 -0
  158. package/dist/snapshots/index.d.ts +9 -0
  159. package/dist/snapshots/index.js +11 -0
  160. package/dist/snapshots.d.ts +7 -4
  161. package/dist/sprites/index.d.ts +343 -31
  162. package/dist/sprites/index.js +866 -84
  163. package/dist/ssh.d.ts +1 -1
  164. package/dist/ssh.js +11 -11
  165. package/dist/terminal.d.ts +24 -0
  166. package/dist/terminal.js +32 -0
  167. package/dist/transport.d.ts +18 -2
  168. package/dist/transport.js +61 -6
  169. package/dist/tunnel.js +1 -1
  170. package/dist/types.d.ts +116 -8
  171. package/dist/vercel/sandbox.d.ts +1 -2
  172. package/dist/vercel/sandbox.js +6 -5
  173. package/dist/vercel/snapshot.js +10 -2
  174. package/dist/volumes/index.d.ts +9 -0
  175. package/dist/volumes/index.js +10 -0
  176. package/package.json +48 -2
@@ -0,0 +1,274 @@
1
+ import { RuntimeError, InvalidRequestError } from "../errors.js";
2
+ import { Page } from "../page.js";
3
+ const enc = encodeURIComponent;
4
+ const protocol = (message) => new RuntimeError({
5
+ code: "invalid_stream",
6
+ status: 422,
7
+ message,
8
+ hint: "Reconnect the log tail. If this repeats, report the request to support.",
9
+ });
10
+ /** Shared CRUD keeps the saved log and trace views on identical contracts. */
11
+ function resources(t, path) {
12
+ const list = async (query = {}, options) => {
13
+ const page = await t.json({
14
+ method: "GET",
15
+ path,
16
+ query,
17
+ ...options,
18
+ });
19
+ return new Page(page.data, page.nextCursor, (cursor) => list({ ...query, cursor }, options));
20
+ };
21
+ return {
22
+ create: (body, options) => t.json({ method: "POST", path, body, ...options }),
23
+ get: (id, options) => t.json({ method: "GET", path: `${path}/${enc(id)}`, ...options }),
24
+ list,
25
+ update: (id, body, options) => t.json({ method: "PUT", path: `${path}/${enc(id)}`, body, ...options }),
26
+ delete: (id, options) => t.json({ method: "DELETE", path: `${path}/${enc(id)}`, ...options }),
27
+ };
28
+ }
29
+ function ingest(t, path, body, options) {
30
+ if (new TextEncoder().encode(JSON.stringify(body)).length > 1_048_576)
31
+ throw new InvalidRequestError({
32
+ code: "payload_too_large",
33
+ status: 413,
34
+ message: "OTLP JSON exceeds 1 MiB.",
35
+ hint: "Split the write into smaller batches, each with its own idempotency key.",
36
+ });
37
+ return t.json({ method: "POST", path, body, ...options });
38
+ }
39
+ /** Parse SSE incrementally, including split UTF-8, CRLF and multi-line data.
40
+ * Incomplete events at disconnect are discarded and replayed from the cursor.
41
+ * A finite event bound prevents a broken peer from growing memory forever. */
42
+ async function* events(stream, signal) {
43
+ const reader = stream.getReader();
44
+ const decoder = new TextDecoder();
45
+ let buffer = "", id = "", event = "", data = [], size = 0;
46
+ const cancel = () => {
47
+ void reader.cancel(signal?.reason).catch(() => undefined);
48
+ };
49
+ if (signal?.aborted)
50
+ cancel();
51
+ else
52
+ signal?.addEventListener("abort", cancel, { once: true });
53
+ try {
54
+ for (;;) {
55
+ signal?.throwIfAborted();
56
+ const chunk = await reader.read().catch((cause) => {
57
+ signal?.throwIfAborted();
58
+ throw new RuntimeError({
59
+ code: "tail_connection_lost",
60
+ status: 503,
61
+ message: "The log stream disconnected.",
62
+ hint: "The SDK resumes from the last delivered cursor.",
63
+ cause,
64
+ });
65
+ });
66
+ signal?.throwIfAborted();
67
+ if (chunk.done)
68
+ break;
69
+ buffer += decoder.decode(chunk.value, { stream: true });
70
+ for (;;) {
71
+ const match = /\r\n|\r|\n/.exec(buffer);
72
+ if (!match || (match[0] === "\r" && match.index === buffer.length - 1))
73
+ break;
74
+ const line = buffer.slice(0, match.index);
75
+ buffer = buffer.slice(match.index + match[0].length);
76
+ size += line.length + 1;
77
+ if (size > 1_048_576)
78
+ throw protocol("A log event exceeds 1 MiB of text.");
79
+ if (!line) {
80
+ if (data.length)
81
+ yield { id, event, data: data.join("\n") };
82
+ id = "";
83
+ event = "";
84
+ data = [];
85
+ size = 0;
86
+ }
87
+ else if (!line.startsWith(":")) {
88
+ const colon = line.indexOf(":");
89
+ const field = colon < 0 ? line : line.slice(0, colon);
90
+ const value = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /, "");
91
+ if (field === "id" && !value.includes("\0"))
92
+ id = value;
93
+ if (field === "event")
94
+ event = value;
95
+ if (field === "data")
96
+ data.push(value);
97
+ }
98
+ }
99
+ if (buffer.length + size > 1_048_576)
100
+ throw protocol("A log event exceeds 1 MiB of text.");
101
+ }
102
+ }
103
+ finally {
104
+ signal?.removeEventListener("abort", cancel);
105
+ await reader.cancel().catch(() => undefined);
106
+ reader.releaseLock();
107
+ }
108
+ }
109
+ function pause(ms, signal) {
110
+ return new Promise((resolve, reject) => {
111
+ const done = () => {
112
+ signal?.removeEventListener("abort", abort);
113
+ resolve();
114
+ };
115
+ const timer = setTimeout(done, ms);
116
+ const abort = () => {
117
+ clearTimeout(timer);
118
+ signal?.removeEventListener("abort", abort);
119
+ reject(signal?.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError"));
120
+ };
121
+ if (signal?.aborted)
122
+ abort();
123
+ else
124
+ signal?.addEventListener("abort", abort, { once: true });
125
+ });
126
+ }
127
+ /** Hidden until Logs launches. */
128
+ export function logs(t) {
129
+ const search = (body, options) => t.json({ method: "POST", path: "/v1/logs:search", body, ...options });
130
+ return {
131
+ search,
132
+ async *pages(input, options) {
133
+ let cursor = input.cursor;
134
+ const seen = new Set();
135
+ if (cursor)
136
+ seen.add(cursor);
137
+ do {
138
+ const page = await search({ ...input, ...(cursor ? { cursor } : {}) }, options);
139
+ yield page;
140
+ cursor = page.next ?? undefined;
141
+ if (cursor && seen.has(cursor))
142
+ throw protocol("Search repeated a paging cursor.");
143
+ if (cursor)
144
+ seen.add(cursor);
145
+ } while (cursor);
146
+ },
147
+ /** The record's cursor, not timestamp or text, identifies a line. The
148
+ * server replays strictly after it. An echoed boundary is ignored; two
149
+ * identical lines at different record positions are both delivered. */
150
+ async *tail(query, options = {}) {
151
+ const { cursor: initial, start, reconnectMs = 250, ...request } = options;
152
+ let cursor = initial, failures = 0;
153
+ if (!Number.isFinite(reconnectMs) || reconnectMs < 0)
154
+ throw protocol("Reconnect delay must be a finite, nonnegative number.");
155
+ for (;;) {
156
+ request.signal?.throwIfAborted();
157
+ try {
158
+ const stream = await t.fileStream({
159
+ method: "GET",
160
+ path: "/v1/logs/tail",
161
+ query: { query, cursor, start },
162
+ accept: "text/event-stream",
163
+ ...request,
164
+ timeoutMs: request.timeoutMs ?? 0,
165
+ onResponse(response) {
166
+ if (!response.headers.get("content-type")?.toLowerCase().startsWith("text/event-stream"))
167
+ throw protocol("Log tail did not return text/event-stream.");
168
+ },
169
+ });
170
+ for await (const event of events(stream, request.signal)) {
171
+ let value;
172
+ try {
173
+ value = JSON.parse(event.data);
174
+ }
175
+ catch {
176
+ throw protocol("Log tail returned invalid JSON.");
177
+ }
178
+ if (event.event === "error") {
179
+ // A reader failure is retryable, but its resume id has not been
180
+ // delivered as a line. Keep the last acknowledged line cursor.
181
+ throw new RuntimeError({
182
+ code: "tail_interrupted",
183
+ status: 503,
184
+ message: "The log reader stopped.",
185
+ hint: "The SDK resumes from the last delivered cursor.",
186
+ });
187
+ }
188
+ if (!event.id)
189
+ throw protocol("Log tail event has no record cursor.");
190
+ if (event.id === cursor)
191
+ continue;
192
+ if (event.event === "start") {
193
+ // Where the server's tail begins: a reconnect before the first
194
+ // line resumes here, so nothing written meanwhile is missed.
195
+ cursor = event.id;
196
+ continue;
197
+ }
198
+ if (event.event === "gap") {
199
+ const gap = value;
200
+ const from = gap?.from ?? cursor;
201
+ const to = gap?.to ?? gap?.cursor;
202
+ if (typeof from !== "string" || typeof to !== "string" || to !== event.id)
203
+ throw protocol("Log tail gap has invalid cursors.");
204
+ cursor = event.id;
205
+ yield { gap: true, from, to };
206
+ }
207
+ else {
208
+ const line = value;
209
+ if (!line ||
210
+ typeof line.timeUnixNano !== "string" ||
211
+ typeof line.time !== "string" ||
212
+ !line.labels ||
213
+ !("body" in line))
214
+ throw protocol("Log tail returned an invalid line.");
215
+ cursor = event.id;
216
+ yield { ...line, cursor };
217
+ }
218
+ failures = 0;
219
+ }
220
+ }
221
+ catch (error) {
222
+ request.signal?.throwIfAborted();
223
+ if (error instanceof RuntimeError && !error.retryable)
224
+ throw error;
225
+ if (!(error instanceof RuntimeError) && !(error instanceof TypeError))
226
+ throw error;
227
+ }
228
+ await pause(Math.min(10_000, Math.max(0, reconnectMs) * 2 ** Math.min(failures++, 6)), request.signal);
229
+ }
230
+ },
231
+ query: (query, options) => t.json({ method: "GET", path: "/v1/logs/query", query, ...options }),
232
+ queryRange: (query, options) => t.json({ method: "GET", path: "/v1/logs/query_range", query, ...options }),
233
+ labels: (query = {}, options) => t.json({ method: "GET", path: "/v1/logs/labels", query, ...options }),
234
+ labelValues: (name, query = {}, options) => t.json({
235
+ method: "GET",
236
+ path: `/v1/logs/labels/${enc(name)}/values`,
237
+ query,
238
+ ...options,
239
+ }),
240
+ views: resources(t, "/v1/logs/views"),
241
+ ingest: (body, options) => ingest(t, "/v1/logs", body, options),
242
+ };
243
+ }
244
+ export function traces(t) {
245
+ return {
246
+ get: (id, options) => t.json({ method: "GET", path: `/v1/traces/${enc(id)}`, ...options }),
247
+ search: (q, options) => t.json({
248
+ method: "GET",
249
+ path: "/v1/traces:search",
250
+ query: { q },
251
+ ...options,
252
+ }),
253
+ tags: (options) => t.json({ method: "GET", path: "/v1/traces/tags", ...options }),
254
+ views: resources(t, "/v1/traces/views"),
255
+ ingest: (body, options) => ingest(t, "/v1/traces", body, options),
256
+ };
257
+ }
258
+ export function alerts(t) {
259
+ return {
260
+ ...resources(t, "/v1/alerts"),
261
+ test: (id, options) => t.json({
262
+ method: "POST",
263
+ path: `/v1/alerts/${enc(id)}/test`,
264
+ body: {},
265
+ ...options,
266
+ }),
267
+ silence: (id, input, options) => t.json({
268
+ method: "POST",
269
+ path: `/v1/alerts/${enc(id)}/silence`,
270
+ body: input,
271
+ ...options,
272
+ }),
273
+ };
274
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,343 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Transport } from "../transport.js";
3
+ import { InvalidRequestError, NotFoundError, RuntimeError } from "../errors.js";
4
+ import { alerts, logs, traces, } from "./logs.js";
5
+ const line = {
6
+ time: "2026-10-06T12:00:00.000000001Z",
7
+ timeUnixNano: "1791288000000000001",
8
+ severity: 9,
9
+ severityText: "INFO",
10
+ body: "same α",
11
+ labels: { app: "api" },
12
+ attributes: {},
13
+ traceId: null,
14
+ spanId: null,
15
+ };
16
+ const view = { name: "errors", labels: {}, query: '{app="api"}' };
17
+ const rule = { ...view, condition: "above 5", for: "5m", actions: [] };
18
+ const options = { idempotencyKey: "repeat-this-write" };
19
+ function fixture(handler) {
20
+ const requests = [];
21
+ const transport = new Transport({
22
+ apiKey: "test-only",
23
+ baseUrl: "https://fake.test",
24
+ maxRetries: 0,
25
+ fetch: (async (input, init) => {
26
+ const url = new URL(input instanceof Request ? input.url : input);
27
+ requests.push({ url, init: init ?? {} });
28
+ return handler(url, init ?? {});
29
+ }),
30
+ });
31
+ return {
32
+ client: { logs: logs(transport), traces: traces(transport), alerts: alerts(transport) },
33
+ requests,
34
+ };
35
+ }
36
+ const calls = [
37
+ {
38
+ name: "search",
39
+ method: "POST",
40
+ path: "/v1/logs:search",
41
+ body: { query: "{}", cursor: "c", limit: 2 },
42
+ run: (c) => c.logs.search({ query: "{}", cursor: "c", limit: 2 }, options),
43
+ },
44
+ {
45
+ name: "instant query",
46
+ method: "GET",
47
+ path: "/v1/logs/query",
48
+ query: { query: "{}", time: "42", direction: "forward", limit: "2" },
49
+ run: (c) => c.logs.query({ query: "{}", time: "42", direction: "forward", limit: 2 }),
50
+ },
51
+ {
52
+ name: "range query",
53
+ method: "GET",
54
+ path: "/v1/logs/query_range",
55
+ query: { query: "{}", start: "1", end: "2", step: "1s" },
56
+ run: (c) => c.logs.queryRange({ query: "{}", start: "1", end: "2", step: "1s" }),
57
+ },
58
+ {
59
+ name: "labels",
60
+ method: "GET",
61
+ path: "/v1/logs/labels",
62
+ query: { start: "1", end: "2" },
63
+ run: (c) => c.logs.labels({ start: "1", end: "2" }),
64
+ },
65
+ {
66
+ name: "label values",
67
+ method: "GET",
68
+ path: "/v1/logs/labels/a%2Fb/values",
69
+ run: (c) => c.logs.labelValues("a/b"),
70
+ },
71
+ { name: "trace get", method: "GET", path: "/v1/traces/a%2Fb", run: (c) => c.traces.get("a/b") },
72
+ {
73
+ name: "trace search",
74
+ method: "GET",
75
+ path: "/v1/traces:search",
76
+ query: { q: "{duration > 1s}" },
77
+ run: (c) => c.traces.search("{duration > 1s}"),
78
+ },
79
+ { name: "trace tags", method: "GET", path: "/v1/traces/tags", run: (c) => c.traces.tags() },
80
+ {
81
+ name: "log ingest",
82
+ method: "POST",
83
+ path: "/v1/logs",
84
+ body: { resourceLogs: [] },
85
+ run: (c) => c.logs.ingest({ resourceLogs: [] }, options),
86
+ },
87
+ {
88
+ name: "trace ingest",
89
+ method: "POST",
90
+ path: "/v1/traces",
91
+ body: { resourceSpans: [] },
92
+ run: (c) => c.traces.ingest({ resourceSpans: [] }, options),
93
+ },
94
+ {
95
+ name: "test rule",
96
+ method: "POST",
97
+ path: "/v1/alerts/a%2Fb/test",
98
+ body: {},
99
+ run: (c) => c.alerts.test("a/b", options),
100
+ },
101
+ {
102
+ name: "silence rule",
103
+ method: "POST",
104
+ path: "/v1/alerts/a%2Fb/silence",
105
+ body: { until: "2026-10-07T00:00:00Z" },
106
+ run: (c) => c.alerts.silence("a/b", { until: "2026-10-07T00:00:00Z" }, options),
107
+ },
108
+ ];
109
+ for (const product of ["logs", "traces", "alerts"]) {
110
+ const path = `/v1/${product}${product === "alerts" ? "" : "/views"}`;
111
+ const resource = (c) => (product === "alerts" ? c.alerts : c[product].views);
112
+ const input = product === "alerts" ? rule : view;
113
+ calls.push({
114
+ name: `${product} create`,
115
+ method: "POST",
116
+ path,
117
+ body: input,
118
+ run: (c) => resource(c).create(input, options),
119
+ }, {
120
+ name: `${product} get`,
121
+ method: "GET",
122
+ path: `${path}/a%2Fb`,
123
+ run: (c) => resource(c).get("a/b"),
124
+ }, {
125
+ name: `${product} list`,
126
+ method: "GET",
127
+ path,
128
+ query: { limit: "2", cursor: "next" },
129
+ run: (c) => resource(c).list({ limit: 2, cursor: "next" }),
130
+ }, {
131
+ name: `${product} update`,
132
+ method: "PUT",
133
+ path: `${path}/a%2Fb`,
134
+ body: { name: "changed" },
135
+ run: (c) => resource(c).update("a/b", { name: "changed" }, options),
136
+ }, {
137
+ name: `${product} delete`,
138
+ method: "DELETE",
139
+ path: `${path}/a%2Fb`,
140
+ run: (c) => resource(c).delete("a/b", options),
141
+ });
142
+ }
143
+ for (const entry of calls)
144
+ describe(entry.name, () => {
145
+ test("request, encoded path and idempotency", async () => {
146
+ const { client, requests } = fixture(() => entry.method === "DELETE"
147
+ ? new Response(null, { status: 204 })
148
+ : Response.json({ data: [], nextCursor: null, marker: "answer" }));
149
+ const answer = await entry.run(client);
150
+ if (entry.method === "DELETE")
151
+ expect(answer).toBeNull();
152
+ else if (!entry.name.endsWith(" list"))
153
+ expect(answer).toEqual({ data: [], nextCursor: null, marker: "answer" });
154
+ expect(requests).toHaveLength(1);
155
+ const request = requests[0];
156
+ expect(request.url.pathname).toBe(entry.path);
157
+ expect(request.init.method).toBe(entry.method);
158
+ expect(Object.fromEntries(request.url.searchParams)).toEqual(entry.query ?? {});
159
+ if (entry.body !== undefined)
160
+ expect(JSON.parse(request.init.body)).toEqual(entry.body);
161
+ if (entry.method !== "GET")
162
+ expect(new Headers(request.init.headers).get("idempotency-key")).toBe(options.idempotencyKey);
163
+ });
164
+ test("typed server error preserves code and hint", async () => {
165
+ const { client } = fixture(() => Response.json({ error: { code: "not_found", message: "Missing", hint: "List resources first." } }, { status: 404 }));
166
+ try {
167
+ await entry.run(client);
168
+ throw new Error("Accepted failure");
169
+ }
170
+ catch (error) {
171
+ expect(error).toBeInstanceOf(NotFoundError);
172
+ expect(error).toMatchObject({ code: "not_found", hint: "List resources first." });
173
+ }
174
+ });
175
+ });
176
+ test("search paging follows opaque cursors and preserves equal timestamp lines", async () => {
177
+ const { client, requests } = fixture((_url, init) => Response.json({
178
+ lines: [line],
179
+ next: JSON.parse(init.body).cursor ? null : "opaque/+",
180
+ stats: {},
181
+ }));
182
+ const pages = [];
183
+ for await (const page of client.logs.pages({ query: "{}", limit: 1 }))
184
+ pages.push(page);
185
+ expect(pages.flatMap((page) => page.lines)).toEqual([line, line]);
186
+ expect(JSON.parse(requests[1].init.body).cursor).toBe("opaque/+");
187
+ });
188
+ test("search detects a cursor loop", async () => {
189
+ const { client } = fixture(() => Response.json({ lines: [], next: "same" }));
190
+ const iterator = client.logs.pages({ query: "{}", cursor: "same" });
191
+ await iterator.next();
192
+ await expect(iterator.next()).rejects.toMatchObject({ code: "invalid_stream" });
193
+ });
194
+ test("resource lists can follow pages", async () => {
195
+ const { client } = fixture((url) => Response.json({
196
+ data: [{ id: url.searchParams.get("cursor") ?? "first" }],
197
+ nextCursor: url.searchParams.has("cursor") ? null : "second",
198
+ }));
199
+ expect(await (await client.logs.views.list()).toArray()).toEqual([
200
+ { id: "first" },
201
+ { id: "second" },
202
+ ]);
203
+ });
204
+ test("UTF-8 bytes enforce the 1 MiB ingest bound before fetch", () => {
205
+ const { client, requests } = fixture(() => {
206
+ throw new Error("No fetch allowed");
207
+ });
208
+ expect(() => client.logs.ingest({ resourceLogs: [{ body: "α".repeat(530_000) }] })).toThrow(InvalidRequestError);
209
+ expect(() => client.traces.ingest({ resourceSpans: [{ body: "α".repeat(530_000) }] })).toThrow(InvalidRequestError);
210
+ expect(requests).toHaveLength(0);
211
+ });
212
+ const event = (id, value = line, kind = "") => `id: ${id}\r\n${kind ? `event: ${kind}\r\n` : ""}data: ${JSON.stringify(value)}\r\n\r\n`;
213
+ function sse(text, bytes = 7) {
214
+ const encoded = new TextEncoder().encode(text);
215
+ let at = 0;
216
+ return new Response(new ReadableStream({
217
+ pull(controller) {
218
+ if (at === encoded.length)
219
+ controller.close();
220
+ else {
221
+ controller.enqueue(encoded.slice(at, at + bytes));
222
+ at = Math.min(at + bytes, encoded.length);
223
+ }
224
+ },
225
+ }), { headers: { "content-type": "text/event-stream" } });
226
+ }
227
+ test("SSE reconnects by cursor, discards torn events, keeps identical records and decodes split UTF-8", async () => {
228
+ let count = 0;
229
+ const { client, requests } = fixture(() => ++count === 1
230
+ ? sse(`: heartbeat\r\n\r\n${event("one")}id: torn\ndata: {`)
231
+ : sse(event("one") + event("two")));
232
+ const iterator = client.logs.tail("{}", { reconnectMs: 0 });
233
+ expect((await iterator.next()).value).toEqual({ ...line, cursor: "one" });
234
+ expect((await iterator.next()).value).toEqual({ ...line, cursor: "two" });
235
+ expect(requests[1].url.searchParams.get("cursor")).toBe("one");
236
+ expect(new Headers(requests[0].init.headers).get("accept")).toBe("text/event-stream");
237
+ await iterator.return(undefined);
238
+ });
239
+ test("a drop before the first line resumes from the start event's cursor", async () => {
240
+ let count = 0;
241
+ const { client, requests } = fixture(() => ++count === 1
242
+ ? sse(event("began", { cursor: "began" }, "start"))
243
+ : sse(event("began", { cursor: "began" }, "start") + event("first")));
244
+ const iterator = client.logs.tail("{}", { reconnectMs: 0 });
245
+ expect((await iterator.next()).value).toEqual({ ...line, cursor: "first" });
246
+ expect(requests[0].url.searchParams.get("cursor")).toBeNull();
247
+ expect(requests[1].url.searchParams.get("cursor")).toBe("began");
248
+ await iterator.return(undefined);
249
+ });
250
+ test("server retention gap becomes typed from/to and resumes after it", async () => {
251
+ const { client } = fixture(() => sse(event("kept", { cursor: "kept" }, "gap") + event("later")));
252
+ const iterator = client.logs.tail("{}", { cursor: "expired", reconnectMs: 0 });
253
+ expect((await iterator.next()).value).toEqual({ gap: true, from: "expired", to: "kept" });
254
+ expect((await iterator.next()).value).toMatchObject({ cursor: "later" });
255
+ await iterator.return(undefined);
256
+ });
257
+ test("reader error, including an echoed cursor, reconnects without moving the cursor", async () => {
258
+ let calls = 0;
259
+ const { client, requests } = fixture(() => sse(++calls === 1 ? event("old", { message: "reader gone" }, "error") : event("new")));
260
+ const iterator = client.logs.tail("{}", { cursor: "old", reconnectMs: 0 });
261
+ expect((await iterator.next()).value).toMatchObject({ cursor: "new" });
262
+ expect(requests[1].url.searchParams.get("cursor")).toBe("old");
263
+ await iterator.return(undefined);
264
+ });
265
+ for (const [name, wire] of [
266
+ ["JSON", "id: a\ndata: nope\n\n"],
267
+ ["cursor", `data: ${JSON.stringify(line)}\n\n`],
268
+ ["line", event("a", {})],
269
+ ["gap", event("a", { cursor: "wrong" }, "gap")],
270
+ ["oversized event", `id: a\ndata: ${"x".repeat(1_048_577)}\n\n`],
271
+ ])
272
+ test(`invalid ${name} fails once with a code and hint`, async () => {
273
+ const { client, requests } = fixture(() => sse(wire, wire.length));
274
+ await expect(client.logs.tail("{}", { cursor: "before", reconnectMs: 0 }).next()).rejects.toMatchObject({ code: "invalid_stream", hint: expect.any(String) });
275
+ expect(requests).toHaveLength(1);
276
+ });
277
+ test("tail authentication errors do not reconnect", async () => {
278
+ const { client, requests } = fixture(() => Response.json({ error: { code: "unauthorized", hint: "Sign in." } }, { status: 401 }));
279
+ await expect(client.logs.tail("{}").next()).rejects.toMatchObject({
280
+ code: "unauthorized",
281
+ hint: "Sign in.",
282
+ });
283
+ expect(requests).toHaveLength(1);
284
+ });
285
+ test("aborting an idle connection closes its reader", async () => {
286
+ let cancelled = false;
287
+ const { client } = fixture(() => new Response(new ReadableStream({
288
+ cancel() {
289
+ cancelled = true;
290
+ },
291
+ }), { headers: { "content-type": "text/event-stream" } }));
292
+ const abort = new AbortController();
293
+ const iterator = client.logs.tail("{}", { signal: abort.signal });
294
+ const pending = iterator.next();
295
+ await new Promise((resolve) => setTimeout(resolve, 10));
296
+ abort.abort(new Error("test stop"));
297
+ await expect(pending).rejects.toThrow("test stop");
298
+ expect(cancelled).toBe(true);
299
+ });
300
+ test("non-SSE success is refused", async () => {
301
+ const { client } = fixture(() => Response.json({}));
302
+ await expect(client.logs.tail("{}").next()).rejects.toBeInstanceOf(RuntimeError);
303
+ });
304
+ test("an abrupt read failure reconnects, rather than only handling clean EOF", async () => {
305
+ let attempt = 0;
306
+ const { client, requests } = fixture(() => {
307
+ if (++attempt > 1)
308
+ return sse(event("second"));
309
+ let pulled = false;
310
+ return new Response(new ReadableStream({
311
+ pull(controller) {
312
+ if (!pulled) {
313
+ pulled = true;
314
+ controller.enqueue(new TextEncoder().encode(event("first")));
315
+ }
316
+ else
317
+ controller.error(new Error("connection reset"));
318
+ },
319
+ }), { headers: { "content-type": "text/event-stream" } });
320
+ });
321
+ const iterator = client.logs.tail("{}", { reconnectMs: 0 });
322
+ expect((await iterator.next()).value).toMatchObject({ cursor: "first" });
323
+ expect((await iterator.next()).value).toMatchObject({ cursor: "second" });
324
+ expect(requests[1].url.searchParams.get("cursor")).toBe("first");
325
+ await iterator.return(undefined);
326
+ });
327
+ test("a multline SSE event and the agreed from/to gap shape are accepted", async () => {
328
+ const { client } = fixture(() => sse('event: gap\nid: kept\ndata: {"from":"old",\ndata: "to":"kept"}\n\n'));
329
+ const iterator = client.logs.tail("{}", { cursor: "old" });
330
+ expect((await iterator.next()).value).toEqual({ gap: true, from: "old", to: "kept" });
331
+ await iterator.return(undefined);
332
+ });
333
+ test("abort during reconnect delay stops without another request", async () => {
334
+ const abort = new AbortController();
335
+ const { client, requests } = fixture(() => sse(event("first")));
336
+ const iterator = client.logs.tail("{}", { signal: abort.signal, reconnectMs: 1000 });
337
+ await iterator.next();
338
+ const pending = iterator.next();
339
+ await new Promise((resolve) => setTimeout(resolve, 10));
340
+ abort.abort(new Error("stop reconnect"));
341
+ await expect(pending).rejects.toThrow("stop reconnect");
342
+ expect(requests).toHaveLength(1);
343
+ });