@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 +26 -0
- package/dist/browser/phpsandbox-sdk.esm.js +142 -10
- package/dist/browser/phpsandbox-sdk.esm.min.js +8 -8
- package/dist/browser/phpsandbox-sdk.iife.js +142 -10
- package/dist/browser/phpsandbox-sdk.iife.min.js +8 -8
- package/dist/filesystem.d.ts +2 -2
- package/dist/filesystem.js +4 -13
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -0
- package/dist/runtime/rest.d.ts +7 -0
- package/dist/runtime/rest.js +122 -1
- package/package.json +1 -1
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
|
-
|
|
16376
|
-
|
|
16377
|
-
|
|
16378
|
-
|
|
16379
|
-
|
|
16380
|
-
|
|
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();
|