@phpsandbox/sdk 0.3.1 → 0.3.3

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
@@ -418,3 +418,41 @@ Preparing or viewing a proposal does not start publishing. Confirmation sends th
418
418
  Inspect deployment, runtime and routing outcomes for each selected app; a partial outcome is not full success. `retryApplication(id)` retries failed apps after revalidation. If an outcome is uncertain, inspect status and logs before retrying. `destroyApplication(id)` unpublishes; deleting managed resources additionally requires `deleteResources: true`.
419
419
 
420
420
  Shared managed-resource creation currently starts with one app. After creation, update the resource definition to `reuse` with the catalog resource ID before attaching other apps. Simultaneous first-time shared-resource creation is unsupported.
421
+
422
+ #### Browser publishing streams through your backend
423
+
424
+ Keep service-account credentials on your server. After authorizing project access and checking the requested publishing attempt, open the original SSE bytes with `publication.stream()`. Return the SDK's standard `Response` directly from your framework:
425
+
426
+ ```ts
427
+ import { createPublicationStreamResponse } from '@phpsandbox/sdk/stream';
428
+
429
+ // Server: `publication` belongs to the authenticated user's project.
430
+ // `requestSignal` is aborted when the downstream client disconnects.
431
+ const bytes = await publication.stream(requestSignal);
432
+ return createPublicationStreamResponse(bytes, {
433
+ signal: requestSignal,
434
+ onDispose: () => notebook.dispose(),
435
+ });
436
+ ```
437
+
438
+ The helper forwards bytes unchanged and supplies SSE headers. It releases resources on completion, upstream failure, or downstream cancellation. Dispose resources yourself if opening the upstream stream fails. Opening this endpoint can start an already queued release; it is not a purely passive log subscription.
439
+
440
+ In the browser, use the lightweight `stream` entry point without constructing a client or exposing a PHPSandbox token. For example, if your backend exposes the proxy at `/api/publishing/stream`:
441
+
442
+ ```ts
443
+ import { readPublicationStream } from '@phpsandbox/sdk/stream';
444
+
445
+ const controller = new AbortController();
446
+ const response = await fetch('/api/publishing/stream', {
447
+ credentials: 'same-origin',
448
+ headers: { Accept: 'text/event-stream' },
449
+ signal: controller.signal,
450
+ });
451
+ for await (const event of readPublicationStream(response)) {
452
+ // Discriminated union: phase, log, or result. `done` is normalized to result.
453
+ if (event.type === 'log') console.log(event.content);
454
+ }
455
+ // Call controller.abort() when the view closes, including while waiting for an event.
456
+ ```
457
+
458
+ `readPublicationStream` also accepts a `ReadableStream<Uint8Array>`. By default, it rejects EOF without a final result (including empty streams). Use `{ requireResult: false }` only when deliberately consuming a stream fragment. It validates recognized event payloads, throws for server error events, and ignores unknown event names and heartbeat comments. Ending iteration early cancels the stream; aborting the fetch interrupts a pending read. Disconnecting observation does not cancel the server-side deployment. `PublicationRun.events()` uses this same parser.
@@ -14795,6 +14795,140 @@ var PublishStreamSseEventSchema = external_exports.discriminatedUnion("event", [
14795
14795
  return PublishStreamEventSchema.parse({ type, ...event.data });
14796
14796
  });
14797
14797
 
