@phpsandbox/sdk 0.6.0-canary.3 → 0.6.0-canary.5

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/README.md CHANGED
@@ -209,6 +209,32 @@ const [hasMore, matches] = await notebook.files.search(
209
209
  );
210
210
  ```
211
211
 
212
+ `files.download()` returns a `ReadableStream<Uint8Array>` containing the notebook ZIP.
213
+ Read it incrementally or pipe it to a writable stream:
214
+
215
+ ```ts
216
+ const controller = new AbortController();
217
+ const archive = notebook.files.download({
218
+ include: ['src/**'],
219
+ exclude: ['vendor/**'],
220
+ signal: controller.signal,
221
+ });
222
+
223
+ for await (const chunk of archive) {
224
+ await consume(chunk);
225
+ }
226
+ ```
227
+
228
+ Downloads use HTTP for both realtime and REST clients and require the runtime's
229
+ `POST /api/v1/notebooks/{notebook}/runtime/files/download` endpoint. Reading starts
230
+ the request; cancelling the stream, aborting its signal, or disposing the notebook
231
+ stops the transfer. Failures reject stream reads and are not retried after bytes
232
+ have started arriving.
233
+
234
+ **Breaking change in 0.6.0-canary.4:** downloads no longer return a `Blob` or accept
235
+ `onChunk`. If you need a Blob, explicitly collect the archive with
236
+ `await new Response(notebook.files.download()).blob()`; this buffers the ZIP in memory.
237
+
212
238
  ### Terminal
213
239
 
214
240
  ```ts
