@phpsandbox/sdk 0.3.2 → 0.3.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.
Files changed (43) hide show
  1. package/README.md +38 -0
  2. package/dist/beacon/index.js +4 -2
  3. package/dist/beacon/navigator.js +0 -1
  4. package/dist/beacon/types.d.ts +1 -1
  5. package/dist/browser/phpsandbox-sdk.esm.js +405 -308
  6. package/dist/browser/phpsandbox-sdk.esm.min.js +23 -25
  7. package/dist/browser/phpsandbox-sdk.iife.js +405 -308
  8. package/dist/browser/phpsandbox-sdk.iife.min.js +8 -10
  9. package/dist/composer.js +8 -4
  10. package/dist/errors/index.d.ts +1 -1
  11. package/dist/errors/index.js +1 -3
  12. package/dist/feedback.js +2 -1
  13. package/dist/filesystem.js +32 -15
  14. package/dist/identity.d.ts +13 -0
  15. package/dist/identity.js +6 -0
  16. package/dist/index.d.ts +5 -4
  17. package/dist/index.js +14 -5
  18. package/dist/integrations/sandbox.js +1 -1
  19. package/dist/lsp.d.ts +2 -2
  20. package/dist/lsp.js +2 -2
  21. package/dist/publication/index.d.ts +1 -0
  22. package/dist/publication/index.js +1 -0
  23. package/dist/publication/instance.d.ts +2 -0
  24. package/dist/publication/instance.js +8 -0
  25. package/dist/publication/notebook.js +7 -5
  26. package/dist/publication/resources.js +2 -1
  27. package/dist/publication/run.js +2 -76
  28. package/dist/publication/stream.d.ts +18 -0
  29. package/dist/publication/stream.js +185 -0
  30. package/dist/runtime/rest.js +2 -1
  31. package/dist/runtime.js +1 -1
  32. package/dist/schemas/publications.js +53 -59
  33. package/dist/servers.d.ts +2 -0
  34. package/dist/servers.js +5 -1
  35. package/dist/shell.js +5 -2
  36. package/dist/socket/index.js +25 -13
  37. package/dist/telemetry.js +8 -3
  38. package/dist/terminal.js +3 -4
  39. package/dist/utils/promise.js +1 -1
  40. package/dist/utils/websocket.js +2 -2
  41. package/package.json +49 -33
  42. package/dist/auth.d.ts +0 -11
  43. package/dist/auth.js +0 -12
package/dist/composer.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ProcessReadable, } from './shell.js';
1
+ import { ProcessReadable } from './shell.js';
2
2
  import { nanoid } from 'nanoid';