14798
+ // src/publication/stream.ts
14799
+ async function* readPublicationStream(input, options = {}) {
14800
+ if (input instanceof Response && (!input.ok || !input.body)) {
14801
+ await input.body?.cancel().catch(() => void 0);
14802
+ throw new PHPSandboxError(`Publishing stream is unavailable (HTTP ${input.status}).`);
14803
+ }
14804
+ const stream = input instanceof Response ? input.body : input;
14805
+ const reader = stream.getReader();
14806
+ const decoder = new TextDecoder();
14807
+ let buffer = "";
14808
+ let eventName = "message";
14809
+ let data = [];
14810
+ let ended = false;
14811
+ let receivedResult = false;
14812
+ function dispatch() {
14813
+ const name = eventName;
14814
+ const lines = data;
14815
+ eventName = "message";
14816
+ data = [];
14817
+ if (!lines.length || !PublishStreamSseEventNameSchema.safeParse(name).success) return null;
14818
+ const raw = lines.join("\n");
14819
+ let payload;
14820
+ try {
14821
+ payload = JSON.parse(raw);
14822
+ } catch {
14823
+ payload = raw;
14824
+ }
14825
+ const event = PublishStreamSseEventSchema.parse({ event: name, data: payload });
14826
+ if (event.type === "result") receivedResult = true;
14827
+ return event;
14828
+ }
14829
+ function line(value) {
14830
+ if (value === "") return dispatch();
14831
+ if (value.startsWith(":")) return null;
14832
+ const colon = value.indexOf(":");
14833
+ const field = colon < 0 ? value : value.slice(0, colon);
14834
+ let content = colon < 0 ? "" : value.slice(colon + 1);
14835
+ if (content.startsWith(" ")) content = content.slice(1);
14836
+ if (field === "event") eventName = content;
14837
+ if (field === "data") data.push(content);
14838
+ return null;
14839
+ }
14840
+ try {
14841
+ while (true) {
14842
+ const { done, value } = await reader.read();
14843
+ buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
14844
+ let match;
14845
+ while (match = /\r\n|\r|\n/.exec(buffer)) {
14846
+ if (!done && match[0] === "\r" && match.index === buffer.length - 1) break;
14847
+ const event = line(buffer.slice(0, match.index));
14848
+ buffer = buffer.slice(match.index + match[0].length);
14849
+ if (event) yield event;
14850
+ }
14851
+ if (done) {
14852
+ ended = true;
14853
+ if (buffer) {
14854
+ const event2 = line(buffer);
14855
+ if (event2) yield event2;
14856
+ }
14857
+ const event = dispatch();
14858
+ if (event) yield event;
14859
+ if (options.requireResult !== false && !receivedResult) {
14860
+ throw new PHPSandboxError("Publish stream ended without result");
14861
+ }
14862
+ return;
14863
+ }
14864
+ }
14865
+ } finally {
14866
+ if (!ended) await reader.cancel().catch(() => void 0);
14867
+ reader.releaseLock();
14868
+ }
14869
+ }
14870
+ function createPublicationStreamResponse(stream, options = {}) {
14871
+ const reader = stream.getReader();
14872
+ let disposed = false;
14873
+ let finished = false;
14874
+ let cancellation;
14875
+ let abort;
14876
+ async function dispose() {
14877
+ if (disposed) return;
14878
+ disposed = true;
14879
+ options.signal?.removeEventListener("abort", abort);
14880
+ reader.releaseLock();
14881
+ await options.onDispose?.();
14882
+ }
14883
+ function cancel(reason) {
14884
+ return cancellation ?? (cancellation = (async () => {
14885
+ finished = true;
14886
+ try {
14887
+ await reader.cancel(reason);
14888
+ } finally {
14889
+ await dispose();
14890
+ }
14891
+ })());
14892
+ }
14893
+ return new Response(new ReadableStream({
14894
+ start(controller) {
14895
+ abort = () => {
14896
+ controller.error(options.signal.reason);
14897
+ void cancel(options.signal.reason).catch(() => void 0);
14898
+ };
14899
+ if (options.signal?.aborted) abort();
14900
+ else options.signal?.addEventListener("abort", abort, { once: true });
14901
+ },
14902
+ async pull(controller) {
14903
+ try {
14904
+ const { done, value } = await reader.read();
14905
+ if (finished) return;
14906
+ if (done) {
14907
+ finished = true;
14908
+ await dispose();
14909
+ controller.close();
14910
+ } else {
14911
+ controller.enqueue(value);
14912
+ }
14913
+ } catch (error51) {
14914
+ if (finished && cancellation) return;
14915
+ try {
14916
+ await cancel(error51);
14917
+ } finally {
14918
+ controller.error(error51);
14919
+ }
14920
+ }
14921
+ },
14922
+ cancel
14923
+ }, { highWaterMark: 0 }), {
14924
+ headers: {
14925
+ "Content-Type": "text/event-stream",
14926
+ "Cache-Control": "no-cache, no-transform",
14927
+ "X-Accel-Buffering": "no"
14928
+ }
14929
+ });
14930
+ }
14931
+
14798
14932
  // src/publication/run.ts
