@phpsandbox/sdk 0.0.29 → 0.0.32

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
@@ -101,6 +101,7 @@ Each `NotebookInstance` exposes service clients:
101
101
  - `notebook.laravel` (`Laravel`)
102
102
  - `notebook.auth` (`Auth`)
103
103
  - `notebook.log` (`Log`)
104
+ - `notebook.services` (`Services`)
104
105
 
105
106
  ## Common Operations
106
107
 
@@ -144,6 +145,28 @@ result.throw();
144
145
  console.log(result.output);
145
146
  ```
146
147
 
148
+ ### Services
149
+
150
+ ```ts
151
+ import { notebookBuiltinServices } from '@phpsandbox/sdk';
152
+
153
+ const services = await notebook.services.list();
154
+
155
+ console.log(notebookBuiltinServices); // ['redis']
156
+
157
+ await notebook.services.run('redis');
158
+ await notebook.services.run('queue', 'php artisan queue:work');
159
+
160
+ const snapshot = await notebook.services.logs('redis', { tail: 50 });
161
+ console.log(snapshot);
162
+
163
+ const stream = notebook.services.logs('redis', { follow: true, tail: 25 });
164
+ const reader = stream.output.getReader();
165
+ const chunk = await reader.read();
166
+ console.log(chunk.value);
167
+ await stream.stop();
168
+ ```
169
+
147
170
  ### Composer
148
171
 
149
172
  ```ts
@@ -572,6 +572,13 @@ var SendTimeoutError = class _SendTimeoutError extends PromiseTimeoutError {
572
572
  return new _SendTimeoutError(error.message, error.time);
573
573
  }
574
574
  };
575
+ var NotebookUnavailableError = class extends Error {
576
+ constructor(message = "Notebook is no longer available", raw = {}) {
577
+ super(message);
578
+ this.raw = raw;
579
+ this.name = "NotebookUnavailableError";
580
+ }
581
+ };
575
582
 
576
583
  // src/utils/promise.ts
577
584
  var timeout = (prom, time) => {
@@ -1019,6 +1026,108 @@ var Shell = class {
1019
1026
  }
1020
1027
  };
1021
1028
 
1029
+ // src/services.ts
1030
+ var notebookBuiltinServices = ["redis"];
1031
+ var notebookKnownServices = ["start", "nginx", ...notebookBuiltinServices];
1032
+ var Services = class {
1033
+ constructor(okra) {
1034
+ this.okra = okra;
1035
+ }
1036
+ list() {
1037
+ return this.okra.invoke("service.list");
1038
+ }
1039
+ run(name, command) {
1040
+ return this.okra.invoke("service.run", {
1041
+ name,
1042
+ command
1043
+ });
1044
+ }
1045
+ stop(name) {
1046
+ return this.okra.invoke("service.stop", { name });
1047
+ }
1048
+ onLog(id, handler) {
1049
+ return this.okra.listen(`service.log.${id}`, handler);
1050
+ }
1051
+ listen(event, handler) {
1052
+ return this.okra.listen(event, handler);
1053
+ }
1054
+ logs(name, options = {}) {
1055
+ if (options.follow) {
1056
+ return this.followLogs(name, options);
1057
+ }
1058
+ return this.okra.invoke("service.logs", {
1059
+ name,
1060
+ tail: options.tail
1061
+ }).then((response) => {
1062
+ if (!("output" in response)) {
1063
+ throw new Error(`Unexpected service.logs response for ${name}`);
1064
+ }
1065
+ return response.output;
1066
+ });
1067
+ }
1068
+ followLogs(name, options) {
1069
+ const id = options.id || nanoid();
1070
+ const disposables = /* @__PURE__ */ new Set();
1071
+ let controller = null;
1072
+ let closed = false;
1073
+ const dispose = () => {
1074
+ for (const disposable of disposables) {
1075
+ disposable.dispose();
1076
+ }
1077
+ disposables.clear();
1078
+ };
1079
+ const close = () => {
1080
+ if (closed) {
1081
+ return;
1082
+ }
1083
+ closed = true;
1084
+ dispose();
1085
+ if (controller) {
1086
+ try {
1087
+ controller.close();
1088
+ } catch {
1089
+ }
1090
+ }
1091
+ };
1092
+ const fail = (error) => {
1093
+ if (closed) {
1094
+ return;
1095
+ }
1096
+ closed = true;
1097
+ dispose();
1098
+ if (controller) {
1099
+ controller.error(error);
1100
+ }
1101
+ };
1102
+ const stop = once(() => {
1103
+ close();
1104
+ return this.okra.invoke("service.stop-logs", { id });
1105
+ });
1106
+ const output = new ReadableStream({
1107
+ start: (_controller) => {
1108
+ controller = _controller;
1109
+ disposables.add(
1110
+ this.onLog(id, (data) => {
1111
+ controller?.enqueue(data.output);
1112
+ })
1113
+ );
1114
+ void this.okra.invoke("service.logs", {
1115
+ name,
1116
+ tail: options.tail,
1117
+ follow: true,
1118
+ id
1119
+ }).catch((error) => {
1120
+ fail(error);
1121
+ });
1122
+ },
1123
+ cancel: () => {
1124
+ void stop();
1125
+ }
1126
+ });
1127
+ return { id, output, stop };
1128
+ }
1129
+ };
1130
+
1022
1131
  // node_modules/@msgpack/msgpack/dist.esm/utils/utf8.mjs
