@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.
@@ -9,3 +9,6 @@ export declare class NotebookUnavailableError extends Error {
9
9
  readonly raw: object;
10
10
  constructor(message?: string, raw?: object);
11
11
  }
12
+ export declare class UnsupportedRuntimeTransportError extends Error {
13
+ constructor(message: string);
14
+ }
@@ -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
+ }
@@ -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 watchOkraConnection;
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[]>;
@@ -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.watchOkraConnection();
75
+ this.registerReconnectHandler(registerReconnect);
76
76
  }
77
- watchOkraConnection() {
78
- this.okra.onDidConnect(() => {
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, void 0);
348
+ _NotebookInstance_initPromise.set(this, null);
333
349
  this.disposables = [];
334
350
  this.emitter = EventManager.createInstance();
335
- this.socket = new Transport(data.okraUrl, this.emitter, {
336
- debug: client.options.debug,
337
- startClosed: client.options.startClosed,
338
- webSocket: client.options.webSocket,
339
- });
340
- this.watchConnection();
341
- __classPrivateFieldSet(this, _NotebookInstance_initPromise, __classPrivateFieldGet(this, _NotebookInstance_instances, "m", _NotebookInstance_init).call(this), "f");
342
- this.file = new Filesystem(this);
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
- const terminalError = this.socket.getTerminalError();
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 && !this.socket.isConnected) {
370
- await this.socket.connect();
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.terminate(new NotebookUnavailableError('Notebook has been deleted.', { id: this.data.id }));
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
- return this.socket.invoke(action, data || {}, options);
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
- return this.socket.send(action, data || {});
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
- !this.socket.isClosed && this.socket.disconnect();
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 terminalError = this.socket.getTerminalError();
476
+ const socket = this.realtimeSocket();
477
+ const terminalError = socket.getTerminalError();
427
478
  if (terminalError) {
428
479
  return Promise.reject(terminalError);
429
480
  }
430
- if (this.socket.isConnected) {
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(this.socket.onDidConnect(() => settle(() => resolve(this))));
502
+ disposables.push(socket.onDidConnect(() => settle(() => resolve(this))));
452
503
  if (options.rejectOnClose) {
453
- disposables.push(this.socket.onDidClose(() => settle(() => reject(new Error('Connection closed')))));
504
+ disposables.push(socket.onDidClose(() => settle(() => reject(new Error('Connection closed')))));
454
505
  }
455
- disposables.push(this.socket.onDidBootError((error) => settle(() => reject(error))));
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.socket.onDidConnect(() => {
464
- this.socket.emit('okra.connected');
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
- this.socket.onDidClose(() => {
470
- this.socket.emit('okra.disconnected');
521
+ socket.onDidClose(() => {
522
+ socket.emit('okra.disconnected');
471
523
  this.initialized = false;
472
524
  });
473
- this.socket.onDidBootError((error) => {
474
- this.socket.emit('okra.boot_error', error);
475
- this.socket.emit('okra.disconnected');
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.socket.removeListener('okra.connected', handler);
481
- const disposable = this.socket.listen('okra.connected', handler);
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.socket.listen('okra.disconnected', handler);
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.socket.listen('okra.boot_error', handler);
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
- this.socket.reconnect();
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
+ }
@@ -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: string, eventEmitter: EventDispatcher, options?: WsOptions);
77
+ constructor(url: TransportUrlProvider, eventEmitter: EventDispatcher, options?: WsOptions);
77
78
  /**
78
79
  * Validate configuration options
79
80
  */