@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 +38 -0
- package/dist/browser/phpsandbox-sdk.esm.js +155 -74
- package/dist/browser/phpsandbox-sdk.esm.min.js +24 -26
- package/dist/browser/phpsandbox-sdk.iife.js +155 -74
- package/dist/browser/phpsandbox-sdk.iife.min.js +9 -11
- package/dist/index.d.ts +1 -1
- package/dist/publication/index.d.ts +1 -0
- package/dist/publication/index.js +1 -0
- package/dist/publication/instance.d.ts +2 -0
- package/dist/publication/instance.js +7 -0
- package/dist/publication/run.js +2 -76
- package/dist/publication/stream.d.ts +18 -0
- package/dist/publication/stream.js +168 -0
- package/dist/servers.d.ts +10 -0
- package/dist/servers.js +10 -0
- package/package.json +6 -1
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
|
|
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
|
|