@@ -16372,17 +16372,14 @@ var Filesystem = class {
16372
16372
  exists(path) {
16373
16373
  return this.stat(path).then(() => true).catch(() => false);
16374
16374
  }
16375
- async download(options = {}) {
16376
- const id = nanoid3();
16377
- const stream = !!options.onChunk;
16378
- const chunks = [];
16379
- const disposable = this.okra.listen(
16380
- `fs.download.${id}`,
16381
- stream ? options.onChunk : (data) => {
16382
- chunks.push(data);
16375
+ download(options = {}) {
16376
+ return this.okra.stream(
16377
+ "fs.download",
16378
+ { include: options.include, exclude: options.exclude },
16379
+ {
16380
+ abortSignal: options.signal
16383
16381
  }
16384
16382
  );
16385
- return this.okra.invoke("fs.download", { id, exclude: options.exclude, include: options.include }).then(() => new Blob(chunks, { type: "application/octet-stream" })).finally(() => disposable.dispose());
16386
16383
  }
16387
16384
  tree(path = "/") {
16388
16385
  return this.okra.invoke("fs.tree", { path });
@@ -20254,8 +20251,12 @@ var ServerInstance = class {
20254
20251
  this.data = data;
20255
20252
  this.client = client;
20256
20253
  }
20257
- async installation() {
20258
- return (await this.client.get(`/servers/${encodeURIComponent(this.data.id)}/installation`)).data;
20254
+ async updateAgent(input) {
20255
+ return (await this.client.post(`/servers/${encodeURIComponent(this.data.id)}/agent/updates`, input)).data;
20256
+ }
20257
+ async installation(options = {}) {
20258
+ const query = options.managedUpdates ? "?managedUpdates=1" : "";
20259
+ return (await this.client.get(`/servers/${encodeURIComponent(this.data.id)}/installation${query}`)).data;
20259
20260
  }
20260
20261
  async members(options = {}) {
20261
20262
  const query = options.page === void 0 ? "" : `?${new URLSearchParams({ page: String(options.page) }).toString()}`;
@@ -20343,6 +20344,7 @@ var restActionRoutes = {
20343
20344
  "shell.exec": { method: "POST", path: "/runtime/executions" },
20344
20345
  "fs.readFile": { method: "GET", path: "/runtime/files/content" },
20345
20346
  "fs.writeFile": { method: "PUT", path: "/runtime/files/content" },
20347
+ "fs.download": { method: "POST", path: "/runtime/files/download" },
20346
20348
  "fs.createDirectory": { method: "PUT", path: "/runtime/directories" },
20347
20349
  "fs.readDirectory": { method: "POST", path: "/runtime/directories/read" },
20348
20350
  "fs.stat": { method: "POST", path: "/runtime/files/stat" },
@@ -20379,6 +20381,7 @@ var RestRuntimeInvoker = class {
20379
20381
  this.fetch = fetch;
20380
20382
  this.refresh = refresh;
20381
20383
  this.refreshPromise = null;
20384
+ this.streams = /* @__PURE__ */ new Set();
20382
20385
  this.notebookId = data.id;
20383
20386
  this.configure(data.runtimeUrl);
20384
20387
  }
@@ -20411,6 +20414,127 @@ var RestRuntimeInvoker = class {
20411
20414
  send() {
20412
20415
  throw new PHPSandboxError("Fire-and-forget actions require the realtime transport.");
20413
20416
  }
20417
+ dispose() {
20418
+ for (const stream of this.streams) {
20419
+ stream.abort(new DOMException("Notebook disposed", "AbortError"));
20420
+ }
20421
+ }
20422
+ stream(action, data, options = {}) {
20423
+ const cancellation = new AbortController();
20424
+ this.streams.add(cancellation);
20425
+ const abort = runtimeAbort({ ...options, abortSignal: cancellation.signal });
20426
+ const abortFromCaller = () => cancellation.abort(options.abortSignal?.reason);
20427
+ let reader;
20428
+ let closed = false;
20429
+ let onAbort = () => {
20430
+ };
20431
+ const dispose = () => {
20432
+ this.streams.delete(cancellation);
20433
+ options.abortSignal?.removeEventListener("abort", abortFromCaller);
20434
+ abort.signal.removeEventListener("abort", onAbort);
20435
+ abort.dispose();
20436
+ };
20437
+ const cancelReader = async (reason) => {
20438
+ const activeReader = reader;
20439
+ reader = void 0;
20440
+ if (activeReader !== void 0) {
20441
+ await activeReader.cancel(reason).catch(() => void 0);
20442
+ activeReader.releaseLock();
20443
+ }
20444
+ };
20445
+ return new ReadableStream(
20446
+ {
20447
+ start: (controller) => {
20448
+ onAbort = () => {
20449
+ if (closed) {
20450
+ return;
20451
+ }
20452
+ closed = true;
20453
+ dispose();
20454
+ controller.error(abort.signal.reason);
20455
+ void cancelReader(abort.signal.reason);
20456
+ };
20457
+ abort.signal.addEventListener("abort", onAbort, { once: true });
20458
+ options.abortSignal?.addEventListener("abort", abortFromCaller, { once: true });
20459
+ if (options.abortSignal?.aborted) {
20460
+ abortFromCaller();
20461
+ }
20462
+ },
20463
+ pull: async (controller) => {
20464
+ try {
20465
+ if (reader === void 0) {
20466
+ const response = await this.streamResponse(action, data, abort.signal);
20467
+ if (closed) {
20468
+ await response.body?.cancel();
20469
+ return;
20470
+ }
20471
+ if (response.body === null) {
20472
+ throw new TransportError("PHPSandbox runtime returned an empty download.", "InvalidResponse", response);
20473
+ }
20474
+ reader = response.body.getReader();
20475
+ }
20476
+ const result = await reader.read();
20477
+ if (closed) {
20478
+ return;
20479
+ }
20480
+ if (result.done) {
20481
+ closed = true;
20482
+ dispose();
20483
+ reader.releaseLock();
20484
+ reader = void 0;
20485
+ controller.close();
20486
+ } else {
20487
+ controller.enqueue(result.value);
20488
+ }
20489
+ } catch (error51) {
20490
+ if (!closed) {
20491
+ closed = true;
20492
+ cancellation.abort(error51);
20493
+ dispose();
20494
+ await cancelReader(error51);
20495
+ controller.error(error51);
20496
+ }
20497
+ }
20498
+ },
20499
+ cancel: async (reason) => {
20500
+ closed = true;
20501
+ cancellation.abort(reason);
20502
+ dispose();
20503
+ await cancelReader(reason);
20504
+ }
20505
+ },
20506
+ { highWaterMark: 0 }
20507
+ );
20508
+ }
20509
+ async streamResponse(action, data, signal) {
20510
+ for (let attempt = 0; attempt < 2; attempt += 1) {
20511
+ signal.throwIfAborted();
20512
+ const response = await runtimeFetch(this.fetch, this.request(action, data, signal));
20513
+ if (signal.aborted) {
20514
+ await response.body?.cancel();
20515
+ signal.throwIfAborted();
20516
+ }
20517
+ if (response.status === 401 && attempt === 0 && this.refresh !== void 0) {
20518
+ await response.body?.cancel();
20519
+ await this.refreshAuthorization();
20520
+ continue;
20521
+ }
20522
+ if (!response.ok) {
20523
+ const payload = await runtimeActionResponse(response);
20524
+ throw remoteError("runtime", payload.error, response);
20525
+ }
20526
+ if (!response.headers.get("content-type")?.toLowerCase().startsWith("application/zip")) {
20527
+ await response.body?.cancel();
20528
+ throw new TransportError("PHPSandbox runtime returned an invalid download.", "InvalidResponse", response);
20529
+ }
20530
+ return response;
20531
+ }
20532
+ throw new RemoteError("Notebook download could not be authorized.", {
20533
+ source: "runtime",
20534
+ status: 401,
20535
+ code: "AuthenticationRequired"
20536
+ });
20537
+ }
20414
20538
  async invokeRaw(action, data, options) {
20415
20539
  const abort = runtimeAbort(options);
20416
20540
  try {
@@ -20443,7 +20567,7 @@ var RestRuntimeInvoker = class {
20443
20567
  const url2 = new URL(this.actionUrl);
20444
20568
  url2.pathname += route.path;
20445
20569
  const headers = {
20446
- Accept: "application/json, application/octet-stream",
20570
+ Accept: action === "fs.download" ? "application/zip" : "application/json, application/octet-stream",
20447
20571
  Authorization: `Bearer ${this.ticket}`
20448
20572
  };
20449
20573
  const body = runtimeActionRequestBody(action, data, url2, headers);
@@ -20948,6 +21072,7 @@ var _NotebookInstance = class _NotebookInstance {
20948
21072
  this.data = data;
20949
21073
  this.client = client;
20950
21074
  __privateAdd(this, _NotebookInstance_instances);
21075
+ this.downloadInvoker = null;
20951
21076
  this.initialized = false;
20952
21077
  __privateAdd(this, _initPromise, null);
20953
21078
  this.disposables = [];
@@ -21069,12 +21194,23 @@ var _NotebookInstance = class _NotebookInstance {
21069
21194
  }
21070
21195
  return this.realtimeSocket().send(action, data || {});
21071
21196
  }
21197
+ /** Binary downloads use HTTP independently of the selected runtime transport. */
21198
+ stream(action, data = {}, options = {}) {
21199
+ const runtimeUrlProvider = this.client.options.runtimeUrlProvider;
21200
+ this.downloadInvoker ?? (this.downloadInvoker = this.restInvoker ?? new RestRuntimeInvoker(
21201
+ this.data,
21202
+ this.client.options.fetch ?? globalThis.fetch,
21203
+ runtimeUrlProvider === void 0 ? void 0 : () => runtimeUrlProvider(this.data.id)
21204
+ ));
21205
+ return this.downloadInvoker.stream(action, data, options);
21206
+ }
21072
21207
  listen(event, handler) {
21073
21208
  const disposable = this.emitter.listen(event, handler);
21074
21209
  this.disposables.push(disposable);
21075
21210
  return disposable;
21076
21211
  }
21077
21212
  dispose() {
21213
+ this.downloadInvoker?.dispose();
21078
21214
  this.disposables.forEach((d) => d.dispose());
21079
21215
  if (this.socket !== null && !this.socket.isClosed) {
21080
21216
  this.socket.disconnect();