1023
1132
  function utf8Count(str) {
1024
1133
  const strLength = str.length;
@@ -3042,6 +3151,7 @@ var Transport = class {
3042
3151
  this.closed = false;
3043
3152
  this.disposables = new NamedDisposable();
3044
3153
  this.connectPromise = null;
3154
+ this.terminalError = null;
3045
3155
  this.pingIntervalStarted = false;
3046
3156
  // Connection health monitoring
3047
3157
  this.connectionStats = {
@@ -3073,7 +3183,7 @@ var Transport = class {
3073
3183
  const startClosed = options.startClosed !== false;
3074
3184
  this.rws = new reconnecting_websocket_mjs_default(this.url.toString(), [], {
3075
3185
  WebSocket: globalThis.WebSocket ?? browser_default,
3076
- connectionTimeout: options.connectionTimeout ?? 1e3,
3186
+ connectionTimeout: options.connectionTimeout,
3077
3187
  maxReconnectionDelay: 2e3,
3078
3188
  minReconnectionDelay: 200,
3079
3189
  maxEnqueuedMessages: 0,
@@ -3116,6 +3226,9 @@ var Transport = class {
3116
3226
  * Ensure the websocket is open without requiring an application-level roundtrip.
3117
3227
  */
3118
3228
  connect() {
3229
+ if (this.terminalError) {
3230
+ return Promise.reject(this.terminalError);
3231
+ }
3119
3232
  return __privateMethod(this, _Transport_instances, connect_fn).call(this);
3120
3233
  }
3121
3234
  id() {
@@ -3172,7 +3285,12 @@ var Transport = class {
3172
3285
  return;
3173
3286
  }
3174
3287
  if (event === "Events.BootError" /* BootError */) {
3288
+ const error = new NotebookUnavailableError(
3289
+ data?.message || "Notebook is no longer available",
3290
+ data || {}
3291
+ );
3175
3292
  this.log("error", "Boot error received", { data });
3293
+ this.terminate(error, 4e3, error.message);
3176
3294
  return;
3177
3295
  }
3178
3296
  if (event === "response" /* Response */) {
@@ -3231,6 +3349,11 @@ var Transport = class {
3231
3349
  listen(event, listener, _context) {
3232
3350
  return this.eventEmitter.listen(event, listener);
3233
3351
  }
3352
+ onDidBootError(listener) {
3353
+ this.eventEmitter.listen("transport.boot_error", (event) => {
3354
+ listener(event.error);
3355
+ });
3356
+ }
3234
3357
  removeListener(event, listener) {
3235
3358
  this.eventEmitter.removeListener(event, listener);
3236
3359
  }
@@ -3257,6 +3380,9 @@ var Transport = class {
3257
3380
  get isClosed() {
3258
3381
  return this.closed;
3259
3382
  }
3383
+ getTerminalError() {
3384
+ return this.terminalError;
3385
+ }
3260
3386
  get status() {
3261
3387
  return {
3262
3388
  0: "CONNECTING",
@@ -3266,6 +3392,9 @@ var Transport = class {
3266
3392
  }[this.rws.readyState];
3267
3393
  }
3268
3394
  async call(action, data = {}, options = {}) {
3395
+ if (this.terminalError) {
3396
+ throw this.terminalError;
3397
+ }
3269
3398
  if (this.isRateLimited()) {
3270
3399
  throw new RateLimitError("Rate limit exceeded - too many requests");
3271
3400
  }
@@ -3282,6 +3411,10 @@ var Transport = class {
3282
3411
  this.eventEmitter.removeListener(errorEvent);
3283
3412
  };
3284
3413
  const handler = async (resolve, reject) => {
3414
+ if (this.terminalError) {
3415
+ reject(this.terminalError);
3416
+ return;
3417
+ }
3285
3418
  const abortError = new DOMException("Request aborted", "AbortError");
3286
3419
  if (options.abortSignal?.aborted) {
3287
3420
  reject(abortError);
@@ -3333,7 +3466,7 @@ var Transport = class {
3333
3466
  reject(new RateLimitError(_ev.reason || "Rate limit exceeded", _ev));
3334
3467
  return;
3335
3468
  }
3336
- reject(new Error(`Connection lost to the notebook during request: ${_ev.reason || "Unknown reason"}`));
3469
+ reject(this.terminalError || new Error(`Connection lost to the notebook during request: ${_ev.reason || "Unknown reason"}`));
3337
3470
  };
3338
3471
  this.rws.addEventListener("close", closeHandler);
3339
3472
  this.rws.addEventListener("error", closeHandler);
@@ -3369,7 +3502,7 @@ var Transport = class {
3369
3502
  try {
3370
3503
  return await sender();
3371
3504
  } catch (e) {
3372
- if (e instanceof ErrorEvent || e instanceof RateLimitError || e instanceof InvalidConfigurationError || e instanceof InvalidMessageError || e instanceof DOMException) {
3505
+ if (e instanceof ErrorEvent || e instanceof RateLimitError || e instanceof InvalidConfigurationError || e instanceof InvalidMessageError || e instanceof NotebookUnavailableError || e instanceof DOMException) {
3373
3506
  this.log("debug", "Non-retryable error, bailing", {
3374
3507
  error: e.message,
3375
3508
  attempt
@@ -3421,6 +3554,9 @@ var Transport = class {
3421
3554
  * This preserves all event listeners and state.
3422
3555
  */
3423
3556
  reconnect() {
3557
+ if (this.terminalError) {
3558
+ throw this.terminalError;
3559
+ }
3424
3560
  if (this.closed) {
3425
3561
  throw new Error("Cannot reconnect a closed transport. The transport has been permanently closed.");
3426
3562
  }
@@ -3435,6 +3571,16 @@ var Transport = class {
3435
3571
  }
3436
3572
  this.close();
3437
3573
  }
3574
+ terminate(error, code = 4e3, reason = error.message) {
3575
+ this.terminalError = error;
3576
+ if (error instanceof NotebookUnavailableError) {
3577
+ this.eventEmitter.emit("transport.boot_error", {
3578
+ error,
3579
+ timestamp: Date.now()
3580
+ });
3581
+ }
3582
+ this.close(code, reason);
3583
+ }
3438
3584
  close(code, reason) {
3439
3585
  if (this.closed) {
3440
3586
  return;
@@ -3442,9 +3588,9 @@ var Transport = class {
3442
3588
  this.log("info", "Closing transport connection", { code, reason });
3443
3589
  this.connectPromise = null;
3444
3590
  const queuedCount = this.messageQueue.length;
3591
+ const closeError = this.terminalError || new Error("Connection closed while message was queued");
3445
3592
  this.messageQueue.forEach((msg) => {
3446
- console.log("Rejecting queued message due to connection close");
3447
- msg.reject(new Error("Connection closed while message was queued"));
3593
+ msg.reject(closeError);
3448
3594
  });
3449
3595
  this.messageQueue = [];
3450
3596
  if (queuedCount > 0) {
@@ -3532,6 +3678,9 @@ var Transport = class {
3532
3678
  this.log("error", "Connection closed due to policy violation");
3533
3679
  this.clearOldQueuedMessages();
3534
3680
  return "stop";
3681
+ case 4e3:
3682
+ this.log("info", "Connection closed due to terminal notebook error");
3683
+ return "stop";
3535
3684
  default:
3536
3685
  this.log("warn", `Unknown close code: ${code}, will reconnect`);
3537
3686
  return "reconnect";
@@ -3747,6 +3896,9 @@ _Transport_instances = new WeakSet();
3747
3896
  * by caching the connection promise.
3748
3897
  */
3749
3898
  connect_fn = function() {
3899
+ if (this.terminalError) {
3900
+ return Promise.reject(this.terminalError);
3901
+ }
3750
3902
  if (this.isConnected) {
3751
3903
  return Promise.resolve();
3752
3904
  }
@@ -4793,9 +4945,14 @@ var NotebookInstance = class {
4793
4945
  this.laravel = new Lravel(this);
4794
4946
  this.shell = new Shell(this);
4795
4947
  this.git = new Git(this);
4948
+ this.services = new Services(this);
4796
4949
  this.secrets = new NotebookSecrets(client, this.data.id);
4797
4950
  }
4798
4951
  async ready() {
4952
+ const terminalError = this.socket.getTerminalError();
4953
+ if (terminalError) {
4954
+ throw terminalError;
4955
+ }
4799
4956
  const ready = async () => {
4800
4957
  if (this.client.options.startClosed && !this.socket.isConnected) {
4801
4958
  await this.socket.connect();
@@ -4808,7 +4965,9 @@ var NotebookInstance = class {
4808
4965
  return this.client.notebook.fork(this.data.id);
4809
4966
  }
4810
4967
  delete() {
4811
- return this.client.notebook.delete(this.data.id);
4968
+ return this.client.notebook.delete(this.data.id).then(() => {
4969
+ this.socket.terminate(new NotebookUnavailableError("Notebook has been deleted.", { id: this.data.id }));
4970
+ });
4812
4971
  }
4813
4972
  stop() {
4814
4973
  return this.container.stop();
@@ -4832,6 +4991,10 @@ var NotebookInstance = class {
4832
4991
  !this.socket.isClosed && this.socket.disconnect();
4833
4992
  }
4834
4993
  connected() {
4994
+ const terminalError = this.socket.getTerminalError();
4995
+ if (terminalError) {
4996
+ return Promise.reject(terminalError);
4997
+ }
4835
4998
  if (this.socket.isConnected) {
4836
4999
  return Promise.resolve(this);
4837
5000
  }
@@ -4839,18 +5002,24 @@ var NotebookInstance = class {
4839
5002
  try {
4840
5003
  this.socket.onDidConnect(() => resolve(this));
4841
5004
  this.socket.onDidClose(() => reject(new Error("Connection closed")));
5005
+ this.socket.onDidBootError((error) => reject(error));
4842
5006
  } catch (e) {
4843
5007
  reject(e);
4844
5008
  }
4845
5009
  });
4846
5010
  }
4847
5011
  whenConnected() {
5012
+ const terminalError = this.socket.getTerminalError();
5013
+ if (terminalError) {
5014
+ return Promise.reject(terminalError);
5015
+ }
4848
5016
  if (this.socket.isConnected) {
4849
5017
  return Promise.resolve(this);
4850
5018
  }
4851
5019
  return new Promise((resolve, reject) => {
4852
5020
  try {
4853
5021
  this.socket.onDidConnect(() => resolve(this));
5022
+ this.socket.onDidBootError((error) => reject(error));
4854
5023
  } catch (e) {
4855
5024
  reject(e);
4856
5025
  }
@@ -4867,6 +5036,11 @@ var NotebookInstance = class {
4867
5036
  this.socket.emit("okra.disconnected");
4868
5037
  this.initialized = false;
4869
5038
  });
5039
+ this.socket.onDidBootError((error) => {
5040
+ this.socket.emit("okra.boot_error", error);
5041
+ this.socket.emit("okra.disconnected");
5042
+ this.initialized = false;
5043
+ });
4870
5044
  }
4871
5045
  onDidConnect(handler) {
4872
5046
  this.socket.removeListener("okra.connected", handler);
@@ -4879,6 +5053,11 @@ var NotebookInstance = class {
4879
5053
  this.disposables.push(disposable);
4880
5054
  return disposable;
4881
5055
  }
5056
+ onDidBootError(handler) {
5057
+ const disposable = this.socket.listen("okra.boot_error", handler);
5058
+ this.disposables.push(disposable);
5059
+ return disposable;
5060
+ }
4882
5061
  update() {
4883
5062
  return this.invoke("notebook.update");
4884
5063
  }
@@ -4904,6 +5083,7 @@ _NotebookInstance_instances = new WeakSet();
4904
5083
  init_fn = function() {
4905
5084
  __privateSet(this, _initPromise, new Promise((resolve, reject) => {
4906
5085
  const initializationListener = this.onDidInitialize((result) => {
5086
+ bootErrorListener.dispose();
4907
5087
  initializationListener.dispose();
4908
5088
  this.initialized = result;
4909
5089
  if (result.type === "error") {
@@ -4912,6 +5092,11 @@ init_fn = function() {
4912
5092
  }
4913
5093
  resolve(result);
4914
5094
  });
5095
+ const bootErrorListener = this.onDidBootError((error) => {
5096
+ initializationListener.dispose();
5097
+ bootErrorListener.dispose();
5098
+ reject(error);
5099
+ });
4915
5100
  }));
4916
5101
  return __privateGet(this, _initPromise);
4917
5102
  };
@@ -4955,6 +5140,7 @@ export {
4955
5140
  NotebookSecretApi,
4956
5141
  NotebookSecrets,
4957
5142
  NotebookState,
5143
+ NotebookUnavailableError,
4958
5144
  PHPSandbox,
4959
5145
  PHPSandboxError,
4960
5146
  PromiseTimeoutError,
@@ -4963,6 +5149,8 @@ export {
4963
5149
  Transport,
4964
5150
  createBeacon,
4965
5151
  isBeaconSupported,
5152
+ notebookBuiltinServices,
5153
+ notebookKnownServices,
4966
5154
  once,
4967
5155
  timeout
4968
5156
  };