@phpsandbox/sdk 0.0.65 → 0.0.66
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 +10 -5
- package/dist/auth.d.ts +3 -4
- package/dist/auth.js +7 -3
- package/dist/browser/phpsandbox-sdk.esm.js +366 -44
- package/dist/browser/phpsandbox-sdk.esm.min.js +24 -24
- package/dist/browser/phpsandbox-sdk.iife.js +366 -44
- package/dist/browser/phpsandbox-sdk.iife.min.js +24 -24
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.js +6 -0
- package/dist/filesystem.d.ts +2 -2
- package/dist/filesystem.js +7 -4
- package/dist/index.d.ts +10 -0
- package/dist/index.js +108 -34
- package/dist/runtime/rest.d.ts +20 -0
- package/dist/runtime/rest.js +226 -0
- package/dist/socket/index.d.ts +2 -1
- package/dist/socket/index.js +12 -3
- package/dist/types.d.ts +1 -1
- package/docs/api-summary.md +8 -2
- package/examples/README.md +2 -2
- package/examples/laravel-load-test.ts +8 -8
- package/package.json +1 -1
package/dist/errors/index.d.ts
CHANGED
package/dist/errors/index.js
CHANGED
|
@@ -16,3 +16,9 @@ export class NotebookUnavailableError extends Error {
|
|
|
16
16
|
this.name = 'NotebookUnavailableError';
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
export class UnsupportedRuntimeTransportError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'UnsupportedRuntimeTransportError';
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/filesystem.d.ts
CHANGED
|
@@ -451,8 +451,8 @@ declare class FilesystemSubscription {
|
|
|
451
451
|
export declare class Filesystem {
|
|
452
452
|
protected okra: NotebookInstance;
|
|
453
453
|
private watches;
|
|
454
|
-
constructor(okra: NotebookInstance);
|
|
455
|
-
private
|
|
454
|
+
constructor(okra: NotebookInstance, registerReconnect?: ((handler: () => void) => Disposable) | null);
|
|
455
|
+
private registerReconnectHandler;
|
|
456
456
|
info(path: string): Promise<FileInfo>;
|
|
457
457
|
write(path: string, contents: string): Promise<boolean>;
|
|
458
458
|
find(query: string, options?: Partial<FileSearchOptions>): Promise<FileResult[]>;
|
package/dist/filesystem.js
CHANGED
|
@@ -69,13 +69,16 @@ class FilesystemSubscription {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
export class Filesystem {
|
|
72
|
-
constructor(okra) {
|
|
72
|
+
constructor(okra, registerReconnect = (handler) => okra.onDidConnect(handler)) {
|
|
73
73
|
this.okra = okra;
|
|
74
74
|
this.watches = new Map();
|
|
75
|
-
this.
|
|
75
|
+
this.registerReconnectHandler(registerReconnect);
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
registerReconnectHandler(registerReconnect) {
|
|
78
|
+
if (registerReconnect === null) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
registerReconnect(() => {
|
|
79
82
|
for (const { path, options } of this.watches.values()) {
|
|
80
83
|
void this.okra.invoke('fs.watch', { path, options }).catch(() => { });
|
|
81
84
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -307,12 +307,16 @@ export interface PHPSandboxClientOptions {
|
|
|
307
307
|
telemetry?: Set<TelemetryFeature>;
|
|
308
308
|
fetch?: typeof globalThis.fetch;
|
|
309
309
|
webSocket?: unknown;
|
|
310
|
+
okraUrlProvider?: (notebookId: string) => Promise<string>;
|
|
310
311
|
}
|
|
312
|
+
export type RuntimeTransport = 'realtime' | 'rest';
|
|
311
313
|
export declare class Client {
|
|
312
314
|
readonly notebook: NotebookApi;
|
|
313
315
|
readonly deployments: DeploymentApi;
|
|
314
316
|
readonly servers: ServerApi;
|
|
315
317
|
readonly options: PHPSandboxClientOptions;
|
|
318
|
+
protected runtimeTransportValue: RuntimeTransport;
|
|
319
|
+
get runtimeTransport(): RuntimeTransport;
|
|
316
320
|
private readonly fetch;
|
|
317
321
|
private readonly baseUrl;
|
|
318
322
|
private readonly headers;
|
|
@@ -334,6 +338,8 @@ export declare class Client {
|
|
|
334
338
|
private makeRequest;
|
|
335
339
|
}
|
|
336
340
|
export declare class PHPSandbox extends Client {
|
|
341
|
+
static realtime(token: string, url?: string, options?: PHPSandboxClientOptions): PHPSandbox;
|
|
342
|
+
static rest(token: string, url?: string, options?: PHPSandboxClientOptions): PHPSandbox;
|
|
337
343
|
}
|
|
338
344
|
export declare class NotebookSecretApi {
|
|
339
345
|
private readonly client;
|
|
@@ -400,8 +406,10 @@ export declare class NotebookInstance {
|
|
|
400
406
|
readonly preview: NotebookPreview;
|
|
401
407
|
readonly mail: NotebookMail;
|
|
402
408
|
private readonly socket;
|
|
409
|
+
private readonly restInvoker;
|
|
403
410
|
readonly emitter: EventDispatcher;
|
|
404
411
|
initialized: NotebookInitResult | false;
|
|
412
|
+
get runtimeTransport(): RuntimeTransport;
|
|
405
413
|
private readonly disposables;
|
|
406
414
|
constructor(data: NotebookData, client: Client);
|
|
407
415
|
ready(): Promise<NotebookInitSuccessResult>;
|
|
@@ -425,10 +433,12 @@ export declare class NotebookInstance {
|
|
|
425
433
|
onDidConnect(handler: () => void): Disposable;
|
|
426
434
|
onDidDisconnect(handler: () => void): Disposable;
|
|
427
435
|
onDidBootError(handler: (error: NotebookUnavailableError) => void): Disposable;
|
|
436
|
+
private restInitialization;
|
|
428
437
|
update(input: Partial<UpdateNotebookInput>): Promise<NotebookInstance>;
|
|
429
438
|
onDidInitialize(handler: (result: NotebookInitResult) => void): Disposable;
|
|
430
439
|
reconnect(): Promise<NotebookInstance>;
|
|
431
440
|
beacon(iframe: HTMLIFrameElement, options?: BeaconOptions): Promise<Beacon>;
|
|
441
|
+
private realtimeSocket;
|
|
432
442
|
}
|
|
433
443
|
export declare class NotebookSecrets {
|
|
434
444
|
private readonly client;
|
package/dist/index.js
CHANGED
|
@@ -26,9 +26,10 @@ import { Transport } from './socket/index.js';
|
|
|
26
26
|
import EventManager from './events/index.js';
|
|
27
27
|
import Git from './git.js';
|
|
28
28
|
import { createBeacon } from './beacon/index.js';
|
|
29
|
-
import { NotebookUnavailableError } from './errors/index.js';
|
|
29
|
+
import { NotebookUnavailableError, UnsupportedRuntimeTransportError } from './errors/index.js';
|
|
30
30
|
import { DeploymentApi, NotebookDeploymentApi } from './deployments.js';
|
|
31
31
|
import { ServerApi } from './servers.js';
|
|
32
|
+
import { RestRuntimeInvoker } from './runtime/rest.js';
|
|
32
33
|
export * from './types.js';
|
|
33
34
|
export { Transport } from './socket/index.js';
|
|
34
35
|
export * from './lsp.js';
|
|
@@ -150,7 +151,11 @@ function normalizeApiBaseUrl(url) {
|
|
|
150
151
|
return baseUrl.toString();
|
|
151
152
|
}
|
|
152
153
|
export class Client {
|
|
154
|
+
get runtimeTransport() {
|
|
155
|
+
return this.runtimeTransportValue;
|
|
156
|
+
}
|
|
153
157
|
constructor(token, url = 'https://api.phpsandbox.io/v1', options = {}) {
|
|
158
|
+
this.runtimeTransportValue = 'realtime';
|
|
154
159
|
this.fetch = globalThis.fetch;
|
|
155
160
|
this.deployments = new DeploymentApi(this);
|
|
156
161
|
this.notebook = new NotebookApi(this);
|
|
@@ -224,6 +229,14 @@ export class Client {
|
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
231
|
export class PHPSandbox extends Client {
|
|
232
|
+
static realtime(token, url = 'https://api.phpsandbox.io/v1', options = {}) {
|
|
233
|
+
return new PHPSandbox(token, url, options);
|
|
234
|
+
}
|
|
235
|
+
static rest(token, url = 'https://api.phpsandbox.io/v1', options = {}) {
|
|
236
|
+
const client = new PHPSandbox(token, url, options);
|
|
237
|
+
client.runtimeTransportValue = 'rest';
|
|
238
|
+
return client;
|
|
239
|
+
}
|
|
227
240
|
}
|
|
228
241
|
export class NotebookSecretApi {
|
|
229
242
|
constructor(client) {
|
|
@@ -324,22 +337,44 @@ export class NotebookMailApi {
|
|
|
324
337
|
}
|
|
325
338
|
}
|
|
326
339
|
export class NotebookInstance {
|
|
340
|
+
get runtimeTransport() {
|
|
341
|
+
return this.client.runtimeTransport;
|
|
342
|
+
}
|
|
327
343
|
constructor(data, client) {
|
|
328
344
|
_NotebookInstance_instances.add(this);
|
|
329
345
|
this.data = data;
|
|
330
346
|
this.client = client;
|
|
331
347
|
this.initialized = false;
|
|
332
|
-
_NotebookInstance_initPromise.set(this,
|
|
348
|
+
_NotebookInstance_initPromise.set(this, null);
|
|
333
349
|
this.disposables = [];
|
|
334
350
|
this.emitter = EventManager.createInstance();
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
351
|
+
if (client.runtimeTransport === 'rest') {
|
|
352
|
+
this.socket = null;
|
|
353
|
+
this.restInvoker = new RestRuntimeInvoker(data, client.options.fetch ?? globalThis.fetch, client.options.okraUrlProvider === undefined
|
|
354
|
+
? undefined
|
|
355
|
+
: () => client.options.okraUrlProvider(data.id));
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
this.restInvoker = null;
|
|
359
|
+
let initialConnection = true;
|
|
360
|
+
const connectionUrl = client.options.okraUrlProvider === undefined
|
|
361
|
+
? data.okraUrl
|
|
362
|
+
: async () => {
|
|
363
|
+
if (initialConnection) {
|
|
364
|
+
initialConnection = false;
|
|
365
|
+
return data.okraUrl;
|
|
366
|
+
}
|
|
367
|
+
return client.options.okraUrlProvider(data.id);
|
|
368
|
+
};
|
|
369
|
+
this.socket = new Transport(connectionUrl, this.emitter, {
|
|
370
|
+
debug: client.options.debug,
|
|
371
|
+
startClosed: client.options.startClosed,
|
|
372
|
+
webSocket: client.options.webSocket,
|
|
373
|
+
});
|
|
374
|
+
this.watchConnection();
|
|
375
|
+
__classPrivateFieldSet(this, _NotebookInstance_initPromise, __classPrivateFieldGet(this, _NotebookInstance_instances, "m", _NotebookInstance_init).call(this), "f");
|
|
376
|
+
}
|
|
377
|
+
this.file = new Filesystem(this, this.socket === null ? null : (handler) => this.onDidConnect(handler));
|
|
343
378
|
this.terminal = new Terminal(this);
|
|
344
379
|
this.auth = new Auth(this);
|
|
345
380
|
this.lsp = new Lsp(this);
|
|
@@ -357,7 +392,11 @@ export class NotebookInstance {
|
|
|
357
392
|
this.mail = new NotebookMail(client, this.data.id);
|
|
358
393
|
}
|
|
359
394
|
async ready() {
|
|
360
|
-
|
|
395
|
+
if (this.restInvoker !== null) {
|
|
396
|
+
return this.restInitialization();
|
|
397
|
+
}
|
|
398
|
+
const socket = this.realtimeSocket();
|
|
399
|
+
const terminalError = socket.getTerminalError();
|
|
361
400
|
if (terminalError) {
|
|
362
401
|
throw terminalError;
|
|
363
402
|
}
|
|
@@ -366,8 +405,8 @@ export class NotebookInstance {
|
|
|
366
405
|
* If ready is called, ensure the websocket is open so server-driven
|
|
367
406
|
* initialization can begin, but do not block on an extra ping roundtrip.
|
|
368
407
|
*/
|
|
369
|
-
if (this.client.options.startClosed && !
|
|
370
|
-
await
|
|
408
|
+
if (this.client.options.startClosed && !socket.isConnected) {
|
|
409
|
+
await socket.connect();
|
|
371
410
|
}
|
|
372
411
|
return __classPrivateFieldGet(this, _NotebookInstance_initPromise, "f");
|
|
373
412
|
};
|
|
@@ -389,7 +428,7 @@ export class NotebookInstance {
|
|
|
389
428
|
}
|
|
390
429
|
delete() {
|
|
391
430
|
return this.client.notebook.delete(this.data.id).then(() => {
|
|
392
|
-
this.socket
|
|
431
|
+
this.socket?.terminate(new NotebookUnavailableError('Notebook has been deleted.', { id: this.data.id }));
|
|
393
432
|
});
|
|
394
433
|
}
|
|
395
434
|
stop() {
|
|
@@ -399,10 +438,19 @@ export class NotebookInstance {
|
|
|
399
438
|
return this.container.restart();
|
|
400
439
|
}
|
|
401
440
|
invoke(action, data = {}, options = {}) {
|
|
402
|
-
|
|
441
|
+
if (this.restInvoker !== null) {
|
|
442
|
+
if (this.restInvoker.supports(String(action))) {
|
|
443
|
+
void this.restInitialization();
|
|
444
|
+
}
|
|
445
|
+
return this.restInvoker.invoke(action, data || {}, options);
|
|
446
|
+
}
|
|
447
|
+
return this.realtimeSocket().invoke(action, data || {}, options);
|
|
403
448
|
}
|
|
404
449
|
send(action, data = {}) {
|
|
405
|
-
|
|
450
|
+
if (this.restInvoker !== null) {
|
|
451
|
+
return this.restInvoker.send();
|
|
452
|
+
}
|
|
453
|
+
return this.realtimeSocket().send(action, data || {});
|
|
406
454
|
}
|
|
407
455
|
ping() {
|
|
408
456
|
return this.invoke('ping');
|
|
@@ -414,7 +462,9 @@ export class NotebookInstance {
|
|
|
414
462
|
}
|
|
415
463
|
dispose() {
|
|
416
464
|
this.disposables.forEach((d) => d.dispose());
|
|
417
|
-
|
|
465
|
+
if (this.socket !== null && !this.socket.isClosed) {
|
|
466
|
+
this.socket.disconnect();
|
|
467
|
+
}
|
|
418
468
|
}
|
|
419
469
|
connected() {
|
|
420
470
|
return this.waitForSocketConnection({ rejectOnClose: true });
|
|
@@ -423,11 +473,12 @@ export class NotebookInstance {
|
|
|
423
473
|
return this.waitForSocketConnection({ rejectOnClose: false });
|
|
424
474
|
}
|
|
425
475
|
waitForSocketConnection(options) {
|
|
426
|
-
const
|
|
476
|
+
const socket = this.realtimeSocket();
|
|
477
|
+
const terminalError = socket.getTerminalError();
|
|
427
478
|
if (terminalError) {
|
|
428
479
|
return Promise.reject(terminalError);
|
|
429
480
|
}
|
|
430
|
-
if (
|
|
481
|
+
if (socket.isConnected) {
|
|
431
482
|
return Promise.resolve(this);
|
|
432
483
|
}
|
|
433
484
|
return new Promise((resolve, reject) => {
|
|
@@ -448,11 +499,11 @@ export class NotebookInstance {
|
|
|
448
499
|
callback();
|
|
449
500
|
};
|
|
450
501
|
try {
|
|
451
|
-
disposables.push(
|
|
502
|
+
disposables.push(socket.onDidConnect(() => settle(() => resolve(this))));
|
|
452
503
|
if (options.rejectOnClose) {
|
|
453
|
-
disposables.push(
|
|
504
|
+
disposables.push(socket.onDidClose(() => settle(() => reject(new Error('Connection closed')))));
|
|
454
505
|
}
|
|
455
|
-
disposables.push(
|
|
506
|
+
disposables.push(socket.onDidBootError((error) => settle(() => reject(error))));
|
|
456
507
|
}
|
|
457
508
|
catch (e) {
|
|
458
509
|
settle(() => reject(e));
|
|
@@ -460,38 +511,54 @@ export class NotebookInstance {
|
|
|
460
511
|
});
|
|
461
512
|
}
|
|
462
513
|
watchConnection() {
|
|
463
|
-
this.
|
|
464
|
-
|
|
514
|
+
const socket = this.realtimeSocket();
|
|
515
|
+
socket.onDidConnect(() => {
|
|
516
|
+
socket.emit('okra.connected');
|
|
465
517
|
if (this.client.options.telemetry) {
|
|
466
518
|
this.container.enableTelemetry(this.client.options.telemetry);
|
|
467
519
|
}
|
|
468
520
|
});
|
|
469
|
-
|
|
470
|
-
|
|
521
|
+
socket.onDidClose(() => {
|
|
522
|
+
socket.emit('okra.disconnected');
|
|
471
523
|
this.initialized = false;
|
|
472
524
|
});
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
525
|
+
socket.onDidBootError((error) => {
|
|
526
|
+
socket.emit('okra.boot_error', error);
|
|
527
|
+
socket.emit('okra.disconnected');
|
|
476
528
|
this.initialized = false;
|
|
477
529
|
});
|
|
478
530
|
}
|
|
479
531
|
onDidConnect(handler) {
|
|
480
|
-
this.
|
|
481
|
-
|
|
532
|
+
const socket = this.realtimeSocket();
|
|
533
|
+
socket.removeListener('okra.connected', handler);
|
|
534
|
+
const disposable = socket.listen('okra.connected', handler);
|
|
482
535
|
this.disposables.push(disposable);
|
|
483
536
|
return disposable;
|
|
484
537
|
}
|
|
485
538
|
onDidDisconnect(handler) {
|
|
486
|
-
const disposable = this.
|
|
539
|
+
const disposable = this.realtimeSocket().listen('okra.disconnected', handler);
|
|
487
540
|
this.disposables.push(disposable);
|
|
488
541
|
return disposable;
|
|
489
542
|
}
|
|
490
543
|
onDidBootError(handler) {
|
|
491
|
-
const disposable = this.
|
|
544
|
+
const disposable = this.realtimeSocket().listen('okra.boot_error', handler);
|
|
492
545
|
this.disposables.push(disposable);
|
|
493
546
|
return disposable;
|
|
494
547
|
}
|
|
548
|
+
restInitialization() {
|
|
549
|
+
if (this.restInvoker === null) {
|
|
550
|
+
throw new UnsupportedRuntimeTransportError('This operation requires the REST transport.');
|
|
551
|
+
}
|
|
552
|
+
if (__classPrivateFieldGet(this, _NotebookInstance_initPromise, "f") === null) {
|
|
553
|
+
__classPrivateFieldSet(this, _NotebookInstance_initPromise, this.restInvoker.ready().then((result) => {
|
|
554
|
+
this.initialized = result;
|
|
555
|
+
this.emitter.emit('notebook.initialized', result);
|
|
556
|
+
return result;
|
|
557
|
+
}), "f");
|
|
558
|
+
void __classPrivateFieldGet(this, _NotebookInstance_initPromise, "f").catch(() => { });
|
|
559
|
+
}
|
|
560
|
+
return __classPrivateFieldGet(this, _NotebookInstance_initPromise, "f");
|
|
561
|
+
}
|
|
495
562
|
update(input) {
|
|
496
563
|
return this.client.notebook.update(this.data.id, input);
|
|
497
564
|
}
|
|
@@ -499,10 +566,11 @@ export class NotebookInstance {
|
|
|
499
566
|
return this.listen('notebook.initialized', handler);
|
|
500
567
|
}
|
|
501
568
|
async reconnect() {
|
|
569
|
+
const socket = this.realtimeSocket();
|
|
502
570
|
const whenConnected = this.whenConnected();
|
|
503
571
|
// Use the socket's reconnect method which preserves listeners
|
|
504
572
|
// and uses the underlying ReconnectingWebSocket mechanism
|
|
505
|
-
|
|
573
|
+
socket.reconnect();
|
|
506
574
|
// Reset initialization state
|
|
507
575
|
this.initialized = false;
|
|
508
576
|
// Wait for the socket to reconnect
|
|
@@ -517,6 +585,12 @@ export class NotebookInstance {
|
|
|
517
585
|
iframe.src = result.data.previewUrl;
|
|
518
586
|
return createBeacon(iframe, options);
|
|
519
587
|
}
|
|
588
|
+
realtimeSocket() {
|
|
589
|
+
if (this.socket === null) {
|
|
590
|
+
throw new UnsupportedRuntimeTransportError('This operation requires the realtime transport.');
|
|
591
|
+
}
|
|
592
|
+
return this.socket;
|
|
593
|
+
}
|
|
520
594
|
}
|
|
521
595
|
_NotebookInstance_initPromise = new WeakMap(), _NotebookInstance_instances = new WeakSet(), _NotebookInstance_init = function _NotebookInstance_init() {
|
|
522
596
|
__classPrivateFieldSet(this, _NotebookInstance_initPromise, new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CallOption, Invokable, NotebookData, NotebookInitSuccessResult } from '../index.js';
|
|
2
|
+
type RestRuntimeAction = keyof Invokable | 'runtime.ready';
|
|
3
|
+
export declare class RestRuntimeInvoker {
|
|
4
|
+
private readonly fetch;
|
|
5
|
+
private readonly refresh?;
|
|
6
|
+
private actionUrl;
|
|
7
|
+
private ticket;
|
|
8
|
+
private refreshPromise;
|
|
9
|
+
private readonly notebookId;
|
|
10
|
+
constructor(data: NotebookData, fetch: typeof globalThis.fetch, refresh?: () => Promise<string>);
|
|
11
|
+
private configure;
|
|
12
|
+
ready(): Promise<NotebookInitSuccessResult>;
|
|
13
|
+
supports(action: string): action is RestRuntimeAction;
|
|
14
|
+
invoke<T extends keyof Invokable>(action: T, data?: Invokable[T]['args'], options?: CallOption): Promise<Invokable[T]['response']>;
|
|
15
|
+
send(): never;
|
|
16
|
+
private invokeRaw;
|
|
17
|
+
private request;
|
|
18
|
+
private refreshAuthorization;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { ErrorEvent } from '../types.js';
|
|
2
|
+
import { UnsupportedRuntimeTransportError } from '../errors/index.js';
|
|
3
|
+
const restActionRoutes = {
|
|
4
|
+
'runtime.ready': { method: 'GET', path: '/runtime' },
|
|
5
|
+
ping: { method: 'GET', path: '/runtime/ping' },
|
|
6
|
+
'config.get': { method: 'GET', path: '/runtime/config' },
|
|
7
|
+
'config.update': { method: 'PATCH', path: '/runtime/config' },
|
|
8
|
+
'config.set-ports': { method: 'PUT', path: '/runtime/config/ports' },
|
|
9
|
+
'container.state': { method: 'GET', path: '/runtime/container' },
|
|
10
|
+
'container.start': { method: 'POST', path: '/runtime/container/start' },
|
|
11
|
+
'container.restart': { method: 'POST', path: '/runtime/container/restart' },
|
|
12
|
+
'container.stop': { method: 'POST', path: '/runtime/container/stop' },
|
|
13
|
+
'container.set-php': { method: 'PUT', path: '/runtime/container/php' },
|
|
14
|
+
'container.opened-ports': { method: 'GET', path: '/runtime/container/ports' },
|
|
15
|
+
'terminal.list': { method: 'GET', path: '/runtime/terminals' },
|
|
16
|
+
'shell.exec': { method: 'POST', path: '/runtime/executions' },
|
|
17
|
+
'fs.info': { method: 'POST', path: '/runtime/files/info' },
|
|
18
|
+
'fs.readFile': { method: 'GET', path: '/runtime/files/content' },
|
|
19
|
+
'fs.writeFile': { method: 'PUT', path: '/runtime/files/content' },
|
|
20
|
+
'fs.write': { method: 'PUT', path: '/runtime/files/text' },
|
|
21
|
+
'fs.mkdir': { method: 'POST', path: '/runtime/directories' },
|
|
22
|
+
'fs.createDirectory': { method: 'PUT', path: '/runtime/directories' },
|
|
23
|
+
'fs.readDirectory': { method: 'POST', path: '/runtime/directories/read' },
|
|
24
|
+
'fs.stat': { method: 'POST', path: '/runtime/files/stat' },
|
|
25
|
+
'fs.move': { method: 'POST', path: '/runtime/files/move' },
|
|
26
|
+
'fs.rename': { method: 'POST', path: '/runtime/files/rename' },
|
|
27
|
+
'fs.copy': { method: 'POST', path: '/runtime/files/copy' },
|
|
28
|
+
'fs.remove': { method: 'POST', path: '/runtime/files/remove' },
|
|
29
|
+
'fs.delete': { method: 'DELETE', path: '/runtime/files' },
|
|
30
|
+
'fs.tail': { method: 'POST', path: '/runtime/files/tail' },
|
|
31
|
+
'fs.find': { method: 'POST', path: '/runtime/files/find' },
|
|
32
|
+
'fs.textSearch': { method: 'POST', path: '/runtime/files/search' },
|
|
33
|
+
'fs.tree': { method: 'POST', path: '/runtime/files/tree' },
|
|
34
|
+
'service.list': { method: 'GET', path: '/runtime/services' },
|
|
35
|
+
'service.run': { method: 'POST', path: '/runtime/services/run' },
|
|
36
|
+
'service.stop': { method: 'POST', path: '/runtime/services/stop' },
|
|
37
|
+
'service.logs': { method: 'POST', path: '/runtime/services/logs' },
|
|
38
|
+
'composer.invoke': { method: 'POST', path: '/runtime/composer/executions' },
|
|
39
|
+
'composer.packages': { method: 'GET', path: '/runtime/composer/packages' },
|
|
40
|
+
'composer.dump-autoload': {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
path: '/runtime/composer/autoload',
|
|
43
|
+
},
|
|
44
|
+
'git.checkpoint': { method: 'POST', path: '/runtime/git/checkpoints' },
|
|
45
|
+
'git.checkout': { method: 'POST', path: '/runtime/git/checkouts' },
|
|
46
|
+
'git.log': { method: 'POST', path: '/runtime/git/log' },
|
|
47
|
+
'git.sync': { method: 'POST', path: '/runtime/git/sync' },
|
|
48
|
+
'git.diff': { method: 'POST', path: '/runtime/git/diff' },
|
|
49
|
+
'git.merge': { method: 'POST', path: '/runtime/git/merge' },
|
|
50
|
+
'git.conflicts': { method: 'GET', path: '/runtime/git/conflicts' },
|
|
51
|
+
'git.resolve': { method: 'POST', path: '/runtime/git/conflicts/resolve' },
|
|
52
|
+
'git.restore': { method: 'POST', path: '/runtime/git/restore' },
|
|
53
|
+
'git.status': { method: 'GET', path: '/runtime/git/status' },
|
|
54
|
+
'git.review': { method: 'GET', path: '/runtime/git/review' },
|
|
55
|
+
'git.stage': { method: 'POST', path: '/runtime/git/stage' },
|
|
56
|
+
'git.unstage': { method: 'POST', path: '/runtime/git/unstage' },
|
|
57
|
+
'git.revert': { method: 'POST', path: '/runtime/git/revert' },
|
|
58
|
+
};
|
|
59
|
+
export class RestRuntimeInvoker {
|
|
60
|
+
constructor(data, fetch, refresh) {
|
|
61
|
+
this.fetch = fetch;
|
|
62
|
+
this.refresh = refresh;
|
|
63
|
+
this.refreshPromise = null;
|
|
64
|
+
this.notebookId = data.id;
|
|
65
|
+
this.configure(data.okraUrl);
|
|
66
|
+
}
|
|
67
|
+
configure(connectionUrl) {
|
|
68
|
+
const okraUrl = new URL(connectionUrl);
|
|
69
|
+
const ticket = okraUrl.searchParams.get('ticket') ?? '';
|
|
70
|
+
if (ticket === '') {
|
|
71
|
+
throw new UnsupportedRuntimeTransportError('The notebook connection does not contain a runtime ticket.');
|
|
72
|
+
}
|
|
73
|
+
this.ticket = ticket;
|
|
74
|
+
okraUrl.protocol = okraUrl.protocol === 'wss:' ? 'https:' : 'http:';
|
|
75
|
+
okraUrl.pathname = `/api/v1/notebooks/${encodeURIComponent(this.notebookId)}`;
|
|
76
|
+
okraUrl.searchParams.delete('ticket');
|
|
77
|
+
okraUrl.hash = '';
|
|
78
|
+
this.actionUrl = okraUrl;
|
|
79
|
+
}
|
|
80
|
+
ready() {
|
|
81
|
+
return this.invokeRaw('runtime.ready', {}, {});
|
|
82
|
+
}
|
|
83
|
+
supports(action) {
|
|
84
|
+
return action in restActionRoutes;
|
|
85
|
+
}
|
|
86
|
+
invoke(action, data = {}, options = {}) {
|
|
87
|
+
return this.invokeRaw(action, data ?? {}, options);
|
|
88
|
+
}
|
|
89
|
+
send() {
|
|
90
|
+
throw new UnsupportedRuntimeTransportError('Fire-and-forget actions require the realtime transport.');
|
|
91
|
+
}
|
|
92
|
+
async invokeRaw(action, data, options) {
|
|
93
|
+
const abort = runtimeAbort(options);
|
|
94
|
+
try {
|
|
95
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
96
|
+
const response = await this.fetch(this.request(action, data, abort.signal));
|
|
97
|
+
if (response.status === 401 && attempt === 0 && this.refresh !== undefined) {
|
|
98
|
+
await this.refreshAuthorization();
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const payload = await runtimeActionResponse(response);
|
|
102
|
+
if (!response.ok || payload.error !== undefined) {
|
|
103
|
+
const error = payload.error ?? payload;
|
|
104
|
+
const message = typeof error.message === 'string' ? error.message : `Runtime action ${action} failed.`;
|
|
105
|
+
const code = typeof error.code === 'number' ? error.code : response.status;
|
|
106
|
+
throw new ErrorEvent(code, message, error);
|
|
107
|
+
}
|
|
108
|
+
return payload.data;
|
|
109
|
+
}
|
|
110
|
+
throw new ErrorEvent(401, `Runtime action ${action} could not be authorized.`, {});
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
abort.dispose();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
request(action, data, signal) {
|
|
117
|
+
const route = restActionRoutes[action];
|
|
118
|
+
if (route === undefined) {
|
|
119
|
+
throw new UnsupportedRuntimeTransportError(`Runtime action ${action} requires the realtime transport.`);
|
|
120
|
+
}
|
|
121
|
+
const url = new URL(this.actionUrl);
|
|
122
|
+
url.pathname += route.path;
|
|
123
|
+
const headers = {
|
|
124
|
+
Accept: 'application/json, application/octet-stream',
|
|
125
|
+
Authorization: `Bearer ${this.ticket}`,
|
|
126
|
+
};
|
|
127
|
+
const body = runtimeActionRequestBody(action, data, url, headers);
|
|
128
|
+
return new Request(url, {
|
|
129
|
+
method: route.method,
|
|
130
|
+
headers,
|
|
131
|
+
body: route.method === 'GET' ? undefined : body,
|
|
132
|
+
signal,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async refreshAuthorization() {
|
|
136
|
+
if (this.refreshPromise === null) {
|
|
137
|
+
this.refreshPromise = this.refresh().then((okraUrl) => this.configure(okraUrl));
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
await this.refreshPromise;
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
this.refreshPromise = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function runtimeActionResponse(response) {
|
|
148
|
+
if (response.ok && response.headers.get('content-type')?.toLowerCase().startsWith('application/octet-stream')) {
|
|
149
|
+
return { data: new Uint8Array(await response.arrayBuffer()) };
|
|
150
|
+
}
|
|
151
|
+
const body = await response.text();
|
|
152
|
+
if (body === '') {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
return JSON.parse(body);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return { error: { code: response.status, message: body } };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function runtimeActionRequestBody(action, data, url, headers) {
|
|
163
|
+
const payload = runtimeActionRecord(data);
|
|
164
|
+
if (action === 'fs.readFile') {
|
|
165
|
+
setRequiredRuntimeQuery(url, 'path', payload.path);
|
|
166
|
+
const lineRange = runtimeActionRecord(payload.lineRange);
|
|
167
|
+
if (lineRange.lineStart !== undefined || lineRange.lineEnd !== undefined) {
|
|
168
|
+
setRequiredRuntimeQuery(url, 'lineStart', lineRange.lineStart);
|
|
169
|
+
setRequiredRuntimeQuery(url, 'lineEnd', lineRange.lineEnd);
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
if (action === 'fs.writeFile') {
|
|
174
|
+
setRequiredRuntimeQuery(url, 'path', payload.path);
|
|
175
|
+
const options = runtimeActionRecord(payload.options);
|
|
176
|
+
setBooleanRuntimeQuery(url, 'overwrite', options.overwrite);
|
|
177
|
+
setBooleanRuntimeQuery(url, 'create', options.create);
|
|
178
|
+
setBooleanRuntimeQuery(url, 'unlock', options.unlock);
|
|
179
|
+
const atomic = runtimeActionRecord(options.atomic);
|
|
180
|
+
if (typeof atomic.postfix === 'string' && atomic.postfix !== '') {
|
|
181
|
+
url.searchParams.set('atomicPostfix', atomic.postfix);
|
|
182
|
+
}
|
|
183
|
+
if (!ArrayBuffer.isView(payload.contents)) {
|
|
184
|
+
throw new TypeError('Runtime file contents must be binary data.');
|
|
185
|
+
}
|
|
186
|
+
headers['Content-Type'] = 'application/octet-stream';
|
|
187
|
+
return Uint8Array.from(new Uint8Array(payload.contents.buffer, payload.contents.byteOffset, payload.contents.byteLength)).buffer;
|
|
188
|
+
}
|
|
189
|
+
headers['Content-Type'] = 'application/json';
|
|
190
|
+
return JSON.stringify(data);
|
|
191
|
+
}
|
|
192
|
+
function runtimeActionRecord(value) {
|
|
193
|
+
return value !== null && typeof value === 'object' ? value : {};
|
|
194
|
+
}
|
|
195
|
+
function setRequiredRuntimeQuery(url, name, value) {
|
|
196
|
+
if ((typeof value !== 'string' && typeof value !== 'number') || String(value) === '') {
|
|
197
|
+
throw new TypeError(`Runtime file ${name} is required.`);
|
|
198
|
+
}
|
|
199
|
+
url.searchParams.set(name, String(value));
|
|
200
|
+
}
|
|
201
|
+
function setBooleanRuntimeQuery(url, name, value) {
|
|
202
|
+
url.searchParams.set(name, value === true ? 'true' : 'false');
|
|
203
|
+
}
|
|
204
|
+
function runtimeAbort(options) {
|
|
205
|
+
if (options.timeout === undefined && options.abortSignal === undefined) {
|
|
206
|
+
return { signal: new AbortController().signal, dispose: () => undefined };
|
|
207
|
+
}
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
const abortFromCaller = () => controller.abort(options.abortSignal?.reason);
|
|
210
|
+
options.abortSignal?.addEventListener('abort', abortFromCaller, {
|
|
211
|
+
once: true,
|
|
212
|
+
});
|
|
213
|
+
if (options.abortSignal?.aborted) {
|
|
214
|
+
abortFromCaller();
|
|
215
|
+
}
|
|
216
|
+
const timeout = options.timeout === undefined ? undefined : setTimeout(() => controller.abort(), options.timeout);
|
|
217
|
+
return {
|
|
218
|
+
signal: controller.signal,
|
|
219
|
+
dispose: () => {
|
|
220
|
+
options.abortSignal?.removeEventListener('abort', abortFromCaller);
|
|
221
|
+
if (timeout !== undefined) {
|
|
222
|
+
clearTimeout(timeout);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
package/dist/socket/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CloseEvent as ReconnectingWebSocketCloseEvent, ErrorEvent as ReconnectingWebSocketErrorEvent } from 'reconnecting-websocket';
|
|
2
2
|
import { EventDispatcher } from '../events/index.js';
|
|
3
3
|
import { NotebookUnavailableError } from '../errors/index.js';
|
|
4
|
+
type TransportUrlProvider = string | (() => string) | (() => Promise<string>);
|
|
4
5
|
interface WsOptions {
|
|
5
6
|
debug?: boolean;
|
|
6
7
|
startClosed?: boolean;
|
|
@@ -73,7 +74,7 @@ export declare class Transport {
|
|
|
73
74
|
private readonly CONNECT_WAIT_TIMEOUT;
|
|
74
75
|
private pingIntervalStarted;
|
|
75
76
|
private incomingMessageChain;
|
|
76
|
-
constructor(url:
|
|
77
|
+
constructor(url: TransportUrlProvider, eventEmitter: EventDispatcher, options?: WsOptions);
|
|
77
78
|
/**
|
|
78
79
|
* Validate configuration options
|
|
79
80
|
*/
|