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

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 });
@@ -20343,6 +20340,7 @@ var restActionRoutes = {
20343
20340
  "shell.exec": { method: "POST", path: "/runtime/executions" },
20344
20341
  "fs.readFile": { method: "GET", path: "/runtime/files/content" },
20345
20342
  "fs.writeFile": { method: "PUT", path: "/runtime/files/content" },
20343
+ "fs.download": { method: "POST", path: "/runtime/files/download" },
20346
20344
  "fs.createDirectory": { method: "PUT", path: "/runtime/directories" },
20347
20345
  "fs.readDirectory": { method: "POST", path: "/runtime/directories/read" },
20348
20346
  "fs.stat": { method: "POST", path: "/runtime/files/stat" },
@@ -20379,6 +20377,7 @@ var RestRuntimeInvoker = class {
20379
20377
  this.fetch = fetch;
20380
20378
  this.refresh = refresh;
20381
20379
  this.refreshPromise = null;
20380
+ this.streams = /* @__PURE__ */ new Set();
20382
20381
  this.notebookId = data.id;
20383
20382
  this.configure(data.runtimeUrl);
20384
20383
  }
@@ -20411,6 +20410,127 @@ var RestRuntimeInvoker = class {
20411
20410
  send() {
20412
20411
  throw new PHPSandboxError("Fire-and-forget actions require the realtime transport.");
20413
20412
  }
20413
+ dispose() {
20414
+ for (const stream of this.streams) {
20415
+ stream.abort(new DOMException("Notebook disposed", "AbortError"));
20416
+ }
20417
+ }
20418
+ stream(action, data, options = {}) {
20419
+ const cancellation = new AbortController();
20420
+ this.streams.add(cancellation);
20421
+ const abort = runtimeAbort({ ...options, abortSignal: cancellation.signal });
20422
+ const abortFromCaller = () => cancellation.abort(options.abortSignal?.reason);
20423
+ let reader;
20424
+ let closed = false;
20425
+ let onAbort = () => {
20426
+ };
20427
+ const dispose = () => {
20428
+ this.streams.delete(cancellation);
20429
+ options.abortSignal?.removeEventListener("abort", abortFromCaller);
20430
+ abort.signal.removeEventListener("abort", onAbort);
20431
+ abort.dispose();
20432
+ };
20433
+ const cancelReader = async (reason) => {
20434
+ const activeReader = reader;
20435
+ reader = void 0;
20436
+ if (activeReader !== void 0) {
20437
+ await activeReader.cancel(reason).catch(() => void 0);
20438
+ activeReader.releaseLock();
20439
+ }
20440
+ };
20441
+ return new ReadableStream(
20442
+ {
20443
+ start: (controller) => {
20444
+ onAbort = () => {
20445
+ if (closed) {
20446
+ return;
20447
+ }
20448
+ closed = true;
20449
+ dispose();
20450
+ controller.error(abort.signal.reason);
20451
+ void cancelReader(abort.signal.reason);
20452
+ };
20453
+ abort.signal.addEventListener("abort", onAbort, { once: true });
20454
+ options.abortSignal?.addEventListener("abort", abortFromCaller, { once: true });
20455
+ if (options.abortSignal?.aborted) {
20456
+ abortFromCaller();
20457
+ }
20458
+ },
20459
+ pull: async (controller) => {
20460
+ try {
20461
+ if (reader === void 0) {
20462
+ const response = await this.streamResponse(action, data, abort.signal);
20463
+ if (closed) {
20464
+ await response.body?.cancel();
20465
+ return;
20466
+ }
20467
+ if (response.body === null) {
20468
+ throw new TransportError("PHPSandbox runtime returned an empty download.", "InvalidResponse", response);
20469
+ }
20470
+ reader = response.body.getReader();
20471
+ }
20472
+ const result = await reader.read();
20473
+ if (closed) {
20474
+ return;
20475
+ }
20476
+ if (result.done) {
20477
+ closed = true;
20478
+ dispose();
20479
+ reader.releaseLock();
20480
+ reader = void 0;
20481
+ controller.close();
20482
+ } else {
20483
+ controller.enqueue(result.value);
20484
+ }
20485
+ } catch (error51) {
20486
+ if (!closed) {
20487
+ closed = true;
20488
+ cancellation.abort(error51);
20489
+ dispose();
20490
+ await cancelReader(error51);
20491
+ controller.error(error51);
20492
+ }
20493
+ }
20494
+ },
20495
+ cancel: async (reason) => {
20496
+ closed = true;
20497
+ cancellation.abort(reason);
20498
+ dispose();
20499
+ await cancelReader(reason);
20500
+ }
20501
+ },
20502
+ { highWaterMark: 0 }
20503
+ );
20504
+ }
20505
+ async streamResponse(action, data, signal) {
20506
+ for (let attempt = 0; attempt < 2; attempt += 1) {
20507
+ signal.throwIfAborted();
20508
+ const response = await runtimeFetch(this.fetch, this.request(action, data, signal));
20509
+ if (signal.aborted) {
20510
+ await response.body?.cancel();
20511
+ signal.throwIfAborted();
20512
+ }
20513
+ if (response.status === 401 && attempt === 0 && this.refresh !== void 0) {
20514
+ await response.body?.cancel();
20515
+ await this.refreshAuthorization();
20516
+ continue;
20517
+ }
20518
+ if (!response.ok) {
20519
+ const payload = await runtimeActionResponse(response);
20520
+ throw remoteError("runtime", payload.error, response);
20521
+ }
20522
+ if (!response.headers.get("content-type")?.toLowerCase().startsWith("application/zip")) {
20523
+ await response.body?.cancel();
20524
+ throw new TransportError("PHPSandbox runtime returned an invalid download.", "InvalidResponse", response);
20525
+ }
20526
+ return response;
20527
+ }
20528
+ throw new RemoteError("Notebook download could not be authorized.", {
20529
+ source: "runtime",
20530
+ status: 401,
20531
+ code: "AuthenticationRequired"
20532
+ });
20533
+ }
20414
20534
  async invokeRaw(action, data, options) {
20415
20535
  const abort = runtimeAbort(options);
20416
20536
  try {
@@ -20443,7 +20563,7 @@ var RestRuntimeInvoker = class {
20443
20563
  const url2 = new URL(this.actionUrl);
20444
20564
  url2.pathname += route.path;
20445
20565
  const headers = {
20446
- Accept: "application/json, application/octet-stream",
20566
+ Accept: action === "fs.download" ? "application/zip" : "application/json, application/octet-stream",
20447
20567
  Authorization: `Bearer ${this.ticket}`
20448
20568
  };
20449
20569
  const body = runtimeActionRequestBody(action, data, url2, headers);
@@ -20948,6 +21068,7 @@ var _NotebookInstance = class _NotebookInstance {
20948
21068
  this.data = data;
20949
21069
  this.client = client;
20950
21070
  __privateAdd(this, _NotebookInstance_instances);
21071
+ this.downloadInvoker = null;
20951
21072
  this.initialized = false;
20952
21073
  __privateAdd(this, _initPromise, null);
20953
21074
  this.disposables = [];
@@ -21069,12 +21190,23 @@ var _NotebookInstance = class _NotebookInstance {
21069
21190
  }
21070
21191
  return this.realtimeSocket().send(action, data || {});
21071
21192
  }
21193
+ /** Binary downloads use HTTP independently of the selected runtime transport. */
21194
+ stream(action, data = {}, options = {}) {
21195
+ const runtimeUrlProvider = this.client.options.runtimeUrlProvider;
21196
+ this.downloadInvoker ?? (this.downloadInvoker = this.restInvoker ?? new RestRuntimeInvoker(
21197
+ this.data,
21198
+ this.client.options.fetch ?? globalThis.fetch,
21199
+ runtimeUrlProvider === void 0 ? void 0 : () => runtimeUrlProvider(this.data.id)
21200
+ ));
21201
+ return this.downloadInvoker.stream(action, data, options);
21202
+ }
21072
21203
  listen(event, handler) {
21073
21204
  const disposable = this.emitter.listen(event, handler);
21074
21205
  this.disposables.push(disposable);
21075
21206
  return disposable;
21076
21207
  }
21077
21208
  dispose() {
21209
+ this.downloadInvoker?.dispose();
21078
21210
  this.disposables.forEach((d) => d.dispose());
21079
21211
  if (this.socket !== null && !this.socket.isClosed) {
21080
21212
  this.socket.disconnect();