14799
14933
  var PublicationRun = class {
14800
14934
  constructor(initial, client) {
@@ -14882,7 +15016,7 @@ var PublicationRun = class {
14882
15016
  async consumeStream(eventStreamUrl) {
14883
15017
  try {
14884
15018
  const stream = await this.client.sse(eventStreamUrl, this.streamAbort.signal);
14885
- for await (const event of readPublishStreamEvents(stream)) {
15019
+ for await (const event of readPublicationStream(stream)) {
14886
15020
  this.publish(event);
14887
15021
  if (event.type === "result") {
14888
15022
  const { type: _, ...result } = event;
@@ -14983,78 +15117,6 @@ var AsyncPublishEventQueue = class {
14983
15117
  });
14984
15118
  }
14985
15119
  };
14986
- async function* readPublishStreamEvents(stream) {
14987
- const reader = parseSSEStream(stream).getReader();
14988
- try {
14989
- while (true) {
14990
- const { done, value } = await reader.read();
14991
- if (done) {
14992
- return;
14993
- }
14994
- const event = parsePublishStreamEvent(value);
14995
- if (event) {
14996
- yield event;
14997
- }
14998
- }
14999
- } finally {
15000
- reader.releaseLock();
15001
- }
15002
- }
15003
- function parsePublishStreamEvent(event) {
15004
- if (!PublishStreamSseEventNameSchema.safeParse(event.event).success) {
15005
- return null;
15006
- }
15007
- return PublishStreamSseEventSchema.parse(event);
15008
- }
15009
- function parseSSEStream(input) {
15010
- const decoder = new TextDecoder();
15011
- let buffer = "";
15012
- return input.pipeThrough(
15013
- new TransformStream({
15014
- transform(chunk, controller) {
15015
- buffer += decoder.decode(chunk, { stream: true });
15016
- const blocks = buffer.split("\n\n");
15017
- buffer = blocks.pop() ?? "";
15018
- for (const block of blocks) {
15019
- const event = parseSSEBlock(block);
15020
- if (event) {
15021
- controller.enqueue(event);
15022
- }
15023
- }
15024
- },
15025
- flush(controller) {
15026
- buffer += decoder.decode();
15027
- if (buffer.trim() !== "") {
15028
- const event = parseSSEBlock(buffer);
15029
- if (event) {
15030
- controller.enqueue(event);
15031
- }
15032
- }
15033
- }
15034
- })
15035
- );
15036
- }
15037
- function parseSSEBlock(block) {
15038
- let eventType = "message";
15039
- let data = "";
15040
- for (const line of block.split("\n")) {
15041
- if (line.startsWith("event: ")) {
15042
- eventType = line.slice(7).trim();
15043
- } else if (line.startsWith("data: ")) {
15044
- data += line.slice(6);
15045
- } else if (line.startsWith("data:")) {
15046
- data += line.slice(5);
15047
- }
15048
- }
15049
- if (data === "") {
15050
- return null;
15051
- }
15052
- try {
15053
- return { event: eventType, data: JSON.parse(data) };
15054
- } catch {
15055
- return { event: eventType, data };
15056
- }
15057
- }
15058
15120
 
15059
15121
  // src/publication/resources.ts
15060
15122
  var PublicationResources = class {
@@ -15146,6 +15208,13 @@ var PublicationInstance = class _PublicationInstance {
15146
15208
  follow() {
15147
15209
  return new PublicationRun(this, this.client).start();
15148
15210
  }
15211
+ /** Open the original publishing SSE bytes for an authenticated, transparent proxy. */
15212
+ async stream(signal) {
15213
+ if (!this.data.eventStreamUrl) {
15214
+ throw new PHPSandboxError(`Publication ${this.data.id} has no eventStreamUrl for SSE streaming`);
15215
+ }
15216
+ return this.client.sse(this.data.eventStreamUrl, signal);
15217
+ }
15149
15218
  async destroy(options = {}) {
15150
15219
  const query = options.deleteResources ? "?deleteResources=1" : "";
15151
15220
  await this.client.delete(`/publications/${encodeURIComponent(this.data.id)}${query}`);
@@ -19613,6 +19682,16 @@ var ServerInstance = class {
19613
19682
  async installation() {
19614
19683
  return (await this.client.get(`/servers/${encodeURIComponent(this.data.id)}/installation`)).data;
19615
19684
  }
19685
+ async members(options = {}) {
19686
+ const query = options.page === void 0 ? "" : `?${new URLSearchParams({ page: String(options.page) }).toString()}`;
19687
+ return this.client.get(`/servers/${encodeURIComponent(this.data.id)}/members${query}`);
19688
+ }
19689
+ async share(email3) {
19690
+ return (await this.client.post(`/servers/${encodeURIComponent(this.data.id)}/members`, { email: email3 })).data;
19691
+ }
19692
+ async revokeAccess(memberId) {
19693
+ await this.client.delete(`/servers/${encodeURIComponent(this.data.id)}/members/${encodeURIComponent(String(memberId))}`);
19694
+ }
19616
19695
  refresh() {
19617
19696
  return this.client.servers.get(this.data.id);
19618
19697
  }
@@ -20618,9 +20697,11 @@ export {
20618
20697
  PublicationRun,
20619
20698
  RemoteError,
20620
20699
  TransportError,
20700
+ createPublicationStreamResponse,
20621
20701
  laravelCloudSetupForRepublish,
20622
20702
  notebookBuiltinServices,
20623
- notebookKnownServices
20703
+ notebookKnownServices,
20704
+ readPublicationStream
20624
20705
  };
20625
20706
  /*! Bundled license information:
20626
20707