3
3
  class ComposerCredentials {
4
4
  constructor(client, notebookId) {
@@ -11,8 +11,9 @@ class ComposerCredentials {
11
11
  }
12
12
  async remove(type, url) {
13
13
  const params = new URLSearchParams({ type });
14
- if (url)
14
+ if (url) {
15
15
  params.set('url', url);
16
+ }
16
17
  await this.client.delete(`/notebook/${this.notebookId}/composer/credentials?${params.toString()}`);
17
18
  }
18
19
  }
@@ -52,10 +53,13 @@ export class ComposerProcess {
52
53
  stdout.fail(error);
53
54
  stderr.fail(error);
54
55
  };
55
- this.completion = this.okra.invoke('composer.invoke', { id, command, args, options }).then((result) => {
56
+ this.completion = this.okra
57
+ .invoke('composer.invoke', { id, command, args, options })
58
+ .then((result) => {
56
59
  finish();
57
60
  return result;
58
- }).catch((error) => {
61
+ })
62
+ .catch((error) => {
59
63
  fail(error);
60
64
  throw error;
61
65
  });
@@ -1,6 +1,6 @@
1
1
  export type RemoteErrorSource = 'core' | 'runtime';
2
2
  declare const remoteErrorCodes: readonly ["ActionNotFound", "AuthenticationRequired", "BadGateway", "BadRequest", "Conflict", "FileAlreadyExists", "FileIsDirectory", "FileNotDirectory", "FileNotFound", "FilesystemUnavailable", "InternalError", "InvalidArgument", "InvalidConfiguration", "InvalidRequest", "InvalidResponse", "InvalidServiceCommand", "MethodNotAllowed", "NotebookInitializationFailed", "NotebookUnavailable", "NotFound", "PayloadTooLarge", "PermissionDenied", "RateLimited", "RequestFailed", "RuntimeOperationFailed", "ServiceNotFound", "ServiceUnavailable", "UnprocessableEntity", "ValidationFailed", "WorkspaceUnavailable"];
3
- export type RemoteErrorCode = typeof remoteErrorCodes[number];
3
+ export type RemoteErrorCode = (typeof remoteErrorCodes)[number];
4
4
  export type TransportErrorCode = 'ConnectionFailed' | 'ConnectionTimeout' | 'InvalidResponse' | 'SendTimeout';
5
5
  export interface ValidationErrorDetails {
6
6
  errors: Record<string, string[]>;
@@ -94,9 +94,7 @@ function nonEmptyString(value) {
94
94
  return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
95
95
  }
96
96
  function record(value) {
97
- return value !== null && typeof value === 'object' && !Array.isArray(value)
98
- ? value
99
- : undefined;
97
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
100
98
  }
101
99
  function stringArrayRecord(value) {
102
100
  const candidate = record(value);
package/dist/feedback.js CHANGED
@@ -18,8 +18,9 @@ export class Feedback {
18
18
  async list(options = {}) {
19
19
  const query = new URLSearchParams();
20
20
  for (const [name, value] of Object.entries(options)) {
21
- if (value !== undefined)
21
+ if (value !== undefined) {
22
22
  query.set(name, String(value));
23
+ }
23
24
  }
24
25
  return this.client.get(`${this.path()}${query.size === 0 ? '' : `?${query.toString()}`}`);
25
26
  }
@@ -80,11 +80,13 @@ export class Filesystem {
80
80
  const source = typeof contents === 'string' ? new TextEncoder().encode(contents) : contents;
81
81
  if (this.okra.runtimeTransport === 'rest') {
82
82
  const bytes = source instanceof Uint8Array ? source : await collectBytes(source);
83
- await this.okra.invoke('fs.writeFile', {
83
+ await this.okra
84
+ .invoke('fs.writeFile', {
84
85
  path,
85
86
  contents: bytes,
86
87
  options: resolvedOptions,
87
- }).catch((error) => this.handleError(error));
88
+ })
89
+ .catch((error) => this.handleError(error));
88
90
  return;
89
91
  }
90
92
  const id = nanoid();
@@ -166,13 +168,16 @@ export class Filesystem {
166
168
  disposable = this.okra.listen(`fs.read.${id}`, (event) => {
167
169
  controller.enqueue(event.data);
168
170
  });
169
- void this.okra.invoke('fs.readStream', { id, path }).then(() => {
171
+ void this.okra
172
+ .invoke('fs.readStream', { id, path })
173
+ .then(() => {
170
174
  if (!closed) {
171
175
  closed = true;
172
176
  disposable?.dispose();
173
177
  controller.close();
174
178
  }
175
- }).catch((error) => {
179
+ })
180
+ .catch((error) => {
176
181
  if (!closed) {
177
182
  closed = true;
178
183
  disposable?.dispose();
@@ -235,7 +240,9 @@ export class Filesystem {
235
240
  options.signal.addEventListener('abort', abort, { once: true });
236
241
  }
237
242
  started = true;
238
- void this.okra.invoke('fs.follow', { id, path, lines: options.lines ?? 10 }).then(() => {
243
+ void this.okra
244
+ .invoke('fs.follow', { id, path, lines: options.lines ?? 10 })
245
+ .then(() => {
239
246
  if (!closed) {
240
247
  closed = true;
241
248
  disposable?.dispose();
@@ -245,7 +252,8 @@ export class Filesystem {
245
252
  abort = null;
246
253
  controller.close();
247
254
  }
248
- }).catch((error) => {
255
+ })
256
+ .catch((error) => {
249
257
  if (!closed) {
250
258
  closed = true;
251
259
  disposable?.dispose();
@@ -340,7 +348,8 @@ export class Filesystem {
340
348
  .finally(() => disposable.dispose());
341
349
  }
342
350
  tail(path, options = {}) {
343
- return this.okra.invoke('fs.tail', { path, lines: options.lines ?? 10 })
351
+ return this.okra
352
+ .invoke('fs.tail', { path, lines: options.lines ?? 10 })
344
353
  .then(({ content }) => content)
345
354
  .catch((e) => this.handleError(e));
346
355
  }
@@ -348,31 +357,39 @@ export class Filesystem {
348
357
  return this.okra.invoke('fs.stat', { path }).catch((e) => this.handleError(e));
349
358
  }
350
359
  move(from, to, options = {}) {
351
- return this.okra.invoke('fs.rename', {
360
+ return this.okra
361
+ .invoke('fs.rename', {
352
362
  from,
353
363
  to,
354
364
  options: { overwrite: false, ...options },
355
- }).catch((e) => this.handleError(e));
365
+ })
366
+ .catch((e) => this.handleError(e));
356
367
  }
357
368
  remove(path, options = {}) {
358
- return this.okra.invoke('fs.delete', {
369
+ return this.okra
370
+ .invoke('fs.delete', {
359
371
  path,
360
372
  options: { recursive: false, useTrash: false, atomic: false, ...options },
361
- }).catch((e) => this.handleError(e));
373
+ })
374
+ .catch((e) => this.handleError(e));
362
375
  }
363
376
  copy(source, destination, options = {}) {
364
- return this.okra.invoke('fs.copy', {
377
+ return this.okra
378
+ .invoke('fs.copy', {
365
379
  source,
366
380
  destination,
367
381
  options: { overwrite: false, ...options },
368
- }).catch((e) => this.handleError(e));
382
+ })
383
+ .catch((e) => this.handleError(e));
369
384
  }
370
385
  list(path, options = {}) {
371
- return this.okra.invoke('fs.readDirectory', {
386
+ return this.okra
387
+ .invoke('fs.readDirectory', {
372
388
  path,
373
389
  include: options.include ?? [],
374
390
  exclude: options.exclude ?? [],
375
- }).catch((e) => this.handleError(e));
391
+ })
392
+ .catch((e) => this.handleError(e));
376
393
  }
377
394
  createDirectory(path) {
378
395
  return this.okra.invoke('fs.createDirectory', { path }).catch((e) => this.handleError(e));
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ export declare const identitySchema: z.ZodObject<{
3
+ user: z.ZodNullable<z.ZodObject<{
4
+ id: z.ZodString;
5
+ name: z.ZodString;
6
+ }, z.core.$strip>>;
7
+ organization: z.ZodObject<{
8
+ id: z.ZodString;
9
+ name: z.ZodString;
10
+ }, z.core.$strip>;
11
+ permissions: z.ZodArray<z.ZodString>;
12
+ }, z.core.$strip>;
13
+ export type Identity = z.infer<typeof identitySchema>;
@@ -0,0 +1,6 @@
1
+ import { z } from 'zod';
2
+ export const identitySchema = z.object({
3
+ user: z.object({ id: z.string(), name: z.string() }).nullable(),
4
+ organization: z.object({ id: z.string(), name: z.string() }),
5
+ permissions: z.array(z.string()),
6
+ });
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import { type Identity } from './identity.js';
2
+ export type { Identity } from './identity.js';
1
3
  import { NotebookPublication } from './publication/index.js';
2
4
  import { Filesystem, FilesystemActions, FilesystemEvents } from './filesystem.js';
3
5
  import Terminals, { TerminalEvents, TerminalActions, type SpawnOptions } from './terminal.js';
4
- import Auth, { AuthActions } from './auth.js';
5
6
  import Lsp, { LspActions, LspEvents } from './lsp.js';
6
7
  import Composer, { ComposerActions, ComposerEvents } from './composer.js';
7
8
  import { LogEvents } from './log.js';
@@ -34,7 +35,7 @@ export type { ConfigPortMapping, ProjectConfig } from './config.js';
34
35
  export { PHPSandboxError, RemoteError, TransportError } from './errors/index.js';
35
36
  export type { HttpResponseDiagnostics, RemoteErrorCode, RemoteErrorSource, TransportErrorCode, ValidationErrorDetails, } from './errors/index.js';
36
37
  export * from './publication/index.js';
37
- export type { CreateServerInput, ServerData, ServerInstance, ServerListOptions, ServerMemberData, ServerSshOptions } from './servers.js';
38
+ export type { CreateServerInput, ServerData, ServerInstance, ServerListOptions, ServerMemberData, ServerSshOptions, } from './servers.js';
38
39
  export type { ConfigureFeedbackInput, FeedbackData, FeedbackListOptions, FeedbackSource, FeedbackStatus, FeedbackType, FeedbackWidgetConfig, FeedbackWidgetTarget, SubmitFeedbackInput, UpdateFeedbackInput, } from './feedback.js';
39
40
  export type { Integration, IntegrationAuthorization, IntegrationAuthorizationType, IntegrationData, IntegrationProvider, IntegrationReference, LinkIntegrationInput, UpdateIntegrationInput, AttachSandboxIntegrationInput, SandboxIntegration, SandboxIntegrationData, SandboxIntegrationEffect, SandboxIntegrationReference, UpdateSandboxIntegrationInput, } from './integrations/index.js';
40
41
  interface Result<TType extends 'success' | 'error' | 'running', TData extends object> {
@@ -108,7 +109,7 @@ export interface Action<Args = object, Response = void> {
108
109
  response: Response;
109
110
  }
110
111
  export type Events = TerminalEvents & RuntimeEvents & LspEvents & ComposerEvents & LogEvents & ServiceEvents & NotebookEvents & ReplEvents & ShellEvents & FilesystemEvents;
111
- export type Invokable = TerminalActions & RuntimeActions & FilesystemActions & AuthActions & LspActions & ComposerActions & ServiceActions & ConfigActions & ReplActions & ShellActions & GitActions;
112
+ export type Invokable = TerminalActions & RuntimeActions & FilesystemActions & LspActions & ComposerActions & ServiceActions & ConfigActions & ReplActions & ShellActions & GitActions;
112
113
  export interface CreateNotebookImportAuthInput {
113
114
  accessToken: string;
114
115
  }
@@ -364,6 +365,7 @@ export declare class PHPSandbox {
364
365
  readonly servers: ServerApi;
365
366
  readonly integrations: IntegrationApi;
366
367
  private constructor();
368
+ whoami(): Promise<Identity>;
367
369
  get runtimeTransport(): RuntimeTransport;
368
370
  static realtime(token: string, url?: string, options?: PHPSandboxClientOptions): PHPSandbox;
369
371
  static rest(token: string, url?: string, options?: PHPSandboxClientOptions): PHPSandbox;
@@ -387,7 +389,6 @@ export declare class NotebookInstance {
387
389
  protected client: Client;
388
390
  readonly files: Filesystem;
389
391
  readonly terminals: Terminals;
390
- readonly auth: Auth;
391
392
  readonly lsp: Lsp;
392
393
  readonly composer: Composer;
393
394
  readonly runtime: Runtime;
package/dist/index.js CHANGED
@@ -10,10 +10,10 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
10
10
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
11
  };
12
12
  var _NotebookInstance_instances, _NotebookInstance_initPromise, _NotebookInstance_init;
13
+ import { identitySchema } from './identity.js';
13
14
  import { NotebookPublication } from './publication/index.js';
14
15
  import { Filesystem } from './filesystem.js';
15
16
  import Terminals from './terminal.js';
16
- import Auth from './auth.js';
17
17
  import Lsp from './lsp.js';
18
18
  import Composer from './composer.js';
19
19
  import Runtime from './runtime.js';
@@ -221,6 +221,14 @@ export class PHPSandbox {
221
221
  this.servers = client.servers;
222
222
  this.integrations = client.integrations;
223
223
  }
224
+ async whoami() {
225
+ const { data } = await this.client.get('/auth/whoami');
226
+ const result = identitySchema.safeParse(data);
227
+ if (!result.success) {
228
+ throw new PHPSandboxError('Invalid identity response.');
229
+ }
230
+ return result.data;
231
+ }
224
232
  get runtimeTransport() {
225
233
  return this.client.runtimeTransport;
226
234
  }
@@ -269,7 +277,6 @@ export class NotebookInstance {
269
277
  }
270
278
  this.files = new Filesystem(this, this.socket === null ? null : (handler) => this.onDidConnect(handler));
271
279
  this.terminals = new Terminals(this);
272
- this.auth = new Auth(this);
273
280
  this.lsp = new Lsp(this);
274
281
  this.composer = new Composer(this, client);
275
282
  this.runtime = new Runtime(this);
@@ -476,7 +483,7 @@ export class NotebookInstance {
476
483
  // Wait for the socket to reconnect
477
484
  await whenConnected;
478
485
  // Re-initialize the notebook
479
- __classPrivateFieldGet(this, _NotebookInstance_instances, "m", _NotebookInstance_init).call(this);
486
+ __classPrivateFieldSet(this, _NotebookInstance_initPromise, __classPrivateFieldGet(this, _NotebookInstance_instances, "m", _NotebookInstance_init).call(this), "f");
480
487
  // Wait for initialization to complete
481
488
  return this.ready().then(() => this);
482
489
  }
@@ -524,8 +531,9 @@ class NotebookSecrets {
524
531
  }
525
532
  async list(options) {
526
533
  const params = new URLSearchParams();
527
- if (options?.environment)
534
+ if (options?.environment) {
528
535
  params.set('environment', options.environment);
536
+ }
529
537
  const query = params.toString();
530
538
  const response = await this.client.get(`/notebook/${this.notebookId}/secrets${query ? `?${query}` : ''}`);
531
539
  return response.data;
@@ -543,8 +551,9 @@ class NotebookSecrets {
543
551
  }
544
552
  async delete(name, options) {
545
553
  const params = new URLSearchParams();
546
- if (options?.environment)
554
+ if (options?.environment) {
547
555
  params.set('environment', options.environment);
556
+ }
548
557
  const query = params.toString();
549
558
  await this.client.delete(`/notebook/${this.notebookId}/secrets/${encodeURIComponent(name)}${query ? `?${query}` : ''}`);
550
559
  }
@@ -1,4 +1,4 @@
1
- import { Integration, integrationId, } from './account.js';
1
+ import { Integration, integrationId } from './account.js';
2
2
  export class SandboxIntegrationApi {
3
3
  constructor(notebookId, client) {
4
4
  this.notebookId = notebookId;
package/dist/lsp.d.ts CHANGED
@@ -47,8 +47,8 @@ export declare class LspConnection {
47
47
  onMessage(cb: (data: string) => void): Disposable;
48
48
  onError(cb: (message: string) => void): Disposable;
49
49
  onClose(cb: (code: number, reason: string) => void): Disposable;
50
- dispose(): void;
51
- start(): void;
50
+ dispose(): Promise<void>;
51
+ start(): Promise<void>;
52
52
  onClientDisconnect(cb: (code: number, message: string) => void): Disposable;
53
53
  whenSocketConnected(): Promise<NotebookInstance>;
54
54
  static create(id: string, lsp: Lsp): LspConnection;
package/dist/lsp.js CHANGED
@@ -51,10 +51,10 @@ export class LspConnection {
51
51
  return this.lsp.onClose(this.id, cb);
52
52
  }
53
53
  dispose() {
54
- this.lsp.close(this.id);
54
+ return this.lsp.close(this.id);
55
55
  }
56
56
  start() {
57
- this.lsp.start(this.id);
57
+ return this.lsp.start(this.id);
58
58
  }
59
59
  onClientDisconnect(cb) {
60
60
  return this.lsp.onClientDisconnect(cb);
@@ -6,3 +6,4 @@ export type { BuiltInPublicationProviderName, PublicationStatus, PublicationBuil
6
6
  export { laravelCloudSetupForRepublish } from './providers/laravel-cloud.js';
7
7
  export { PublicationResources } from './resources.js';
8
8
  export { PublicationDomains } from './domains.js';
9
+ export { readPublicationStream, createPublicationStreamResponse } from './stream.js';
@@ -4,3 +4,4 @@ export { PublicationRun } from './run.js';
4
4
  export { laravelCloudSetupForRepublish } from './providers/laravel-cloud.js';
5
5
  export { PublicationResources } from './resources.js';
6
6
  export { PublicationDomains } from './domains.js';
7
+ export { readPublicationStream, createPublicationStreamResponse } from './stream.js';
@@ -21,6 +21,8 @@ export declare class PublicationInstance<TProvider extends PublicationProviderNa
21
21
  refresh(): Promise<PublicationInstance<TProvider>>;
22
22
  /** Start or reattach to the current release stream without queuing another release. */
23
23
  follow(): PublicationRun<TProvider>;
24
+ /** Open the original publishing SSE bytes for an authenticated, transparent proxy. */
25
+ stream(signal?: AbortSignal): Promise<ReadableStream<Uint8Array>>;
24
26
  destroy(options?: PublicationDestroyOptions): Promise<void>;
25
27
  wait(options?: {
26
28
  intervalMs?: number;
@@ -36,6 +36,13 @@ export class PublicationInstance {
36
36
  follow() {
37
37
  return new PublicationRun(this, this.client).start();
38
38
  }
39
+ /** Open the original publishing SSE bytes for an authenticated, transparent proxy. */
40
+ async stream(signal) {
41
+ if (!this.data.eventStreamUrl) {
42
+ throw new PHPSandboxError(`Publication ${this.data.id} has no eventStreamUrl for SSE streaming`);
43
+ }
44
+ return this.client.sse(this.data.eventStreamUrl, signal);
45
+ }
39
46
  async destroy(options = {}) {
40
47
  const query = options.deleteResources ? '?deleteResources=1' : '';
41
48
  await this.client.delete(`/publications/${encodeURIComponent(this.data.id)}${query}`);
@@ -44,6 +51,7 @@ export class PublicationInstance {
44
51
  const intervalMs = options.intervalMs ?? 1000;
45
52
  const timeoutMs = options.timeoutMs ?? 300000;
46
53
  const startedAt = Date.now();
54
+ // eslint-disable-next-line typescript/no-this-alias -- Polling starts with this instance and replaces it after each refresh.
47
55
  let current = this;
48
56
  while (!publicationStatusIsTerminal(current.data.status)) {
49
57
  if (Date.now() - startedAt > timeoutMs) {
@@ -41,8 +41,9 @@ export class NotebookPublication {
41
41
  /** Publish the exact app selection in a server-owned review. */
42
42
  async publish(input) {
43
43
  const application = (await this.client.post(`${this.path}/publication`, input)).data;
44
- if (!('confirmationRequired' in application))
44
+ if (!('confirmationRequired' in application)) {
45
45
  this.startApplicationRuns(application);
46
+ }
46
47
  return application;
47
48
  }
48
49
  async application(applicationId) {
@@ -60,10 +61,12 @@ export class NotebookPublication {
60
61
  }
61
62
  startApplicationRuns(application) {
62
63
  for (const app of application.apps) {
63
- if (!(application.selectedAppIds ?? []).includes(app.appId))
64
+ if (!(application.selectedAppIds ?? []).includes(app.appId)) {
64
65
  continue;
65
- if (!['pending', 'queued', 'building', 'deploying'].includes(app.publication.status))
66
+ }
67
+ if (!['pending', 'queued', 'building', 'deploying'].includes(app.publication.status)) {
66
68
  continue;
69
+ }
67
70
  new PublicationRun(new PublicationInstance(app.publication, this.client, this.notebookId), this.client).start();
68
71
  }
69
72
  }
@@ -72,7 +75,6 @@ export class NotebookPublication {
72
75
  if (serverId !== undefined) {
73
76
  query.set('serverId', serverId);
74
77
  }
75
- return (await this.client.get(`${this.path}/publication/catalog?${query}`))
76
- .data;
78
+ return (await this.client.get(`${this.path}/publication/catalog?${query}`)).data;
77
79
  }
78
80
  }
@@ -4,7 +4,8 @@ export class PublicationResources {
4
4
  this.path = path;
5
5
  }
6
6
  async inspect(id) {
7
- return (await this.client.post(`/publication-resources/${encodeURIComponent(id)}/inspect`, {})).data;
7
+ return (await this.client.post(`/publication-resources/${encodeURIComponent(id)}/inspect`, {}))
8
+ .data;
8
9
  }
9
10
  async list(options = {}) {
10
11
  const query = new URLSearchParams();
@@ -1,4 +1,4 @@
1
- import { PublishStreamSseEventNameSchema, PublishStreamSseEventSchema } from '../schemas/publications.js';
1
+ import { readPublicationStream } from './stream.js';
2
2
  import { PHPSandboxError } from '../errors/index.js';
3
3
  export class PublicationRun {
4
4
  constructor(initial, client) {
@@ -89,7 +89,7 @@ export class PublicationRun {
89
89
  async consumeStream(eventStreamUrl) {
90
90
  try {
91
91
  const stream = await this.client.sse(eventStreamUrl, this.streamAbort.signal);
92
- for await (const event of readPublishStreamEvents(stream)) {
92
+ for await (const event of readPublicationStream(stream)) {
93
93
  this.publish(event);
94
94
  if (event.type === 'result') {
95
95
  const { type: _, ...result } = event;
@@ -191,77 +191,3 @@ class AsyncPublishEventQueue {
191
191
  });
192
192
  }
193
193
  }
194
- async function* readPublishStreamEvents(stream) {
195
- const reader = parseSSEStream(stream).getReader();
196
- try {
197
- while (true) {
198
- const { done, value } = await reader.read();
199
- if (done) {
200
- return;
201
- }
202
- const event = parsePublishStreamEvent(value);
203
- if (event) {
204
- yield event;
205
- }
206
- }
207
- }
208
- finally {
209
- reader.releaseLock();
210
- }
211
- }
212
- function parsePublishStreamEvent(event) {
213
- if (!PublishStreamSseEventNameSchema.safeParse(event.event).success) {
214
- return null;
215
- }
216
- return PublishStreamSseEventSchema.parse(event);
217
- }
218
- function parseSSEStream(input) {
219
- const decoder = new TextDecoder();
220
- let buffer = '';
221
- return input.pipeThrough(new TransformStream({
222
- transform(chunk, controller) {
223
- buffer += decoder.decode(chunk, { stream: true });
224
- const blocks = buffer.split('\n\n');
225
- buffer = blocks.pop() ?? '';
226
- for (const block of blocks) {
227
- const event = parseSSEBlock(block);
228
- if (event) {
229
- controller.enqueue(event);
230
- }
231
- }
232
- },
233
- flush(controller) {
234
- buffer += decoder.decode();
235
- if (buffer.trim() !== '') {
236
- const event = parseSSEBlock(buffer);
237
- if (event) {
238
- controller.enqueue(event);
239
- }
240
- }
241
- },
242
- }));
243
- }
244
- function parseSSEBlock(block) {
245
- let eventType = 'message';
246
- let data = '';
247
- for (const line of block.split('\n')) {
248
- if (line.startsWith('event: ')) {
249
- eventType = line.slice(7).trim();
250
- }
251
- else if (line.startsWith('data: ')) {
252
- data += line.slice(6);
253
- }
254
- else if (line.startsWith('data:')) {
255
- data += line.slice(5);
256
- }
257
- }
258
- if (data === '') {
259
- return null;
260
- }
261
- try {
262
- return { event: eventType, data: JSON.parse(data) };
263
- }
264
- catch {
265
- return { event: eventType, data };
266
- }
267
- }
@@ -0,0 +1,18 @@
1
+ import type { PublishStreamEvent } from '../schemas/publications.js';
2
+ export type { PublishStreamEvent } from '../schemas/publications.js';
3
+ /** Consume publishing SSE in browsers or servers, including through an authenticated proxy.
4
+ * Cancels the source when iteration ends early or parsing fails. Fetch cancellation belongs
5
+ * to the caller's AbortSignal. Unknown event names and heartbeat comments are ignored.
6
+ * A complete stream must include a result; set requireResult to false only for fragments.
7
+ */
8
+ export declare function readPublicationStream(input: Response | ReadableStream<Uint8Array>, options?: {
9
+ requireResult?: boolean;
10
+ }): AsyncIterable<PublishStreamEvent>;
11
+ /** Forward the original bytes with SSE headers and exactly-once resource cleanup.
12
+ * Works with Fetch-based servers and frameworks that accept a standard Response.
13
+ * Does not parse, buffer a replay, or re-encode events. Upstream errors fail the body.
14
+ */
15
+ export declare function createPublicationStreamResponse(stream: ReadableStream<Uint8Array>, options?: {
16
+ signal?: AbortSignal;
17
+ onDispose?: () => void | Promise<void>;
18
+ }): Response;