@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/dist/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type { ConfigPortMapping, ProjectConfig } from './config.js';
|
|
|
34
34
|
export { PHPSandboxError, RemoteError, TransportError } from './errors/index.js';
|
|
35
35
|
export type { HttpResponseDiagnostics, RemoteErrorCode, RemoteErrorSource, TransportErrorCode, ValidationErrorDetails, } from './errors/index.js';
|
|
36
36
|
export * from './publication/index.js';
|
|
37
|
-
export type { CreateServerInput, ServerData, ServerInstance, ServerListOptions, ServerSshOptions } from './servers.js';
|
|
37
|
+
export type { CreateServerInput, ServerData, ServerInstance, ServerListOptions, ServerMemberData, ServerSshOptions } from './servers.js';
|
|
38
38
|
export type { ConfigureFeedbackInput, FeedbackData, FeedbackListOptions, FeedbackSource, FeedbackStatus, FeedbackType, FeedbackWidgetConfig, FeedbackWidgetTarget, SubmitFeedbackInput, UpdateFeedbackInput, } from './feedback.js';
|
|
39
39
|
export type { Integration, IntegrationAuthorization, IntegrationAuthorizationType, IntegrationData, IntegrationProvider, IntegrationReference, LinkIntegrationInput, UpdateIntegrationInput, AttachSandboxIntegrationInput, SandboxIntegration, SandboxIntegrationData, SandboxIntegrationEffect, SandboxIntegrationReference, UpdateSandboxIntegrationInput, } from './integrations/index.js';
|
|
40
40
|
interface Result<TType extends 'success' | 'error' | 'running', TData extends object> {
|
|
@@ -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}`);
|
package/dist/publication/run.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { PublishStreamSseEventNameSchema, PublishStreamSseEventSchema } from '../schemas/publications.js';
|
|
2
|
+
import { PHPSandboxError } from '../errors/index.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 async function* readPublicationStream(input, options = {}) {
|
|
9
|
+
if (input instanceof Response && (!input.ok || !input.body)) {
|
|
10
|
+
await input.body?.cancel().catch(() => undefined);
|
|
11
|
+
throw new PHPSandboxError(`Publishing stream is unavailable (HTTP ${input.status}).`);
|
|
12
|
+
}
|
|
13
|
+
const stream = input instanceof Response ? input.body : input;
|
|
14
|
+
const reader = stream.getReader();
|
|
15
|
+
const decoder = new TextDecoder();
|
|
16
|
+
let buffer = '';
|
|
17
|
+
let eventName = 'message';
|
|
18
|
+
let data = [];
|
|
19
|
+
let ended = false;
|
|
20
|
+
let receivedResult = false;
|
|
21
|
+
function dispatch() {
|
|
22
|
+
const name = eventName;
|
|
23
|
+
const lines = data;
|
|
24
|
+
eventName = 'message';
|
|
25
|
+
data = [];
|
|
26
|
+
if (!lines.length || !PublishStreamSseEventNameSchema.safeParse(name).success)
|
|
27
|
+
return null;
|
|
28
|
+
const raw = lines.join('\n');
|
|
29
|
+
let payload;
|
|
30
|
+
try {
|
|
31
|
+
payload = JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
payload = raw;
|
|
35
|
+
}
|
|
36
|
+
const event = PublishStreamSseEventSchema.parse({ event: name, data: payload });
|
|
37
|
+
if (event.type === 'result')
|
|
38
|
+
receivedResult = true;
|
|
39
|
+
return event;
|
|
40
|
+
}
|
|
41
|
+
function line(value) {
|
|
42
|
+
if (value === '')
|
|
43
|
+
return dispatch();
|
|
44
|
+
if (value.startsWith(':'))
|
|
45
|
+
return null;
|
|
46
|
+
const colon = value.indexOf(':');
|
|
47
|
+
const field = colon < 0 ? value : value.slice(0, colon);
|
|
48
|
+
let content = colon < 0 ? '' : value.slice(colon + 1);
|
|
49
|
+
if (content.startsWith(' '))
|
|
50
|
+
content = content.slice(1);
|
|
51
|
+
if (field === 'event')
|
|
52
|
+
eventName = content;
|
|
53
|
+
if (field === 'data')
|
|
54
|
+
data.push(content);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
while (true) {
|
|
59
|
+
const { done, value } = await reader.read();
|
|
60
|
+
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
61
|
+
// A CR at the end of a chunk may be the first half of CRLF.
|
|
62
|
+
let match;
|
|
63
|
+
while ((match = /\r\n|\r|\n/.exec(buffer))) {
|
|
64
|
+
if (!done && match[0] === '\r' && match.index === buffer.length - 1)
|
|
65
|
+
break;
|
|
66
|
+
const event = line(buffer.slice(0, match.index));
|
|
67
|
+
buffer = buffer.slice(match.index + match[0].length);
|
|
68
|
+
if (event)
|
|
69
|
+
yield event;
|
|
70
|
+
}
|
|
71
|
+
if (done) {
|
|
72
|
+
ended = true;
|
|
73
|
+
if (buffer) {
|
|
74
|
+
const event = line(buffer);
|
|
75
|
+
if (event)
|
|
76
|
+
yield event;
|
|
77
|
+
}
|
|
78
|
+
// Preserve the SDK's support for a final event without a trailing blank line.
|
|
79
|
+
const event = dispatch();
|
|
80
|
+
if (event)
|
|
81
|
+
yield event;
|
|
82
|
+
if (options.requireResult !== false && !receivedResult) {
|
|
83
|
+
throw new PHPSandboxError('Publish stream ended without result');
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
if (!ended)
|
|
91
|
+
await reader.cancel().catch(() => undefined);
|
|
92
|
+
reader.releaseLock();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Forward the original bytes with SSE headers and exactly-once resource cleanup.
|
|
96
|
+
* Works with Fetch-based servers and frameworks that accept a standard Response.
|
|
97
|
+
* Does not parse, buffer a replay, or re-encode events. Upstream errors fail the body.
|
|
98
|
+
*/
|
|
99
|
+
export function createPublicationStreamResponse(stream, options = {}) {
|
|
100
|
+
const reader = stream.getReader();
|
|
101
|
+
let disposed = false;
|
|
102
|
+
let finished = false;
|
|
103
|
+
let cancellation;
|
|
104
|
+
let abort;
|
|
105
|
+
async function dispose() {
|
|
106
|
+
if (disposed)
|
|
107
|
+
return;
|
|
108
|
+
disposed = true;
|
|
109
|
+
options.signal?.removeEventListener('abort', abort);
|
|
110
|
+
reader.releaseLock();
|
|
111
|
+
await options.onDispose?.();
|
|
112
|
+
}
|
|
113
|
+
function cancel(reason) {
|
|
114
|
+
return cancellation ?? (cancellation = (async () => {
|
|
115
|
+
finished = true;
|
|
116
|
+
try {
|
|
117
|
+
await reader.cancel(reason);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await dispose();
|
|
121
|
+
}
|
|
122
|
+
})());
|
|
123
|
+
}
|
|
124
|
+
return new Response(new ReadableStream({
|
|
125
|
+
start(controller) {
|
|
126
|
+
abort = () => {
|
|
127
|
+
controller.error(options.signal.reason);
|
|
128
|
+
void cancel(options.signal.reason).catch(() => undefined);
|
|
129
|
+
};
|
|
130
|
+
if (options.signal?.aborted)
|
|
131
|
+
abort();
|
|
132
|
+
else
|
|
133
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
134
|
+
},
|
|
135
|
+
async pull(controller) {
|
|
136
|
+
try {
|
|
137
|
+
const { done, value } = await reader.read();
|
|
138
|
+
if (finished)
|
|
139
|
+
return;
|
|
140
|
+
if (done) {
|
|
141
|
+
finished = true;
|
|
142
|
+
await dispose();
|
|
143
|
+
controller.close();
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
controller.enqueue(value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (finished && cancellation)
|
|
151
|
+
return;
|
|
152
|
+
try {
|
|
153
|
+
await cancel(error);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
controller.error(error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
cancel,
|
|
161
|
+
}, { highWaterMark: 0 }), {
|
|
162
|
+
headers: {
|
|
163
|
+
'Content-Type': 'text/event-stream',
|
|
164
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
165
|
+
'X-Accel-Buffering': 'no',
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
package/dist/servers.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface ServerData {
|
|
|
5
5
|
host: string;
|
|
6
6
|
provider: string;
|
|
7
7
|
status: string;
|
|
8
|
+
access: 'owner' | 'deployer';
|
|
8
9
|
token?: string;
|
|
9
10
|
sshPublicKey?: string;
|
|
10
11
|
installCommand?: string;
|
|
@@ -12,6 +13,12 @@ export interface ServerData {
|
|
|
12
13
|
createdAt: string | null;
|
|
13
14
|
updatedAt: string | null;
|
|
14
15
|
}
|
|
16
|
+
export interface ServerMemberData {
|
|
17
|
+
id: number;
|
|
18
|
+
name: string;
|
|
19
|
+
email: string;
|
|
20
|
+
access: 'deployer';
|
|
21
|
+
}
|
|
15
22
|
export interface ServerSshOptions {
|
|
16
23
|
user?: string;
|
|
17
24
|
port?: number;
|
|
@@ -38,6 +45,9 @@ export declare class ServerInstance {
|
|
|
38
45
|
installation(): Promise<{
|
|
39
46
|
installCommand: string;
|
|
40
47
|
}>;
|
|
48
|
+
members(options?: ServerListOptions): Promise<PaginatedApiResponse<ServerMemberData>>;
|
|
49
|
+
share(email: string): Promise<ServerMemberData>;
|
|
50
|
+
revokeAccess(memberId: number): Promise<void>;
|
|
41
51
|
refresh(): Promise<ServerInstance>;
|
|
42
52
|
delete(): Promise<void>;
|
|
43
53
|
waitReady(options?: {
|
package/dist/servers.js
CHANGED
|
@@ -23,6 +23,16 @@ export class ServerInstance {
|
|
|
23
23
|
async installation() {
|
|
24
24
|
return (await this.client.get(`/servers/${encodeURIComponent(this.data.id)}/installation`)).data;
|
|
25
25
|
}
|
|
26
|
+
async members(options = {}) {
|
|
27
|
+
const query = options.page === undefined ? '' : `?${new URLSearchParams({ page: String(options.page) }).toString()}`;
|
|
28
|
+
return this.client.get(`/servers/${encodeURIComponent(this.data.id)}/members${query}`);
|
|
29
|
+
}
|
|
30
|
+
async share(email) {
|
|
31
|
+
return (await this.client.post(`/servers/${encodeURIComponent(this.data.id)}/members`, { email })).data;
|
|
32
|
+
}
|
|
33
|
+
async revokeAccess(memberId) {
|
|
34
|
+
await this.client.delete(`/servers/${encodeURIComponent(this.data.id)}/members/${encodeURIComponent(String(memberId))}`);
|
|
35
|
+
}
|
|
26
36
|
refresh() {
|
|
27
37
|
return this.client.servers.get(this.data.id);
|
|
28
38
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phpsandbox/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "The TypeScript SDK for PHPSandbox cloud development environments",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"import": "./dist/index.js",
|
|
13
13
|
"default": "./dist/index.js"
|
|
14
14
|
},
|
|
15
|
+
"./stream": {
|
|
16
|
+
"types": "./dist/publication/stream.d.ts",
|
|
17
|
+
"import": "./dist/publication/stream.js",
|
|
18
|
+
"default": "./dist/publication/stream.js"
|
|
19
|
+
},
|
|
15
20
|
"./beacon": {
|
|
16
21
|
"types": "./dist/beacon/index.d.ts",
|
|
17
22
|
"import": "./dist/beacon/index.js",
|